Iluvatar-mrv100 SDK 4.3.0
This commit is contained in:
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM git.modelhub.org.cn:9443/enginex-iluvatar/mr-bi150-4.3.0-x86-ubuntu20.04-py3.10-poc-llm-infer:v1.2.3
|
||||||
|
|
||||||
|
RUN mkdir /workspace
|
||||||
|
WORKDIR /workspace/
|
||||||
|
|
||||||
|
COPY ./launch_service /workspace/launch_service
|
||||||
|
|
||||||
|
ENTRYPOINT ["./launch_service"]
|
||||||
112
README.md
112
README.md
@@ -1 +1,111 @@
|
|||||||
# README
|
# 天数智芯 智铠100 文本生成引擎(基于 vLLM 优化)
|
||||||
|
|
||||||
|
本项目是为**天数智芯-智铠100**加速卡深度优化的高性能文本生成推理引擎,基于开源 **vLLM** 框架进行架构级适配与增强,实现对 **Qwen3 系列**等最新大模型的高效支持。通过引入 **Prefix Caching**、PagedAttention 等先进优化技术,显著提升吞吐与响应速度,同时提供标准 **OpenAI 兼容 API 接口**,便于无缝集成现有应用生态。
|
||||||
|
|
||||||
|
## 支持模型
|
||||||
|
|
||||||
|
- **Qwen3**
|
||||||
|
- **Llama3**
|
||||||
|
- **DeepSeek-R1-Distill**
|
||||||
|
- 其他兼容 vLLM 的 HuggingFace 模型(持续扩展中)
|
||||||
|
|
||||||
|
> 模型下载地址:[https://modelscope.cn/models/Qwen](https://modelscope.cn/models/Qwen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. 模型下载
|
||||||
|
|
||||||
|
从 ModelScope 下载所需模型(以 Qwen2.5-7B-Instruct 为例):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
modelscope download --model qwen/Qwen2.5-7B-Instruct README.md --local_dir /mnt/models/Qwen2.5-7B-Instruct
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 请确保模型路径在后续 Docker 启动时正确挂载。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 拉取并构建 Docker 镜像
|
||||||
|
|
||||||
|
我们提供已预装智铠100驱动与vLLM优化版本的Docker镜像:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 本地构建
|
||||||
|
docker build -t enginex-iluvatar-vllm:bi100 -f Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 启动服务容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm -p 8000:80 \
|
||||||
|
--name vllm-iluvatar \
|
||||||
|
-v /mnt/models/Qwen2.5-7B-Instruct:/model:ro \
|
||||||
|
--privileged \
|
||||||
|
-e TENSOR_PARALLEL_SIZE=1 \
|
||||||
|
-e PREFIX_CACHING=true \
|
||||||
|
-e MAX_MODEL_LEN=10000 \
|
||||||
|
enginex-iluvatar-vllm:bi100
|
||||||
|
```
|
||||||
|
|
||||||
|
> ✅ 参数说明:
|
||||||
|
> - `PREFIX_CACHING=true`: 启用 Prefix Caching 优化,显著提升多请求共享前缀的推理效率
|
||||||
|
> - `MAX_MODEL_LEN=10000`: 支持长上下文推理
|
||||||
|
> - `--privileged`: 确保智铠100设备可见
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 测试服务(使用 OpenAI 兼容接口)
|
||||||
|
|
||||||
|
服务启动后,可通过标准 OpenAI SDK 或 `curl` 进行测试。
|
||||||
|
|
||||||
|
### 示例:文本生成请求
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/v1/chat/completions \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"model": "qwen3-8b",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "请用中文介绍一下上海的特点。"}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 512
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 OpenAI Python SDK(需安装 `openai>=1.0`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="qwen3-8b",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "请简要介绍杭州的特色文化。"}
|
||||||
|
],
|
||||||
|
max_tokens=512,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试结果对比(A100 vs 智铠100)
|
||||||
|
|
||||||
|
在相同模型和输入条件下,测试平均输出速度(单位:字每秒),结果如下:
|
||||||
|
|
||||||
|
| 模型 | 智铠100 输出速度 | Nvidia A100 输出速度 |
|
||||||
|
|--------|--------------------------|-------------------------------|
|
||||||
|
| Qwen2.5-7B-Instruct | 56.4 | 112.4 |
|
||||||
|
| Qwen2.5-1.5B-Instruct-AWQ | 123.1 | 100.8 |
|
||||||
|
|
||||||
|
|||||||
79
launch_service
Executable file
79
launch_service
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export PYTHONPATH=/usr/local/corex/lib64/python3/dist-packages
|
||||||
|
export LD_LIBRARY_PATH=/usr/local/corex/lib64:/usr/local/openmpi/lib
|
||||||
|
export PATH=/usr/local/corex/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/corex/lib64/python3/dist-packages/bin:/usr/local/openmpi/bin
|
||||||
|
export JAVA_HOME=/root/apps/jdk1.8.0_411
|
||||||
|
export JRE_HOME=/root/apps/jdk1.8.0_411/jre
|
||||||
|
export JMETER_HOME=/root/apps/apache-jmeter-5.6.3
|
||||||
|
export CLASSPATH=.:/root/apps/jdk1.8.0_411/lib/dt.jar:/root/apps/jdk1.8.0_411/lib/tools.jar:/root/apps/apache-jmeter-5.6.3/lib/ext/ApacheJMeter_core.jar:/root/apps/apache-jmeter-5.6.3/lib/jorphan.jar:/root/apps/apache-jmeter-5.6.3/lib/logkit-2.0.jar:
|
||||||
|
export PATH=/root/apps/apache-jmeter-5.6.3/bin:/root/apps/jdk1.8.0_411/bin:/usr/local/corex/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/corex/lib64/python3/dist-packages/bin:/usr/local/openmpi/bin
|
||||||
|
/iluvatar/welcome.sh
|
||||||
|
|
||||||
|
data
|
||||||
|
cat /proc/cpuinfo | tail -n 50
|
||||||
|
ixsmi
|
||||||
|
unset CUDA_VISIBLE_DEVICES
|
||||||
|
export
|
||||||
|
date
|
||||||
|
|
||||||
|
DEFAULT_HOST="0.0.0.0"
|
||||||
|
DEFAULT_PORT="80"
|
||||||
|
DEFAULT_SERVED_MODEL_NAME="llm"
|
||||||
|
DEFAULT_MODEL_PATH="/model"
|
||||||
|
DEFAULT_MAX_MODEL_LEN="10000"
|
||||||
|
DEFAULT_TENSOR_PARALLEL_SIZE="1"
|
||||||
|
DEFAULT_MAX_NUM_SEQS="64"
|
||||||
|
DEFAULT_ENFORCE_EAGER="true"
|
||||||
|
DEFAULT_DISABLE_LOG_REQUESTS="true"
|
||||||
|
DEFAULT_PREFIX_CACHING="true"
|
||||||
|
|
||||||
|
HOST_VAL=${HOST:-$DEFAULT_HOST}
|
||||||
|
PORT_VAL=${PORT:-$DEFAULT_PORT}
|
||||||
|
SERVED_MODEL_NAME_VAL=${SERVED_MODEL_NAME:-$DEFAULT_SERVED_MODEL_NAME}
|
||||||
|
MODEL_PATH_VAL=${MODEL_PATH:-$DEFAULT_MODEL_PATH}
|
||||||
|
MAX_MODEL_LEN_VAL=${MAX_MODEL_LEN:-$DEFAULT_MAX_MODEL_LEN}
|
||||||
|
TENSOR_PARALLEL_SIZE_VAL=${TENSOR_PARALLEL_SIZE:-$DEFAULT_TENSOR_PARALLEL_SIZE}
|
||||||
|
MAX_NUM_SEQS_VAL=${MAX_NUM_SEQS:-$DEFAULT_MAX_NUM_SEQS}
|
||||||
|
INCLUDE_ENFORCE_EAGER_FLAG=${ENFORCE_EAGER:-$DEFAULT_ENFORCE_EAGER}
|
||||||
|
INCLUDE_DISABLE_LOG_REQUESTS_FLAG=${DISABLE_LOG_REQUESTS:-$DEFAULT_DISABLE_LOG_REQUESTS}
|
||||||
|
INCLUDE_PREFIX_CACHING_FLAG=${PREFIX_CACHING:-$DEFAULT_PREFIX_CACHING}
|
||||||
|
|
||||||
|
CMD_ARGS=()
|
||||||
|
CMD_ARGS+=(--host "$HOST_VAL")
|
||||||
|
CMD_ARGS+=(--port "$PORT_VAL")
|
||||||
|
|
||||||
|
if [[ "$INCLUDE_ENFORCE_EAGER_FLAG" != "false" && "$INCLUDE_ENFORCE_EAGER_FLAG" != "0" ]]; then
|
||||||
|
CMD_ARGS+=(--enforce-eager)
|
||||||
|
fi
|
||||||
|
if [[ "$INCLUDE_DISABLE_LOG_REQUESTS_FLAG" != "false" && "$INCLUDE_DISABLE_LOG_REQUESTS_FLAG" != "0" ]]; then
|
||||||
|
CMD_ARGS+=(--disable-log-requests)
|
||||||
|
fi
|
||||||
|
if [[ "$INCLUDE_PREFIX_CACHING_FLAG" != "false" && "$INCLUDE_PREFIX_CACHING_FLAG" != "0" ]]; then
|
||||||
|
CMD_ARGS+=(--enable-prefix-caching)
|
||||||
|
fi
|
||||||
|
|
||||||
|
CMD_ARGS+=(--served-model-name "$SERVED_MODEL_NAME_VAL")
|
||||||
|
CMD_ARGS+=(--model "$MODEL_PATH_VAL")
|
||||||
|
CMD_ARGS+=(--max-model-len "$MAX_MODEL_LEN_VAL")
|
||||||
|
CMD_ARGS+=(--tensor-parallel-size "$TENSOR_PARALLEL_SIZE_VAL")
|
||||||
|
CMD_ARGS+=(--max-num-seqs "$MAX_NUM_SEQS_VAL")
|
||||||
|
|
||||||
|
echo "--------------------------------------------------"
|
||||||
|
echo "Starting VLLM OpenAI API Server..."
|
||||||
|
echo "Using effective arguments:"
|
||||||
|
echo " Host (--host): $HOST_VAL"
|
||||||
|
echo " Port (--port): $PORT_VAL"
|
||||||
|
echo " Enforce Eager (--enforce-eager):" $([[ "$INCLUDE_ENFORCE_EAGER_FLAG" != "false" && "$INCLUDE_ENFORCE_EAGER_FLAG" != "0" ]] && echo "Enabled" || echo "Disabled (Env: ENFORCE_EAGER=$ENFORCE_EAGER)")
|
||||||
|
echo " Disable Log Req (--disable-log-requests):" $([[ "$INCLUDE_DISABLE_LOG_REQUESTS_FLAG" != "false" && "$INCLUDE_DISABLE_LOG_REQUESTS_FLAG" != "0" ]] && echo "Enabled" || echo "Disabled (Env: DISABLE_LOG_REQUESTS=$DISABLE_LOG_REQUESTS)")
|
||||||
|
echo " Served Model Name (--served-model-name): $SERVED_MODEL_NAME_VAL"
|
||||||
|
echo " Model Path (--model): $MODEL_PATH_VAL"
|
||||||
|
echo " Max Model Length (--max-model-len): $MAX_MODEL_LEN_VAL"
|
||||||
|
echo " Tensor Parallel Size (--tensor-parallel-size): $TENSOR_PARALLEL_SIZE_VAL"
|
||||||
|
echo " Max Num Seqs (--max-num-seqs): $MAX_NUM_SEQS_VAL"
|
||||||
|
echo "--------------------------------------------------"
|
||||||
|
echo "Full cmd:"
|
||||||
|
echo "python3 -m vllm.entrypoints.openai.api_server ${CMD_ARGS[*]}"
|
||||||
|
echo "--------------------------------------------------"
|
||||||
|
|
||||||
|
python3 -m vllm.entrypoints.openai.api_server "${CMD_ARGS[@]}"
|
||||||
52
vllm/__init__.py
Normal file
52
vllm/__init__.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""vLLM: a high-throughput and memory-efficient inference engine for LLMs"""
|
||||||
|
# The version.py should be independent library, and we always import the
|
||||||
|
# version library first. Such assumption is critical for some customization.
|
||||||
|
from .version import __version__, __version_tuple__ # isort:skip
|
||||||
|
|
||||||
|
# The environment variables override should be imported before any other
|
||||||
|
# modules to ensure that the environment variables are set before any
|
||||||
|
# other modules are imported.
|
||||||
|
import vllm.env_override # isort:skip # noqa: F401
|
||||||
|
|
||||||
|
from vllm.engine.arg_utils import AsyncEngineArgs, EngineArgs
|
||||||
|
from vllm.engine.async_llm_engine import AsyncLLMEngine
|
||||||
|
from vllm.engine.llm_engine import LLMEngine
|
||||||
|
from vllm.entrypoints.llm import LLM
|
||||||
|
from vllm.executor.ray_utils import initialize_ray_cluster
|
||||||
|
from vllm.inputs import PromptType, TextPrompt, TokensPrompt
|
||||||
|
from vllm.model_executor.models import ModelRegistry
|
||||||
|
from vllm.outputs import (ClassificationOutput, ClassificationRequestOutput,
|
||||||
|
CompletionOutput, EmbeddingOutput,
|
||||||
|
EmbeddingRequestOutput, PoolingOutput,
|
||||||
|
PoolingRequestOutput, RequestOutput, ScoringOutput,
|
||||||
|
ScoringRequestOutput)
|
||||||
|
from vllm.pooling_params import PoolingParams
|
||||||
|
from vllm.sampling_params import SamplingParams
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"__version_tuple__",
|
||||||
|
"LLM",
|
||||||
|
"ModelRegistry",
|
||||||
|
"PromptType",
|
||||||
|
"TextPrompt",
|
||||||
|
"TokensPrompt",
|
||||||
|
"SamplingParams",
|
||||||
|
"RequestOutput",
|
||||||
|
"CompletionOutput",
|
||||||
|
"PoolingOutput",
|
||||||
|
"PoolingRequestOutput",
|
||||||
|
"EmbeddingOutput",
|
||||||
|
"EmbeddingRequestOutput",
|
||||||
|
"ClassificationOutput",
|
||||||
|
"ClassificationRequestOutput",
|
||||||
|
"ScoringOutput",
|
||||||
|
"ScoringRequestOutput",
|
||||||
|
"LLMEngine",
|
||||||
|
"EngineArgs",
|
||||||
|
"AsyncLLMEngine",
|
||||||
|
"AsyncEngineArgs",
|
||||||
|
"initialize_ray_cluster",
|
||||||
|
"PoolingParams",
|
||||||
|
]
|
||||||
1570
vllm/_custom_ops.py
Normal file
1570
vllm/_custom_ops.py
Normal file
File diff suppressed because it is too large
Load Diff
241
vllm/_ipex_ops.py
Normal file
241
vllm/_ipex_ops.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import intel_extension_for_pytorch as ipex
|
||||||
|
except ImportError as e:
|
||||||
|
logger.warning("Import error msg: %s", e.msg)
|
||||||
|
|
||||||
|
|
||||||
|
class ipex_ops:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reshape_activation_tensor(
|
||||||
|
x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
num = x.size(0)
|
||||||
|
d = x.size(1) // 2
|
||||||
|
x = x.reshape(num, 2, d)
|
||||||
|
x1, x2 = torch.chunk(x, chunks=2, dim=1)
|
||||||
|
x1 = x1.reshape(num, d)
|
||||||
|
x2 = x2.reshape(num, d)
|
||||||
|
return x1, x2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def silu_and_mul(out: torch.Tensor, x: torch.Tensor) -> None:
|
||||||
|
ipex.llm.functional.silu_and_mul(x, out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gelu_and_mul(out: torch.Tensor, x: torch.Tensor) -> None:
|
||||||
|
ipex.llm.functional.gelu_and_mul(x, out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gelu_tanh_and_mul(out: torch.Tensor, x: torch.Tensor) -> None:
|
||||||
|
ipex.llm.functional.gelu_and_mul(x, out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gelu_fast(x: torch.Tensor) -> torch.Tensor:
|
||||||
|
return torch.nn.functional.gelu(x)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gelu_new(x: torch.Tensor) -> torch.Tensor:
|
||||||
|
return torch.nn.functional.gelu(x)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gelu_quick(out: torch.Tensor, x: torch.Tensor) -> None:
|
||||||
|
ipex.llm.functional.gelu_quick(x, out)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def paged_attention_v1(
|
||||||
|
out: torch.Tensor,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
block_size: int,
|
||||||
|
max_context_len: int,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
tp_rank: int = 0,
|
||||||
|
blocksparse_local_blocks: int = 0,
|
||||||
|
blocksparse_vert_stride: int = 0,
|
||||||
|
blocksparse_block_size: int = 64,
|
||||||
|
blocksparse_head_sliding_step: int = 0,
|
||||||
|
) -> None:
|
||||||
|
assert kv_cache_dtype == "auto"
|
||||||
|
num_heads = out.size(1)
|
||||||
|
num_queries_per_tokens = num_heads // num_kv_heads
|
||||||
|
ipex.llm.modules.PagedAttention.single_query_kv_attention(
|
||||||
|
out,
|
||||||
|
query.contiguous(),
|
||||||
|
key_cache.view_as(value_cache),
|
||||||
|
value_cache,
|
||||||
|
num_queries_per_tokens,
|
||||||
|
scale,
|
||||||
|
block_tables,
|
||||||
|
context_lens,
|
||||||
|
block_size,
|
||||||
|
max_context_len,
|
||||||
|
alibi_slopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def paged_attention_v2(
|
||||||
|
out: torch.Tensor,
|
||||||
|
exp_sum: torch.Tensor,
|
||||||
|
max_logits: torch.Tensor,
|
||||||
|
tmp_out: torch.Tensor,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
block_size: int,
|
||||||
|
max_context_len: int,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
tp_rank: int = 0,
|
||||||
|
blocksparse_local_blocks: int = 0,
|
||||||
|
blocksparse_vert_stride: int = 0,
|
||||||
|
blocksparse_block_size: int = 64,
|
||||||
|
blocksparse_head_sliding_step: int = 0,
|
||||||
|
) -> None:
|
||||||
|
assert kv_cache_dtype == "auto"
|
||||||
|
num_heads = out.size(1)
|
||||||
|
num_queries_per_tokens = num_heads // num_kv_heads
|
||||||
|
ipex.llm.modules.PagedAttention.single_query_kv_attention(
|
||||||
|
out,
|
||||||
|
query.contiguous(),
|
||||||
|
key_cache.view_as(value_cache),
|
||||||
|
value_cache,
|
||||||
|
num_queries_per_tokens,
|
||||||
|
scale,
|
||||||
|
block_tables,
|
||||||
|
context_lens,
|
||||||
|
block_size,
|
||||||
|
max_context_len,
|
||||||
|
alibi_slopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rotary_embedding(
|
||||||
|
positions: torch.Tensor, # [batch_size, seq_len]
|
||||||
|
query: torch.Tensor, # [batch_size, seq_len, num_heads*head_size]
|
||||||
|
key: torch.Tensor, # [batch_size, seq_len, num_kv_heads*head_size]
|
||||||
|
head_size: int,
|
||||||
|
cos_sin_cache: torch.Tensor, # [cos_sin_dim, rot_dim]
|
||||||
|
is_neox: bool,
|
||||||
|
) -> None:
|
||||||
|
rot_dim = cos_sin_cache.size(1)
|
||||||
|
ipex.llm.functional.rotary_embedding_batched(positions, query, key,
|
||||||
|
head_size, cos_sin_cache,
|
||||||
|
is_neox, rot_dim)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def batched_rotary_embedding(positions: torch.Tensor, query: torch.Tensor,
|
||||||
|
key: torch.Tensor, head_size: int,
|
||||||
|
cos_sin_cache: torch.Tensor, is_neox: bool,
|
||||||
|
rot_dim: int,
|
||||||
|
cos_sin_cache_offsets: torch.Tensor) -> None:
|
||||||
|
ipex.llm.functional.rotary_embedding_batched(positions, query, key,
|
||||||
|
head_size, cos_sin_cache,
|
||||||
|
is_neox, rot_dim,
|
||||||
|
cos_sin_cache_offsets)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rms_norm(input: torch.Tensor, weight: torch.Tensor,
|
||||||
|
epsilon: float) -> torch.Tensor:
|
||||||
|
return ipex.llm.functional.rms_norm(input, weight, epsilon)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fused_add_rms_norm(input: torch.Tensor, residual: torch.Tensor,
|
||||||
|
weight: torch.Tensor, epsilon: float) -> None:
|
||||||
|
tmp = ipex.llm.functional.add_rms_norm(residual, input, weight, None,
|
||||||
|
epsilon, True)
|
||||||
|
input.copy_(tmp)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def varlen_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
out: torch.Tensor,
|
||||||
|
seqlen_q: torch.Tensor,
|
||||||
|
seqlen_k: torch.Tensor,
|
||||||
|
max_seqlen_q: int,
|
||||||
|
max_seqlen_k: int,
|
||||||
|
pdropout: float,
|
||||||
|
softmax_scale: float,
|
||||||
|
zero_tensors: bool,
|
||||||
|
is_causal: bool,
|
||||||
|
return_softmax: bool,
|
||||||
|
gen_: torch.Generator,
|
||||||
|
logits_soft_cap: float,
|
||||||
|
) -> None:
|
||||||
|
if ipex.__version__.endswith("cpu"):
|
||||||
|
if logits_soft_cap != 0.0:
|
||||||
|
raise ValueError("IPEX CPU does not support logits_soft_cap")
|
||||||
|
ipex.llm.functional.varlen_attention(query.contiguous(),
|
||||||
|
key.contiguous(),
|
||||||
|
value.contiguous(), out,
|
||||||
|
seqlen_q.int(),
|
||||||
|
seqlen_k.int(), max_seqlen_q,
|
||||||
|
max_seqlen_k, pdropout,
|
||||||
|
softmax_scale, zero_tensors,
|
||||||
|
is_causal, return_softmax,
|
||||||
|
gen_)
|
||||||
|
else: # XPU build
|
||||||
|
ipex.llm.functional.varlen_attention(query.contiguous(),
|
||||||
|
key.contiguous(),
|
||||||
|
value.contiguous(), out,
|
||||||
|
seqlen_q.int(),
|
||||||
|
seqlen_k.int(), max_seqlen_q,
|
||||||
|
max_seqlen_k, pdropout,
|
||||||
|
softmax_scale, zero_tensors,
|
||||||
|
is_causal, return_softmax,
|
||||||
|
gen_, logits_soft_cap)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reshape_and_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
) -> None:
|
||||||
|
assert kv_cache_dtype == "auto"
|
||||||
|
ipex.llm.modules.PagedAttention.reshape_and_cache(
|
||||||
|
key, value, key_cache, value_cache, slot_mapping)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(key_caches: list[torch.Tensor],
|
||||||
|
value_caches: list[torch.Tensor],
|
||||||
|
block_mapping: torch.Tensor) -> None:
|
||||||
|
torch.xpu.copy_blocks( # type: ignore
|
||||||
|
key_caches,
|
||||||
|
value_caches,
|
||||||
|
block_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(src: torch.Tensor, dst: torch.Tensor,
|
||||||
|
block_mapping: torch.Tensor) -> None:
|
||||||
|
torch.xpu.swap_blocks(src, dst, block_mapping) # type: ignore
|
||||||
0
vllm/adapter_commons/__init__.py
Normal file
0
vllm/adapter_commons/__init__.py
Normal file
16
vllm/adapter_commons/layers.py
Normal file
16
vllm/adapter_commons/layers.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdapterMapping:
|
||||||
|
# Per every token in input_ids:
|
||||||
|
index_mapping: Tuple[int, ...]
|
||||||
|
# Per sampled token:
|
||||||
|
prompt_mapping: Tuple[int, ...]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.index_mapping = tuple(self.index_mapping)
|
||||||
|
self.prompt_mapping = tuple(self.prompt_mapping)
|
||||||
105
vllm/adapter_commons/models.py
Normal file
105
vllm/adapter_commons/models.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Callable, Dict, Optional, TypeVar
|
||||||
|
|
||||||
|
from torch import nn
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.utils import LRUCache
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterModel(ABC):
|
||||||
|
|
||||||
|
def __init__(self, model_id=None):
|
||||||
|
self.id = model_id
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def from_local_checkpoint(cls, model_dir, model_id=None, **kwargs):
|
||||||
|
# Common initialization code
|
||||||
|
# Load weights or embeddings from local checkpoint
|
||||||
|
raise NotImplementedError("Subclasses must implement this method.")
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterLRUCache(LRUCache[int, T]):
|
||||||
|
|
||||||
|
def __init__(self, capacity: int, deactivate_fn: Callable[[int], object]):
|
||||||
|
super().__init__(capacity)
|
||||||
|
self.deactivate_fn = deactivate_fn
|
||||||
|
|
||||||
|
def _on_remove(self, key: int, value: Optional[T]):
|
||||||
|
logger.debug("Removing adapter int id: %d", key)
|
||||||
|
self.deactivate_fn(key)
|
||||||
|
return super()._on_remove(key, value)
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterModelManager(ABC):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
model: nn.Module,
|
||||||
|
):
|
||||||
|
"""Create a AdapterModelManager and adapter for a given model.
|
||||||
|
Args:
|
||||||
|
model: the model to be adapted.
|
||||||
|
"""
|
||||||
|
self.model: nn.Module = model
|
||||||
|
self._registered_adapters: Dict[int, Any] = {}
|
||||||
|
# Dict instead of a Set for compatibility with LRUCache.
|
||||||
|
self._active_adapters: Dict[int, None] = {}
|
||||||
|
self.adapter_type = 'Adapter'
|
||||||
|
self._last_mapping = None
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._registered_adapters)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def adapter_slots(self) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def capacity(self) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def activate_adapter(self, adapter_id: int) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def deactivate_adapter(self, adapter_id: int) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_adapter(self, adapter: Any) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_adapter_mapping(self, mapping: Any) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_adapter(self, adapter_id: int) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_all_adapters(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_adapter(self, adapter_id: int) -> Optional[Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_adapters(self) -> Dict[int, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def pin_adapter(self, adapter_id: int) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
25
vllm/adapter_commons/request.py
Normal file
25
vllm/adapter_commons/request.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class AdapterRequest(ABC):
|
||||||
|
"""
|
||||||
|
Base class for adapter requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def adapter_id(self) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.adapter_id < 1:
|
||||||
|
raise ValueError(f"id must be > 0, got {self.adapter_id}")
|
||||||
|
|
||||||
|
def __eq__(self, value: object) -> bool:
|
||||||
|
return isinstance(
|
||||||
|
value, self.__class__) and self.adapter_id == value.adapter_id
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(self.adapter_id)
|
||||||
92
vllm/adapter_commons/utils.py
Normal file
92
vllm/adapter_commons/utils.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, Optional, Set
|
||||||
|
|
||||||
|
|
||||||
|
## model functions
|
||||||
|
def deactivate_adapter(adapter_id: int, active_adapters: Dict[int, None],
|
||||||
|
deactivate_func: Callable) -> bool:
|
||||||
|
if adapter_id in active_adapters:
|
||||||
|
deactivate_func(adapter_id)
|
||||||
|
active_adapters.pop(adapter_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_adapter(adapter: Any, registered_adapters: Dict[int, Any],
|
||||||
|
capacity: int, add_func: Callable) -> bool:
|
||||||
|
if adapter.id not in registered_adapters:
|
||||||
|
if len(registered_adapters) >= capacity:
|
||||||
|
raise RuntimeError('No free adapter slots.')
|
||||||
|
add_func(adapter)
|
||||||
|
registered_adapters[adapter.id] = adapter
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_adapter_mapping(mapping: Any, last_mapping: Any,
|
||||||
|
set_mapping_func: Callable) -> Any:
|
||||||
|
if last_mapping != mapping:
|
||||||
|
set_mapping_func(mapping)
|
||||||
|
return mapping
|
||||||
|
return last_mapping
|
||||||
|
|
||||||
|
|
||||||
|
def remove_adapter(adapter_id: int, registered_adapters: Dict[int, Any],
|
||||||
|
deactivate_func: Callable) -> bool:
|
||||||
|
deactivate_func(adapter_id)
|
||||||
|
return bool(registered_adapters.pop(adapter_id, None))
|
||||||
|
|
||||||
|
|
||||||
|
def list_adapters(registered_adapters: Dict[int, Any]) -> Dict[int, Any]:
|
||||||
|
return dict(registered_adapters)
|
||||||
|
|
||||||
|
|
||||||
|
def get_adapter(adapter_id: int,
|
||||||
|
registered_adapters: Dict[int, Any]) -> Optional[Any]:
|
||||||
|
return registered_adapters.get(adapter_id)
|
||||||
|
|
||||||
|
|
||||||
|
## worker functions
|
||||||
|
def set_active_adapters_worker(requests: Set[Any], mapping: Optional[Any],
|
||||||
|
apply_adapters_func,
|
||||||
|
set_adapter_mapping_func) -> None:
|
||||||
|
apply_adapters_func(requests)
|
||||||
|
set_adapter_mapping_func(mapping)
|
||||||
|
|
||||||
|
|
||||||
|
def add_adapter_worker(adapter_request: Any, list_adapters_func,
|
||||||
|
load_adapter_func, add_adapter_func,
|
||||||
|
activate_adapter_func) -> bool:
|
||||||
|
if adapter_request.adapter_id in list_adapters_func():
|
||||||
|
return False
|
||||||
|
loaded_adapter = load_adapter_func(adapter_request)
|
||||||
|
loaded = add_adapter_func(loaded_adapter)
|
||||||
|
activate_adapter_func(loaded_adapter.id)
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
|
||||||
|
def apply_adapters_worker(adapter_requests: Set[Any], list_adapters_func,
|
||||||
|
adapter_slots: int, remove_adapter_func,
|
||||||
|
add_adapter_func) -> None:
|
||||||
|
models_that_exist = list_adapters_func()
|
||||||
|
models_map = {
|
||||||
|
adapter_request.adapter_id: adapter_request
|
||||||
|
for adapter_request in adapter_requests if adapter_request
|
||||||
|
}
|
||||||
|
if len(models_map) > adapter_slots:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Number of requested models ({len(models_map)}) is greater "
|
||||||
|
f"than the number of GPU model slots "
|
||||||
|
f"({adapter_slots}).")
|
||||||
|
new_models = set(models_map)
|
||||||
|
models_to_add = new_models - models_that_exist
|
||||||
|
models_to_remove = models_that_exist - new_models
|
||||||
|
for adapter_id in models_to_remove:
|
||||||
|
remove_adapter_func(adapter_id)
|
||||||
|
for adapter_id in models_to_add:
|
||||||
|
add_adapter_func(models_map[adapter_id])
|
||||||
|
|
||||||
|
|
||||||
|
def list_adapters_worker(adapter_manager_list_adapters_func) -> Set[int]:
|
||||||
|
return set(adapter_manager_list_adapters_func())
|
||||||
38
vllm/adapter_commons/worker_manager.py
Normal file
38
vllm/adapter_commons/worker_manager.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Optional, Set
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractWorkerManager(ABC):
|
||||||
|
|
||||||
|
def __init__(self, device: torch.device):
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_active_adapters(self, requests: Set[Any],
|
||||||
|
mapping: Optional[Any]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_adapter(self, adapter_request: Any) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_adapter(self, adapter_id: int) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_all_adapters(self) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_adapters(self) -> Set[int]:
|
||||||
|
raise NotImplementedError
|
||||||
0
vllm/assets/__init__.py
Normal file
0
vllm/assets/__init__.py
Normal file
38
vllm/assets/audio.py
Normal file
38
vllm/assets/audio.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Literal
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
from vllm.utils import PlaceholderModule
|
||||||
|
|
||||||
|
from .base import VLLM_S3_BUCKET_URL, get_vllm_public_assets
|
||||||
|
|
||||||
|
try:
|
||||||
|
import librosa
|
||||||
|
except ImportError:
|
||||||
|
librosa = PlaceholderModule("librosa") # type: ignore[assignment]
|
||||||
|
|
||||||
|
ASSET_DIR = "multimodal_asset"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AudioAsset:
|
||||||
|
name: Literal["winning_call", "mary_had_lamb"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def audio_and_sample_rate(self) -> tuple[npt.NDArray, float]:
|
||||||
|
audio_path = get_vllm_public_assets(filename=f"{self.name}.ogg",
|
||||||
|
s3_prefix=ASSET_DIR)
|
||||||
|
return librosa.load(audio_path, sr=None)
|
||||||
|
|
||||||
|
def get_local_path(self) -> Path:
|
||||||
|
return get_vllm_public_assets(filename=f"{self.name}.ogg",
|
||||||
|
s3_prefix=ASSET_DIR)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return urljoin(VLLM_S3_BUCKET_URL, f"{ASSET_DIR}/{self.name}.ogg")
|
||||||
40
vllm/assets/base.py
Normal file
40
vllm/assets/base.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.connections import global_http_connection
|
||||||
|
|
||||||
|
VLLM_S3_BUCKET_URL = "https://vllm-public-assets.s3.us-west-2.amazonaws.com"
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_dir() -> Path:
|
||||||
|
"""Get the path to the cache for storing downloaded assets."""
|
||||||
|
path = Path(envs.VLLM_ASSETS_CACHE)
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_vllm_public_assets(filename: str,
|
||||||
|
s3_prefix: Optional[str] = None) -> Path:
|
||||||
|
"""
|
||||||
|
Download an asset file from ``s3://vllm-public-assets``
|
||||||
|
and return the path to the downloaded file.
|
||||||
|
"""
|
||||||
|
asset_directory = get_cache_dir() / "vllm_public_assets"
|
||||||
|
asset_directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
asset_path = asset_directory / filename
|
||||||
|
if not asset_path.exists():
|
||||||
|
if s3_prefix is not None:
|
||||||
|
filename = s3_prefix + "/" + filename
|
||||||
|
global_http_connection.download_file(
|
||||||
|
f"{VLLM_S3_BUCKET_URL}/{filename}",
|
||||||
|
asset_path,
|
||||||
|
timeout=envs.VLLM_IMAGE_FETCH_TIMEOUT)
|
||||||
|
|
||||||
|
return asset_path
|
||||||
31
vllm/assets/image.py
Normal file
31
vllm/assets/image.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .base import get_vllm_public_assets
|
||||||
|
|
||||||
|
VLM_IMAGES_DIR = "vision_model_images"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ImageAsset:
|
||||||
|
name: Literal["stop_sign", "cherry_blossom"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pil_image(self) -> Image.Image:
|
||||||
|
image_path = get_vllm_public_assets(filename=f"{self.name}.jpg",
|
||||||
|
s3_prefix=VLM_IMAGES_DIR)
|
||||||
|
return Image.open(image_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_embeds(self) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Image embeddings, only used for testing purposes with llava 1.5.
|
||||||
|
"""
|
||||||
|
image_path = get_vllm_public_assets(filename=f"{self.name}.pt",
|
||||||
|
s3_prefix=VLM_IMAGES_DIR)
|
||||||
|
return torch.load(image_path, map_location="cpu", weights_only=True)
|
||||||
87
vllm/assets/video.py
Normal file
87
vllm/assets/video.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from huggingface_hub import hf_hub_download
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from .base import get_cache_dir
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def download_video_asset(filename: str) -> str:
|
||||||
|
"""
|
||||||
|
Download and open an image from huggingface
|
||||||
|
repo: raushan-testing-hf/videos-test
|
||||||
|
"""
|
||||||
|
video_directory = get_cache_dir() / "video-example-data"
|
||||||
|
video_directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
video_path = video_directory / filename
|
||||||
|
video_path_str = str(video_path)
|
||||||
|
if not video_path.exists():
|
||||||
|
video_path_str = hf_hub_download(
|
||||||
|
repo_id="raushan-testing-hf/videos-test",
|
||||||
|
filename=filename,
|
||||||
|
repo_type="dataset",
|
||||||
|
cache_dir=video_directory,
|
||||||
|
)
|
||||||
|
return video_path_str
|
||||||
|
|
||||||
|
|
||||||
|
def video_to_ndarrays(path: str, num_frames: int = -1) -> npt.NDArray:
|
||||||
|
cap = cv2.VideoCapture(path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise ValueError(f"Could not open video file {path}")
|
||||||
|
|
||||||
|
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
num_frames = num_frames if num_frames > 0 else total_frames
|
||||||
|
frame_indices = np.linspace(0, total_frames - 1, num_frames, dtype=int)
|
||||||
|
for idx in range(total_frames):
|
||||||
|
ok = cap.grab() # next img
|
||||||
|
if not ok:
|
||||||
|
break
|
||||||
|
if idx in frame_indices: # only decompress needed
|
||||||
|
ret, frame = cap.retrieve()
|
||||||
|
if ret:
|
||||||
|
frames.append(frame)
|
||||||
|
|
||||||
|
frames = np.stack(frames)
|
||||||
|
if len(frames) < num_frames:
|
||||||
|
raise ValueError(f"Could not read enough frames from video file {path}"
|
||||||
|
f" (expected {num_frames} frames, got {len(frames)})")
|
||||||
|
return frames
|
||||||
|
|
||||||
|
|
||||||
|
def video_to_pil_images_list(path: str,
|
||||||
|
num_frames: int = -1) -> list[Image.Image]:
|
||||||
|
frames = video_to_ndarrays(path, num_frames)
|
||||||
|
return [
|
||||||
|
Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
||||||
|
for frame in frames
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class VideoAsset:
|
||||||
|
name: Literal["sample_demo_1.mp4"]
|
||||||
|
num_frames: int = -1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pil_images(self) -> list[Image.Image]:
|
||||||
|
video_path = download_video_asset(self.name)
|
||||||
|
ret = video_to_pil_images_list(video_path, self.num_frames)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def np_ndarrays(self) -> npt.NDArray:
|
||||||
|
video_path = download_video_asset(self.name)
|
||||||
|
ret = video_to_ndarrays(video_path, self.num_frames)
|
||||||
|
return ret
|
||||||
19
vllm/attention/__init__.py
Normal file
19
vllm/attention/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionState, AttentionType)
|
||||||
|
from vllm.attention.layer import Attention
|
||||||
|
from vllm.attention.selector import get_attn_backend
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Attention",
|
||||||
|
"AttentionBackend",
|
||||||
|
"AttentionMetadata",
|
||||||
|
"AttentionType",
|
||||||
|
"AttentionMetadataBuilder",
|
||||||
|
"Attention",
|
||||||
|
"AttentionState",
|
||||||
|
"get_attn_backend",
|
||||||
|
]
|
||||||
0
vllm/attention/backends/__init__.py
Normal file
0
vllm/attention/backends/__init__.py
Normal file
301
vllm/attention/backends/abstract.py
Normal file
301
vllm/attention/backends/abstract.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
from typing import (TYPE_CHECKING, Any, Dict, Generic, List, Optional,
|
||||||
|
Protocol, Set, Tuple, Type, TypeVar)
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.multimodal import MultiModalPlaceholderMap
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner_base import (ModelRunnerBase,
|
||||||
|
ModelRunnerInputBase,
|
||||||
|
ModelRunnerInputBuilderBase)
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionType:
|
||||||
|
"""
|
||||||
|
Attention type.
|
||||||
|
Use string to be compatible with `torch.compile`.
|
||||||
|
"""
|
||||||
|
# Decoder attention between previous layer Q/K/V
|
||||||
|
DECODER = "decoder"
|
||||||
|
# Encoder attention between previous layer Q/K/V for encoder-decoder
|
||||||
|
ENCODER = "encoder"
|
||||||
|
# Encoder attention between previous layer Q/K/V
|
||||||
|
ENCODER_ONLY = "encoder_only"
|
||||||
|
# Attention between dec. Q and enc. K/V for encoder-decoder
|
||||||
|
ENCODER_DECODER = "encoder_decoder"
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionBackend(ABC):
|
||||||
|
"""Abstract class for attention backends."""
|
||||||
|
# For some attention backends, we allocate an output tensor before
|
||||||
|
# calling the custom op. When piecewise cudagraph is enabled, this
|
||||||
|
# makes sure the output tensor is allocated inside the cudagraph.
|
||||||
|
accept_output_buffer: bool = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_impl_cls() -> Type["AttentionImpl"]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_state_cls() -> Type["AttentionState"]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_metadata(cls, *args, **kwargs) -> "AttentionMetadata":
|
||||||
|
return cls.get_metadata_cls()(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_builder_cls() -> Type["AttentionMetadataBuilder"]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def advance_step(self, model_input: "ModelRunnerInputBase",
|
||||||
|
sampled_token_ids: Optional[torch.Tensor],
|
||||||
|
block_size: int, num_seqs: int, num_queries: int) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AttentionMetadata:
|
||||||
|
"""Attention metadata for prefill and decode batched together."""
|
||||||
|
# Total number of prefill requests.
|
||||||
|
num_prefills: int
|
||||||
|
# Number of prefill tokens.
|
||||||
|
num_prefill_tokens: int
|
||||||
|
# Number of decode tokens. Note that it is equivalent to the number of
|
||||||
|
# decode requests.
|
||||||
|
num_decode_tokens: int
|
||||||
|
# (num_tokens,). The indices of the token slots that input tokens will be
|
||||||
|
# stored into. E.g., if `slot_mapping` is [35, 2, 17] and the block size
|
||||||
|
# is 16, the three tokens are stored in the 3rd slot in block 2, 2nd slot
|
||||||
|
# in block 0, and 1st slot in block 1, respectively.
|
||||||
|
slot_mapping: torch.Tensor
|
||||||
|
|
||||||
|
# The index maps that relate multi-modal embeddings to the corresponding
|
||||||
|
# placeholders.
|
||||||
|
#
|
||||||
|
# N.B. These aren't really related to attention and don't belong on this
|
||||||
|
# type -- this is just a temporary solution to make them available to
|
||||||
|
# `model_executable`.
|
||||||
|
multi_modal_placeholder_index_maps: Optional[Dict[
|
||||||
|
str, MultiModalPlaceholderMap.IndexMap]]
|
||||||
|
|
||||||
|
# Enable/disable KV scales calculation. This is so that we can disable the
|
||||||
|
# calculation until after prefill and cuda graph capture.
|
||||||
|
enable_kv_scales_calculation: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def prefill_metadata(self) -> Optional["AttentionMetadata"]:
|
||||||
|
"""Return the attention metadata that's required to run prefill
|
||||||
|
attention."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def decode_metadata(self) -> Optional["AttentionMetadata"]:
|
||||||
|
"""Return the attention metadata that's required to run decode
|
||||||
|
attention."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def asdict_zerocopy(self,
|
||||||
|
skip_fields: Optional[Set[str]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Similar to dataclasses.asdict, but avoids deepcopying."""
|
||||||
|
if skip_fields is None:
|
||||||
|
skip_fields = set()
|
||||||
|
# Note that if we add dataclasses as fields, they will need
|
||||||
|
# similar handling.
|
||||||
|
return {
|
||||||
|
field.name: getattr(self, field.name)
|
||||||
|
for field in fields(self) if field.name not in skip_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=AttentionMetadata)
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionState(ABC, Generic[T]):
|
||||||
|
"""Holds attention backend-specific objects reused during the
|
||||||
|
lifetime of the model runner."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, runner: "ModelRunnerBase"):
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
@contextmanager
|
||||||
|
def graph_capture(self, max_batch_size: int):
|
||||||
|
"""Context manager used when capturing CUDA graphs."""
|
||||||
|
yield
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def graph_clone(self, batch_size: int) -> "AttentionState[T]":
|
||||||
|
"""Clone attention state to save in CUDA graph metadata."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def graph_capture_get_metadata_for_batch(
|
||||||
|
self,
|
||||||
|
batch_size: int,
|
||||||
|
is_encoder_decoder_model: bool = False) -> T:
|
||||||
|
"""Get attention metadata for CUDA graph capture of batch_size."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_graph_input_buffers(
|
||||||
|
self,
|
||||||
|
attn_metadata: T,
|
||||||
|
is_encoder_decoder_model: bool = False) -> Dict[str, Any]:
|
||||||
|
"""Get attention-specific input buffers for CUDA graph capture."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def prepare_graph_input_buffers(
|
||||||
|
self,
|
||||||
|
input_buffers: Dict[str, Any],
|
||||||
|
attn_metadata: T,
|
||||||
|
is_encoder_decoder_model: bool = False) -> None:
|
||||||
|
"""In-place modify input buffers dict for CUDA graph replay."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def begin_forward(self, model_input: "ModelRunnerInputBase") -> None:
|
||||||
|
"""Prepare state for forward pass."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionMetadataBuilder(ABC, Generic[T]):
|
||||||
|
"""Abstract class for attention metadata builders."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, input_builder: "ModelRunnerInputBuilderBase") -> None:
|
||||||
|
"""Create the builder, remember some configuration and parameters."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def prepare(self) -> None:
|
||||||
|
"""Prepare for one batch."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int) -> T:
|
||||||
|
"""Build attention metadata with on-device tensors."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionLayer(Protocol):
|
||||||
|
|
||||||
|
_q_scale: torch.Tensor
|
||||||
|
_k_scale: torch.Tensor
|
||||||
|
_v_scale: torch.Tensor
|
||||||
|
_k_scale_float: float
|
||||||
|
_v_scale_float: float
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: AttentionMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionImpl(ABC, Generic[T]):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: Optional[int] = None,
|
||||||
|
alibi_slopes: Optional[List[float]] = None,
|
||||||
|
sliding_window: Optional[int] = None,
|
||||||
|
kv_cache_dtype: str = "auto",
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: T,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MLAAttentionImpl(AttentionImpl[T], Generic[T]):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
hidden_states_or_cq: torch.Tensor,
|
||||||
|
kv_c_normed: torch.Tensor,
|
||||||
|
k_pe: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: T,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def is_quantized_kv_cache(kv_cache_dtype: str) -> bool:
|
||||||
|
return kv_cache_dtype != "auto"
|
||||||
457
vllm/attention/backends/blocksparse_attn.py
Normal file
457
vllm/attention/backends/blocksparse_attn.py
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType)
|
||||||
|
from vllm.attention.backends.utils import (CommonAttentionState,
|
||||||
|
CommonMetadataBuilder)
|
||||||
|
from vllm.attention.ops.blocksparse_attention.interface import (
|
||||||
|
LocalStridedBlockSparseAttn, get_head_sliding_step)
|
||||||
|
from vllm.attention.ops.paged_attn import PagedAttention
|
||||||
|
from vllm.distributed import (get_tensor_model_parallel_rank,
|
||||||
|
get_tensor_model_parallel_world_size)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlocksparseParams:
|
||||||
|
max_seqlen: int
|
||||||
|
|
||||||
|
# Num q heads per tensor-parallel rank/partition
|
||||||
|
num_heads: int # per TP partition
|
||||||
|
# Num kv heads per tensor-parallel rank/partition
|
||||||
|
num_kv_heads: int
|
||||||
|
|
||||||
|
# block size used for blocksparse attention.
|
||||||
|
# This is the block_size used in `local_blocks`, `vert_stride`.
|
||||||
|
block_size: int
|
||||||
|
|
||||||
|
# Number of blocks for local attention, i.e., number of
|
||||||
|
# local attended tokens / `sparse_block_size`
|
||||||
|
local_blocks: int
|
||||||
|
|
||||||
|
# Attend to one block per every `vert_stride` blocks.
|
||||||
|
# Controlling the sparsity
|
||||||
|
vert_stride: int
|
||||||
|
"""
|
||||||
|
If to use the same vertical stride offset for all heads,
|
||||||
|
i.e., attend to the same block of tokens on all heads.
|
||||||
|
By default, it is False, i.e., attention on the non-local
|
||||||
|
blocks depends on the `head_idx`, that is on
|
||||||
|
blocks satisfying
|
||||||
|
`(block_idx + head_idx * head_sliding_step + 1) % vert_stride == 0`
|
||||||
|
where `head_sliding_step=max(1, int(vert_stride / num_total_heads))`,
|
||||||
|
`block_idx = position_id // sparse_block_size`.
|
||||||
|
See `..ops.blocksparse_attention.utils:get_sparse_attn_mask`
|
||||||
|
for more detail.
|
||||||
|
"""
|
||||||
|
homo_head: bool = False
|
||||||
|
|
||||||
|
# If within a group, the kv offsets that each q attends is the same or no.
|
||||||
|
homo_head_group: bool = False
|
||||||
|
|
||||||
|
# Decided by homo_head and homo_head group
|
||||||
|
head_sliding_step: int = field(init=False)
|
||||||
|
|
||||||
|
# range of q heads to for a TP rank
|
||||||
|
active_head_range: Tuple = field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.block_size > 0
|
||||||
|
assert self.local_blocks >= 0
|
||||||
|
assert self.vert_stride >= 1
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
|
||||||
|
tp_size = get_tensor_model_parallel_world_size()
|
||||||
|
tp_rank = get_tensor_model_parallel_rank()
|
||||||
|
total_heads = tp_size * self.num_heads
|
||||||
|
total_kv_heads = tp_size * self.num_kv_heads
|
||||||
|
|
||||||
|
if self.homo_head:
|
||||||
|
self.head_sliding_step = 0
|
||||||
|
elif self.homo_head_group:
|
||||||
|
head_sliding_step = get_head_sliding_step(total_kv_heads,
|
||||||
|
self.vert_stride)
|
||||||
|
# negative indicates sliding along kv heads, i.e., homo q group
|
||||||
|
self.head_sliding_step = -head_sliding_step
|
||||||
|
else:
|
||||||
|
self.head_sliding_step = get_head_sliding_step(
|
||||||
|
total_heads, self.vert_stride)
|
||||||
|
|
||||||
|
self.active_head_range = (
|
||||||
|
tp_rank * self.num_heads,
|
||||||
|
(tp_rank + 1) * self.num_heads,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BlocksparseFlashAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "BLOCK_SPARSE_FLASH_ATTN"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["BlocksparseFlashAttentionImpl"]:
|
||||||
|
return BlocksparseFlashAttentionImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return BlocksparseFlashAttentionMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["BlocksparseFlashAttentionMetadataBuilder"]:
|
||||||
|
return BlocksparseFlashAttentionMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return PagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: Dict[int, int],
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: Dict[int, List[int]],
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.copy_blocks(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BlocksparseFlashAttentionMetadata(AttentionMetadata):
|
||||||
|
"""A copy of Metadata for FlashAttentionBackend,
|
||||||
|
to avoid having to install flash_attn.
|
||||||
|
|
||||||
|
NOTE: Any python object stored here is not updated when it is
|
||||||
|
cuda-graph replayed. If you have values that need to be changed
|
||||||
|
dynamically, it should be stored in tensor. The tensor has to be
|
||||||
|
updated from `CUDAGraphRunner.forward` API.
|
||||||
|
"""
|
||||||
|
# (batch_size,). The sequence length per sequence. Sequence length means
|
||||||
|
# the computed tokens + new tokens None if it is a decoding.
|
||||||
|
seq_lens: Optional[List[int]]
|
||||||
|
# seq_lens stored as a tensor.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# NOTE(sang): Definition of context_len, query_len, and seq_len.
|
||||||
|
# |---------- N-1 iteration --------|
|
||||||
|
# |---------------- N iteration ---------------------|
|
||||||
|
# |- tokenA -|......................|-- newTokens ---|
|
||||||
|
# |---------- context_len ----------|
|
||||||
|
# |-------------------- seq_len ----------------------|
|
||||||
|
# |-- query_len ---|
|
||||||
|
|
||||||
|
# Maximum query length in the batch. None for decoding.
|
||||||
|
max_query_len: Optional[int]
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
# Maximum sequence length among decode batch. 0 if there are prefill
|
||||||
|
# requests only.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
# (batch_size + 1,). The cumulative subquery lengths of the sequences in
|
||||||
|
# the batch, used to index into subquery. E.g., if the subquery length
|
||||||
|
# is [4, 6], it is [0, 4, 10].
|
||||||
|
query_start_loc: Optional[torch.Tensor]
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
seq_start_loc: Optional[torch.Tensor]
|
||||||
|
# (batch_size,) A tensor of context lengths (tokens that are computed
|
||||||
|
# so far).
|
||||||
|
context_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# (batch_size, max_blocks_per_seq).
|
||||||
|
# Block addresses per sequence. (Seq id -> list of physical block)
|
||||||
|
# E.g., [0, 1, 2] means tokens are stored in 0th, 1st, and 2nd blocks
|
||||||
|
# in the kv cache. Each block can contain up to block_size tokens.
|
||||||
|
# 2nd dimensions are padded up to max_blocks_per_seq if it is cuda-graph
|
||||||
|
# captured.
|
||||||
|
block_tables: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# Whether or not if cuda graph is enabled.
|
||||||
|
# Cuda-graph is currently enabled for decoding only.
|
||||||
|
# TODO(woosuk): Move `use_cuda_graph` out since it's unrelated to attention.
|
||||||
|
use_cuda_graph: bool
|
||||||
|
|
||||||
|
# Max number of query tokens for among request in the batch.
|
||||||
|
max_decode_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
_cached_prefill_metadata: Optional[
|
||||||
|
"BlocksparseFlashAttentionMetadata"] = None
|
||||||
|
_cached_decode_metadata: Optional[
|
||||||
|
"BlocksparseFlashAttentionMetadata"] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(
|
||||||
|
self) -> Optional["BlocksparseFlashAttentionMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_prefill_metadata is not None:
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
assert self.query_start_loc is not None
|
||||||
|
assert self.context_lens_tensor is not None
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.seq_start_loc is not None
|
||||||
|
|
||||||
|
self._cached_prefill_metadata = BlocksparseFlashAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=0,
|
||||||
|
slot_mapping=self.slot_mapping[:self.num_prefill_tokens],
|
||||||
|
multi_modal_placeholder_index_maps=self.
|
||||||
|
multi_modal_placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=self.enable_kv_scales_calculation,
|
||||||
|
seq_lens=self.seq_lens[:self.num_prefills],
|
||||||
|
seq_lens_tensor=self.seq_lens_tensor[:self.num_prefills],
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=self.max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=0,
|
||||||
|
query_start_loc=self.query_start_loc[:self.num_prefills + 1],
|
||||||
|
seq_start_loc=self.seq_start_loc[:self.num_prefills + 1],
|
||||||
|
context_lens_tensor=self.context_lens_tensor[:self.num_prefills],
|
||||||
|
block_tables=self.block_tables[:self.num_prefills],
|
||||||
|
use_cuda_graph=False,
|
||||||
|
)
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["BlocksparseFlashAttentionMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_decode_metadata is not None:
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
|
||||||
|
self._cached_decode_metadata = BlocksparseFlashAttentionMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=self.slot_mapping[self.num_prefill_tokens:],
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=False,
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=self.seq_lens_tensor[self.num_prefills:],
|
||||||
|
max_query_len=None,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
query_start_loc=None,
|
||||||
|
seq_start_loc=None,
|
||||||
|
context_lens_tensor=None,
|
||||||
|
block_tables=self.block_tables[self.num_prefills:],
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
)
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
|
||||||
|
|
||||||
|
class BlocksparseFlashAttentionMetadataBuilder(
|
||||||
|
CommonMetadataBuilder[BlocksparseFlashAttentionMetadata]):
|
||||||
|
|
||||||
|
_metadata_cls = BlocksparseFlashAttentionMetadata
|
||||||
|
|
||||||
|
|
||||||
|
class BlocksparseFlashAttentionImpl(AttentionImpl):
|
||||||
|
"""
|
||||||
|
If the input tensors contain prompt tokens, the layout is as follows:
|
||||||
|
|<--------------- num_prompt_tokens -------------->|
|
||||||
|
|<--prompt_0-->|<--prompt_1-->|...|<--prompt_N-1-->|
|
||||||
|
|
||||||
|
Otherwise, the layout is as follows:
|
||||||
|
|<------------------ num_generation_tokens (M) ----------------->|
|
||||||
|
|<--generation_0-->|..........|<--generation_M-1-->|<--padding-->|
|
||||||
|
|
||||||
|
Generation tokens can contain padding when cuda-graph is used.
|
||||||
|
Currently, prompt tokens don't contain any padding.
|
||||||
|
|
||||||
|
The prompts might have different lengths, while the generation tokens
|
||||||
|
always have length 1.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
assert blocksparse_params is not None
|
||||||
|
assert alibi_slopes is None, ValueError(
|
||||||
|
"Alibi not support for blocksparse flash attention.")
|
||||||
|
assert sliding_window is None, ValueError(
|
||||||
|
"sliding_window is invalid for blocksparse attention.")
|
||||||
|
assert logits_soft_cap is None, ValueError(
|
||||||
|
"logits_soft_cap is invalid for blocksparse attention.")
|
||||||
|
|
||||||
|
if "num_heads" not in blocksparse_params:
|
||||||
|
blocksparse_params["num_heads"] = num_heads
|
||||||
|
if "num_kv_heads" not in blocksparse_params:
|
||||||
|
blocksparse_params["num_kv_heads"] = num_kv_heads or num_heads
|
||||||
|
self.blocksparse_params = BlocksparseParams(**blocksparse_params)
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
self.local_blocks = self.blocksparse_params.local_blocks
|
||||||
|
self.vert_stride = self.blocksparse_params.vert_stride
|
||||||
|
self.sparse_block_size = self.blocksparse_params.block_size
|
||||||
|
self.head_sliding_step = self.blocksparse_params.head_sliding_step
|
||||||
|
|
||||||
|
supported_head_sizes = PagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in supported_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {supported_head_sizes}.")
|
||||||
|
|
||||||
|
self.tp_size = get_tensor_model_parallel_world_size()
|
||||||
|
self.tp_rank = get_tensor_model_parallel_rank()
|
||||||
|
|
||||||
|
total_num_heads = num_heads * self.tp_size
|
||||||
|
self.bs_attn = LocalStridedBlockSparseAttn(
|
||||||
|
total_num_heads,
|
||||||
|
self.blocksparse_params.max_seqlen,
|
||||||
|
self.blocksparse_params.local_blocks,
|
||||||
|
self.blocksparse_params.vert_stride,
|
||||||
|
self.blocksparse_params.block_size,
|
||||||
|
homo_head=self.blocksparse_params.homo_head,
|
||||||
|
active_head_range=self.blocksparse_params.active_head_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"BlocksparseFlashAttentionImpl")
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: BlocksparseFlashAttentionMetadata,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with FlashAttention and PagedAttention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
num_tokens, hidden_size = query.shape
|
||||||
|
# Reshape the query, key, and value tensors.
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
if kv_cache.numel() > 0:
|
||||||
|
key_cache, value_cache = PagedAttention.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
# Reshape the input keys and values and store them in the cache.
|
||||||
|
# If kv_cache is not provided, the new key and value tensors are
|
||||||
|
# not cached. This happens during the initial memory profiling run.
|
||||||
|
|
||||||
|
PagedAttention.write_to_paged_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.slot_mapping,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
|
||||||
|
# Prompt run.
|
||||||
|
# normal attention
|
||||||
|
# When block_tables are not filled, it means q and k are the
|
||||||
|
# prompt, and they have the same length.
|
||||||
|
|
||||||
|
assert kv_cache.numel() == 0 \
|
||||||
|
or prefill_meta.block_tables is None \
|
||||||
|
or prefill_meta.block_tables.numel() == 0, \
|
||||||
|
"Does not support prefix-enabled attention."
|
||||||
|
|
||||||
|
output = self.bs_attn(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
cu_seqlens_q=prefill_meta.seq_start_loc,
|
||||||
|
cu_seqlens_k=prefill_meta.seq_start_loc,
|
||||||
|
sm_scale=self.scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if decode_meta := attn_metadata.decode_metadata:
|
||||||
|
# Decoding run.
|
||||||
|
output = PagedAttention.forward_decode(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
decode_meta.block_tables,
|
||||||
|
decode_meta.seq_lens_tensor,
|
||||||
|
self.blocksparse_params.max_seqlen,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
tp_rank=self.tp_rank,
|
||||||
|
blocksparse_local_blocks=self.local_blocks,
|
||||||
|
blocksparse_vert_stride=self.vert_stride,
|
||||||
|
blocksparse_block_size=self.sparse_block_size,
|
||||||
|
blocksparse_head_sliding_step=self.head_sliding_step,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output is not None
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(num_tokens, hidden_size)
|
||||||
303
vllm/attention/backends/cpu_mla.py
Normal file
303
vllm/attention/backends/cpu_mla.py
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import vllm._custom_ops as ops
|
||||||
|
from vllm._ipex_ops import ipex_ops
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.mla.common import MLACommonImpl, MLACommonState
|
||||||
|
from vllm.attention.backends.torch_sdpa import TorchSDPAMetadata
|
||||||
|
from vllm.utils import make_tensor_with_pad
|
||||||
|
from vllm.worker.cpu_model_runner import ModelInputForCPUBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class CPUMLABackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "CPU_MLA"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["CPUMLAMetadata"]:
|
||||||
|
return CPUMLAMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["CPUMLAMetadataBuilder"]:
|
||||||
|
return CPUMLAMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["MLACommonState"]:
|
||||||
|
return MLACommonState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["CPUMLAImpl"]:
|
||||||
|
return CPUMLAImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int, # assumed to be 1 for MLA
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (num_blocks, block_size, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
ops.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
ops.copy_blocks_mla(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [576]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CPUMLAMetadata(TorchSDPAMetadata):
|
||||||
|
# New for MLA
|
||||||
|
# Input positions for rotrary embeddings since for MLA the rotary
|
||||||
|
# position embeddings are applied inside the attention backend
|
||||||
|
input_positions: torch.Tensor = None
|
||||||
|
|
||||||
|
# required by MLACommonImpl
|
||||||
|
is_profile_run: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class CPUMLAMetadataBuilder(AttentionMetadataBuilder[CPUMLAMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, input_builder: ModelInputForCPUBuilder) -> None:
|
||||||
|
self.chunked_prefill = input_builder.chunked_prefill
|
||||||
|
self.input_builder = input_builder
|
||||||
|
assert not self.chunked_prefill, \
|
||||||
|
"chunked prefill is currently not supported"
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.input_data = self.input_builder.input_data
|
||||||
|
|
||||||
|
def build(self, seq_lens, query_lens, cuda_graph_pad_size, batch_size):
|
||||||
|
input_data = self.input_data
|
||||||
|
prefill_seq_lens = seq_lens[0:input_data.num_prefills]
|
||||||
|
prefill_query_lens = query_lens[0:input_data.num_prefills]
|
||||||
|
slot_mapping = torch.tensor(input_data.slot_mapping,
|
||||||
|
dtype=torch.long,
|
||||||
|
device="cpu")
|
||||||
|
|
||||||
|
# metadata for prefill
|
||||||
|
if input_data.num_prefills > 0:
|
||||||
|
query_lens_tensor = torch.tensor(prefill_query_lens,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
kv_lens_tensor = torch.tensor(prefill_seq_lens,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
query_start_loc = torch.zeros(input_data.num_prefills + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
kv_start_loc = torch.zeros(input_data.num_prefills + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
torch.cumsum(query_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
out=query_start_loc[1:])
|
||||||
|
torch.cumsum(kv_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
out=kv_start_loc[1:])
|
||||||
|
max_query_len = max(prefill_query_lens)
|
||||||
|
max_kv_len = max(prefill_seq_lens)
|
||||||
|
|
||||||
|
# for chunked-prefill
|
||||||
|
if self.chunked_prefill:
|
||||||
|
prefill_block_tables = make_tensor_with_pad(
|
||||||
|
self.input_data.prefill_block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prefill_block_tables = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
query_start_loc = None
|
||||||
|
kv_start_loc = None
|
||||||
|
max_query_len = None
|
||||||
|
max_kv_len = None
|
||||||
|
prefill_block_tables = None
|
||||||
|
|
||||||
|
# metadata for decode
|
||||||
|
if input_data.num_decode_tokens != 0:
|
||||||
|
seq_lens_tensor = torch.tensor(
|
||||||
|
input_data.seq_lens[input_data.num_prefills:],
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
block_tables = make_tensor_with_pad(
|
||||||
|
self.input_data.decode_block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
block_tables = torch.tensor([])
|
||||||
|
seq_lens_tensor = torch.tensor(
|
||||||
|
input_data.seq_lens[:input_data.num_prefills],
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
|
||||||
|
# For multi-modal models
|
||||||
|
placeholder_index_maps = None
|
||||||
|
if len(input_data.multi_modal_inputs_list) != 0:
|
||||||
|
placeholder_index_maps = {
|
||||||
|
modality: placeholder_map.index_map()
|
||||||
|
for modality, placeholder_map in
|
||||||
|
input_data.multi_modal_placeholder_maps.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return CPUMLAMetadata(
|
||||||
|
chunked_prefill=self.chunked_prefill,
|
||||||
|
seq_lens=prefill_seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=max_query_len,
|
||||||
|
max_kv_len=max_kv_len,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
kv_start_loc=kv_start_loc,
|
||||||
|
max_decode_seq_len=input_data.max_decode_seq_len,
|
||||||
|
num_prefills=input_data.num_prefills,
|
||||||
|
num_prefill_tokens=input_data.num_prefill_tokens,
|
||||||
|
num_decode_tokens=input_data.num_decode_tokens,
|
||||||
|
block_tables=block_tables,
|
||||||
|
prefill_block_tables=prefill_block_tables,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=False,
|
||||||
|
input_positions=torch.tensor([self.input_data.input_positions]))
|
||||||
|
|
||||||
|
|
||||||
|
class CPUMLAImpl(MLACommonImpl[CPUMLAMetadata]):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]],
|
||||||
|
logits_soft_cap: Optional[float],
|
||||||
|
attn_type: str,
|
||||||
|
# MLA Specific Arguments
|
||||||
|
**mla_args) -> None:
|
||||||
|
super().__init__(num_heads, head_size, scale, num_kv_heads,
|
||||||
|
alibi_slopes, sliding_window, kv_cache_dtype,
|
||||||
|
blocksparse_params, logits_soft_cap, attn_type,
|
||||||
|
**mla_args)
|
||||||
|
|
||||||
|
unsupported_features = [
|
||||||
|
alibi_slopes, sliding_window, blocksparse_params, logits_soft_cap
|
||||||
|
]
|
||||||
|
if any(unsupported_features):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"CPUMLAImpl does not support one of the following: "
|
||||||
|
"alibi_slopes, sliding_window, blocksparse_params, "
|
||||||
|
"logits_soft_cap")
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"CPUMLAImpl")
|
||||||
|
|
||||||
|
# states is implemented.
|
||||||
|
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"CPUMLAImpl with FP8 KV cache not yet supported")
|
||||||
|
|
||||||
|
def _forward_prefill(
|
||||||
|
self,
|
||||||
|
q: torch.Tensor,
|
||||||
|
kv_c_normed: torch.Tensor,
|
||||||
|
k_pe: torch.Tensor,
|
||||||
|
kv_c_and_k_pe_cache: torch.Tensor,
|
||||||
|
attn_metadata: CPUMLAMetadata, # type: ignore[override]
|
||||||
|
) -> torch.Tensor:
|
||||||
|
|
||||||
|
prefill_metadata = attn_metadata.prefill_metadata
|
||||||
|
assert prefill_metadata is not None
|
||||||
|
|
||||||
|
kv_nope = self.kv_b_proj(kv_c_normed)[0].view(\
|
||||||
|
-1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim)
|
||||||
|
k_nope, v = kv_nope\
|
||||||
|
.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
|
||||||
|
|
||||||
|
k = torch.cat((k_nope, k_pe.expand((*k_nope.shape[:-1], -1))), dim=-1)
|
||||||
|
|
||||||
|
# For MLA the v head dim is smaller than qk head dim so we pad out
|
||||||
|
# v with 0s to match the qk head dim
|
||||||
|
v_padded = torch.nn.functional.pad(v, [0, q.shape[-1] - v.shape[-1]],
|
||||||
|
value=0)
|
||||||
|
|
||||||
|
output = torch.empty_like(q)
|
||||||
|
ipex_ops.varlen_attention(
|
||||||
|
query=q,
|
||||||
|
key=k,
|
||||||
|
value=v_padded,
|
||||||
|
out=output,
|
||||||
|
seqlen_q=prefill_metadata.query_start_loc,
|
||||||
|
seqlen_k=prefill_metadata.query_start_loc,
|
||||||
|
max_seqlen_q=prefill_metadata.max_query_len,
|
||||||
|
max_seqlen_k=prefill_metadata.max_query_len,
|
||||||
|
pdropout=0.0,
|
||||||
|
softmax_scale=self.scale,
|
||||||
|
zero_tensors=False,
|
||||||
|
is_causal=True,
|
||||||
|
return_softmax=False,
|
||||||
|
gen_=None,
|
||||||
|
logits_soft_cap=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove padding
|
||||||
|
output = output.view(-1, self.num_heads,
|
||||||
|
q.shape[-1])[..., :v.shape[-1]]
|
||||||
|
output = output.reshape(-1, self.num_heads * v.shape[-1])
|
||||||
|
return self.o_proj(output)[0]
|
||||||
|
|
||||||
|
def _forward_decode(
|
||||||
|
self,
|
||||||
|
q_nope: torch.Tensor,
|
||||||
|
q_pe: torch.Tensor,
|
||||||
|
kv_c_and_k_pe_cache: torch.Tensor,
|
||||||
|
attn_metadata: CPUMLAMetadata, # type: ignore[override]
|
||||||
|
) -> torch.Tensor:
|
||||||
|
assert kv_c_and_k_pe_cache.numel() > 0
|
||||||
|
|
||||||
|
decode_meta = attn_metadata.decode_metadata
|
||||||
|
assert decode_meta is not None
|
||||||
|
|
||||||
|
q = torch.cat([q_nope, q_pe], dim=-1)
|
||||||
|
o = q.new_empty(q.shape[0], self.num_heads, self.kv_lora_rank)
|
||||||
|
|
||||||
|
# Run MQA
|
||||||
|
ops.mla_decode_kvcache_cpu(o, q, kv_c_and_k_pe_cache, self.scale,
|
||||||
|
decode_meta.block_tables,
|
||||||
|
decode_meta.seq_lens_tensor)
|
||||||
|
return self._v_up_proj_and_o_proj(o)
|
||||||
995
vllm/attention/backends/flash_attn.py
Normal file
995
vllm/attention/backends/flash_attn.py
Normal file
@@ -0,0 +1,995 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Attention layer with FlashAttention."""
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from itertools import accumulate
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
# yapf conflicts with isort for this block
|
||||||
|
# yapf: disable
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
# yapf: enable
|
||||||
|
from vllm.attention.backends.utils import (
|
||||||
|
PAD_SLOT_ID, CommonAttentionState, compute_slot_mapping,
|
||||||
|
compute_slot_mapping_start_idx, get_num_prefill_decode_query_kv_tokens,
|
||||||
|
get_seq_len_block_table_args, is_all_cross_attn_metadata_set,
|
||||||
|
is_all_encoder_attn_metadata_set, is_block_tables_empty)
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.multimodal import MultiModalPlaceholderMap
|
||||||
|
from vllm.utils import async_tensor_h2d, make_tensor_with_pad
|
||||||
|
# from vllm.vllm_flash_attn import (flash_attn_varlen_func,
|
||||||
|
# flash_attn_with_kvcache)
|
||||||
|
from ixformer.contrib.vllm_flash_attn import (flash_attn_varlen_func,
|
||||||
|
flash_attn_with_kvcache)
|
||||||
|
from vllm.vllm_flash_attn.fa_utils import (flash_attn_supports_fp8,
|
||||||
|
get_flash_attn_version)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import (ModelInputForGPUBuilder,
|
||||||
|
ModelInputForGPUWithSamplingMetadata)
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class FlashAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
accept_output_buffer: bool = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [32, 64, 72, 80, 96, 128, 160, 192, 224, 256]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "FLASH_ATTN"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["FlashAttentionImpl"]:
|
||||||
|
return FlashAttentionImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return FlashAttentionMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["FlashAttentionMetadataBuilder"]:
|
||||||
|
return FlashAttentionMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
if block_size % 16 != 0:
|
||||||
|
raise ValueError("Block size must be a multiple of 16.")
|
||||||
|
return (2, num_blocks, num_kv_heads, block_size, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
src_key_cache = src_kv_cache[0]
|
||||||
|
dst_key_cache = dst_kv_cache[0]
|
||||||
|
ops.swap_blocks(src_key_cache, dst_key_cache, src_to_dst)
|
||||||
|
src_value_cache = src_kv_cache[1]
|
||||||
|
dst_value_cache = dst_kv_cache[1]
|
||||||
|
ops.swap_blocks(src_value_cache, dst_value_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||||||
|
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||||||
|
|
||||||
|
ops.copy_blocks(key_caches, value_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FlashAttentionMetadata(AttentionMetadata):
|
||||||
|
"""Metadata for FlashAttentionBackend.
|
||||||
|
|
||||||
|
NOTE: Any python object stored here is not updated when it is
|
||||||
|
cuda-graph replayed. If you have values that need to be changed
|
||||||
|
dynamically, it should be stored in tensor. The tensor has to be
|
||||||
|
updated from `CUDAGraphRunner.forward` API.
|
||||||
|
"""
|
||||||
|
# (batch_size,). The sequence length per sequence. Sequence length means
|
||||||
|
# the computed tokens + new tokens None if it is a decoding.
|
||||||
|
seq_lens: Optional[List[int]]
|
||||||
|
# seq_lens stored as a tensor.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# NOTE(sang): Definition of context_len, query_len, and seq_len.
|
||||||
|
# |---------- N-1 iteration --------|
|
||||||
|
# |---------------- N iteration ---------------------|
|
||||||
|
# |- tokenA -|......................|-- newTokens ---|
|
||||||
|
# |---------- context_len ----------|
|
||||||
|
# |-------------------- seq_len ---------------------|
|
||||||
|
# |-- query_len ---|
|
||||||
|
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
# Maximum sequence length among decode batch. 0 if there are prefill
|
||||||
|
# requests only.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
# (batch_size,) A tensor of context lengths (tokens that are computed
|
||||||
|
# so far).
|
||||||
|
context_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# (batch_size, max_blocks_per_seq).
|
||||||
|
# Block addresses per sequence. (Seq id -> list of physical block)
|
||||||
|
# E.g., [0, 1, 2] means tokens are stored in 0th, 1st, and 2nd blocks
|
||||||
|
# in the kv cache. Each block can contain up to block_size tokens.
|
||||||
|
# 2nd dimensions are padded up to max_blocks_per_seq if it is cuda-graph
|
||||||
|
# captured.
|
||||||
|
block_tables: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# Whether or not if cuda graph is enabled.
|
||||||
|
# Cuda-graph is currently enabled for decoding only.
|
||||||
|
# TODO(woosuk): Move `use_cuda_graph` out since it's unrelated to attention.
|
||||||
|
|
||||||
|
use_cuda_graph: bool
|
||||||
|
|
||||||
|
# Maximum query length in the batch.
|
||||||
|
max_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
# Max number of query tokens among request in the batch.
|
||||||
|
max_decode_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
# (batch_size + 1,). The cumulative subquery lengths of the sequences in
|
||||||
|
# the batch, used to index into subquery. E.g., if the subquery length
|
||||||
|
# is [4, 6], it is [0, 4, 10].
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
_cached_prefill_metadata: Optional["FlashAttentionMetadata"] = None
|
||||||
|
_cached_decode_metadata: Optional["FlashAttentionMetadata"] = None
|
||||||
|
|
||||||
|
# Begin encoder attn & enc/dec cross-attn fields...
|
||||||
|
|
||||||
|
# Encoder sequence lengths representation
|
||||||
|
encoder_seq_lens: Optional[List[int]] = None
|
||||||
|
encoder_seq_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
encoder_seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# Maximum sequence length among encoder sequences
|
||||||
|
max_encoder_seq_len: Optional[int] = None
|
||||||
|
# Number of tokens input to encoder
|
||||||
|
num_encoder_tokens: Optional[int] = None
|
||||||
|
|
||||||
|
# Cross-attention memory-mapping data structures: slot mapping
|
||||||
|
# and block tables
|
||||||
|
cross_slot_mapping: Optional[torch.Tensor] = None
|
||||||
|
cross_block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_encoder_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for encoder attention is set.
|
||||||
|
'''
|
||||||
|
return is_all_encoder_attn_metadata_set(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_cross_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for enc/dec cross-attention is set.
|
||||||
|
|
||||||
|
Superset of encoder attention required metadata.
|
||||||
|
'''
|
||||||
|
return is_all_cross_attn_metadata_set(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["FlashAttentionMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_prefill_metadata is not None:
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
assert ((self.seq_lens is not None)
|
||||||
|
or (self.encoder_seq_lens is not None))
|
||||||
|
assert ((self.seq_lens_tensor is not None)
|
||||||
|
or (self.encoder_seq_lens_tensor is not None))
|
||||||
|
|
||||||
|
# Compute some attn_metadata fields which default to None
|
||||||
|
query_start_loc = (None if self.query_start_loc is None else
|
||||||
|
self.query_start_loc[:self.num_prefills + 1])
|
||||||
|
slot_mapping = (None if self.slot_mapping is None else
|
||||||
|
self.slot_mapping[:self.num_prefill_tokens])
|
||||||
|
seq_lens = (None if self.seq_lens is None else
|
||||||
|
self.seq_lens[:self.num_prefills])
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[:self.num_prefills])
|
||||||
|
seq_start_loc = (None if self.seq_start_loc is None else
|
||||||
|
self.seq_start_loc[:self.num_prefills + 1])
|
||||||
|
context_lens_tensor = (None if self.context_lens_tensor is None else
|
||||||
|
self.context_lens_tensor[:self.num_prefills])
|
||||||
|
block_tables = (None if self.block_tables is None else
|
||||||
|
self.block_tables[:self.num_prefills])
|
||||||
|
|
||||||
|
self._cached_prefill_metadata = FlashAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=0,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=self.
|
||||||
|
multi_modal_placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=self.enable_kv_scales_calculation,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=self.max_prefill_seq_len,
|
||||||
|
max_decode_query_len=0,
|
||||||
|
max_decode_seq_len=0,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
seq_start_loc=seq_start_loc,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=False,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
encoder_seq_start_loc=self.encoder_seq_start_loc,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["FlashAttentionMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_decode_metadata is not None:
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
assert ((self.seq_lens_tensor is not None)
|
||||||
|
or (self.encoder_seq_lens_tensor is not None))
|
||||||
|
|
||||||
|
# Compute some attn_metadata fields which default to None
|
||||||
|
slot_mapping = (None if self.slot_mapping is None else
|
||||||
|
self.slot_mapping[self.num_prefill_tokens:])
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[self.num_prefills:])
|
||||||
|
block_tables = (None if self.block_tables is None else
|
||||||
|
self.block_tables[self.num_prefills:])
|
||||||
|
# if self.use_cuda_graph:
|
||||||
|
# self.max_decode_seq_len = self.block_tables.shape[-1] * 16
|
||||||
|
|
||||||
|
self._cached_decode_metadata = FlashAttentionMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_decode_query_len=self.max_decode_query_len,
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
# Batch may be composed of prefill|decodes, adjust query start
|
||||||
|
# indices to refer to the start of decodes. E.g.
|
||||||
|
# in tokens:[3 prefills|6 decodes], query_start_loc=[3,9] => [0,6].
|
||||||
|
query_start_loc=(self.query_start_loc[self.num_prefills:] -
|
||||||
|
self.query_start_loc[self.num_prefills])
|
||||||
|
if self.query_start_loc is not None else None,
|
||||||
|
seq_start_loc=self.seq_start_loc[self.num_prefills:]
|
||||||
|
if self.seq_start_loc is not None else None,
|
||||||
|
context_lens_tensor=None,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
encoder_seq_start_loc=self.encoder_seq_start_loc,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
|
||||||
|
def advance_step(self,
|
||||||
|
model_input: "ModelInputForGPUWithSamplingMetadata",
|
||||||
|
sampled_token_ids: Optional[torch.Tensor],
|
||||||
|
block_size: int,
|
||||||
|
num_seqs: int,
|
||||||
|
num_queries: int,
|
||||||
|
turn_prefills_into_decodes: bool = False):
|
||||||
|
"""
|
||||||
|
Update metadata in-place to advance one decode step.
|
||||||
|
"""
|
||||||
|
# When using cudagraph, the num_seqs is padded to the next captured
|
||||||
|
# batch sized, but num_queries tracks the actual number of requests in
|
||||||
|
# the batch. For --enforce-eager mode, num_seqs == num_queries
|
||||||
|
if num_seqs != num_queries:
|
||||||
|
assert num_seqs > num_queries
|
||||||
|
assert self.use_cuda_graph
|
||||||
|
|
||||||
|
if turn_prefills_into_decodes:
|
||||||
|
# When Mutli-Step is enabled with Chunked-Prefill, prefills and
|
||||||
|
# decodes are scheduled together. In the first step, all the
|
||||||
|
# prefills turn into decodes. This update reflects that
|
||||||
|
# conversion.
|
||||||
|
assert self.num_decode_tokens + self.num_prefills == num_seqs
|
||||||
|
self.num_decode_tokens += self.num_prefills
|
||||||
|
self.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.max_prefill_seq_len = 0
|
||||||
|
self.max_query_len = 1
|
||||||
|
|
||||||
|
self.slot_mapping = self.slot_mapping[:num_seqs]
|
||||||
|
else:
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert self.max_decode_seq_len == max(self.seq_lens)
|
||||||
|
|
||||||
|
assert self.num_prefills == 0
|
||||||
|
assert self.num_prefill_tokens == 0
|
||||||
|
assert self.num_decode_tokens == num_seqs
|
||||||
|
assert self.slot_mapping.shape == (num_seqs, )
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert len(self.seq_lens) == num_seqs
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
assert self.seq_lens_tensor.shape == (num_seqs, )
|
||||||
|
assert self.max_query_len == 1
|
||||||
|
assert self.max_prefill_seq_len == 0
|
||||||
|
|
||||||
|
assert self.query_start_loc is not None
|
||||||
|
assert self.query_start_loc.shape == (num_queries + 1, )
|
||||||
|
assert self.seq_start_loc is not None
|
||||||
|
assert self.seq_start_loc.shape == (num_seqs + 1, )
|
||||||
|
|
||||||
|
assert self.context_lens_tensor is not None
|
||||||
|
assert self.context_lens_tensor.shape == (num_queries, )
|
||||||
|
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.block_tables.shape[0] == num_seqs
|
||||||
|
|
||||||
|
# Update query lengths. Note that we update only queries and not seqs,
|
||||||
|
# since tensors may be padded due to captured cuda graph batch size
|
||||||
|
for i in range(num_queries):
|
||||||
|
self.seq_lens[i] += 1
|
||||||
|
self.max_decode_seq_len = max(self.seq_lens)
|
||||||
|
|
||||||
|
ops.advance_step_flashattn(num_seqs=num_seqs,
|
||||||
|
num_queries=num_queries,
|
||||||
|
block_size=block_size,
|
||||||
|
input_tokens=model_input.input_tokens,
|
||||||
|
sampled_token_ids=sampled_token_ids,
|
||||||
|
input_positions=model_input.input_positions,
|
||||||
|
seq_lens=self.seq_lens_tensor,
|
||||||
|
slot_mapping=self.slot_mapping,
|
||||||
|
block_tables=self.block_tables)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashAttentionMetadataBuilder(
|
||||||
|
AttentionMetadataBuilder[FlashAttentionMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, input_builder: "ModelInputForGPUBuilder"):
|
||||||
|
self.input_builder = input_builder
|
||||||
|
self.runner = input_builder.runner
|
||||||
|
self.sliding_window = input_builder.sliding_window
|
||||||
|
self.block_size = input_builder.block_size
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.slot_mapping: List[int] = []
|
||||||
|
self.prefill_seq_lens: List[int] = []
|
||||||
|
self.context_lens: List[int] = []
|
||||||
|
self.block_tables: List[List[int]] = []
|
||||||
|
self.curr_seq_lens: List[int] = []
|
||||||
|
self.multimodal_placeholder_maps: Dict[
|
||||||
|
str,
|
||||||
|
MultiModalPlaceholderMap] = defaultdict(MultiModalPlaceholderMap)
|
||||||
|
self.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.num_decode_tokens = 0
|
||||||
|
self.has_prefix_cache_hit = False
|
||||||
|
|
||||||
|
def _add_seq_group(
|
||||||
|
self, inter_data: "ModelInputForGPUBuilder.InterDataForSeqGroup",
|
||||||
|
chunked_prefill_enabled: bool, prefix_cache_hit: bool):
|
||||||
|
"""Add a sequence group to the metadata. Specifically update/append
|
||||||
|
1. context length.
|
||||||
|
2. block table.
|
||||||
|
3. slot mapping.
|
||||||
|
"""
|
||||||
|
is_prompt = inter_data.is_prompt
|
||||||
|
block_tables = inter_data.block_tables
|
||||||
|
|
||||||
|
for (seq_id, token_len, seq_len, curr_seq_len, query_len, context_len,
|
||||||
|
curr_sliding_window_block) in zip(
|
||||||
|
inter_data.seq_ids, [len(t) for t in inter_data.input_tokens],
|
||||||
|
inter_data.orig_seq_lens, inter_data.seq_lens,
|
||||||
|
inter_data.query_lens, inter_data.context_lens,
|
||||||
|
inter_data.curr_sliding_window_blocks):
|
||||||
|
self.context_lens.append(context_len)
|
||||||
|
|
||||||
|
if is_prompt:
|
||||||
|
mm_maps = inter_data.multi_modal_placeholder_maps
|
||||||
|
if mm_maps:
|
||||||
|
for modality, placeholders in mm_maps.items():
|
||||||
|
self.multimodal_placeholder_maps[modality].extend(
|
||||||
|
placeholders)
|
||||||
|
|
||||||
|
self.num_prefills += 1
|
||||||
|
self.num_prefill_tokens += token_len
|
||||||
|
self.prefill_seq_lens.append(seq_len)
|
||||||
|
else:
|
||||||
|
self.num_decode_tokens += query_len
|
||||||
|
self.curr_seq_lens.append(curr_seq_len)
|
||||||
|
|
||||||
|
# Compute block table.
|
||||||
|
# TODO(sang): Combine chunked prefill and prefix caching by
|
||||||
|
# only allowing multiple of block_size chunk size.
|
||||||
|
# NOTE: This only works for oooooooxxx style attention.
|
||||||
|
block_table = []
|
||||||
|
if prefix_cache_hit:
|
||||||
|
# NOTE(woosuk): For flash-attn, the block table should
|
||||||
|
# include the entries for the incoming prefill tokens.
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
elif ((chunked_prefill_enabled or not is_prompt)
|
||||||
|
and block_tables is not None):
|
||||||
|
if curr_sliding_window_block == 0:
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
else:
|
||||||
|
block_table = block_tables[seq_id][
|
||||||
|
-curr_sliding_window_block:]
|
||||||
|
self.block_tables.append(block_table)
|
||||||
|
|
||||||
|
# Compute slot mapping.
|
||||||
|
is_profile_run = is_block_tables_empty(block_tables)
|
||||||
|
start_idx = compute_slot_mapping_start_idx(is_prompt, query_len,
|
||||||
|
context_len,
|
||||||
|
self.sliding_window)
|
||||||
|
compute_slot_mapping(is_profile_run, self.slot_mapping, seq_id,
|
||||||
|
seq_len, context_len, start_idx,
|
||||||
|
self.block_size, inter_data.block_tables)
|
||||||
|
|
||||||
|
def _get_graph_runner_block_tables(
|
||||||
|
self, num_seqs: int,
|
||||||
|
block_tables: List[List[int]]) -> torch.Tensor:
|
||||||
|
# The shape of graph_block_tables is
|
||||||
|
# [max batch size, max context len // block size].
|
||||||
|
max_batch_size, max_blocks = self.runner.graph_block_tables.shape
|
||||||
|
assert max_batch_size >= num_seqs
|
||||||
|
|
||||||
|
graph_block_tables = self.runner.graph_block_tables[:num_seqs]
|
||||||
|
for i, block_table in enumerate(block_tables):
|
||||||
|
if block_table:
|
||||||
|
num_blocks = len(block_table)
|
||||||
|
if num_blocks <= max_blocks:
|
||||||
|
graph_block_tables[i, :num_blocks] = block_table
|
||||||
|
else:
|
||||||
|
# It may be possible to have more blocks allocated due
|
||||||
|
# to lookahead slots of multi-step, however, they are
|
||||||
|
# not used anyway, so can be safely ignored.
|
||||||
|
graph_block_tables[
|
||||||
|
i, :max_blocks] = block_table[:max_blocks]
|
||||||
|
|
||||||
|
return torch.from_numpy(graph_block_tables).to(
|
||||||
|
device=self.runner.device, non_blocking=True)
|
||||||
|
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int):
|
||||||
|
"""Build attention metadata with on-device tensors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_lens: The maybe padded sequence lengths of the input sequences.
|
||||||
|
query_lens: The query lengths of the input sequences.
|
||||||
|
cuda_graph_pad_size: The padding size for cuda graph.
|
||||||
|
-1 if cuda graph is not used.
|
||||||
|
batch_size: The maybe padded batch size.
|
||||||
|
"""
|
||||||
|
prefix_cache_hit = any([
|
||||||
|
inter_data.prefix_cache_hit
|
||||||
|
for inter_data in self.input_builder.inter_data_list
|
||||||
|
])
|
||||||
|
for inter_data in self.input_builder.inter_data_list:
|
||||||
|
self._add_seq_group(inter_data,
|
||||||
|
self.input_builder.chunked_prefill_enabled,
|
||||||
|
prefix_cache_hit)
|
||||||
|
|
||||||
|
device = self.runner.device
|
||||||
|
use_captured_graph = cuda_graph_pad_size != -1
|
||||||
|
|
||||||
|
max_query_len = max(query_lens)
|
||||||
|
decode_query_lens = query_lens[self.num_prefills:]
|
||||||
|
if len(decode_query_lens) > 0:
|
||||||
|
max_decode_query_len = max(decode_query_lens)
|
||||||
|
else:
|
||||||
|
max_decode_query_len = 1
|
||||||
|
max_prefill_seq_len = max(self.prefill_seq_lens, default=0)
|
||||||
|
max_decode_seq_len = max(self.curr_seq_lens, default=0)
|
||||||
|
num_decode_tokens = self.num_decode_tokens
|
||||||
|
query_start_loc = list(accumulate(query_lens, initial=0))
|
||||||
|
seq_start_loc = list(accumulate(seq_lens, initial=0))
|
||||||
|
|
||||||
|
num_seqs = len(seq_lens)
|
||||||
|
if use_captured_graph:
|
||||||
|
self.slot_mapping.extend([PAD_SLOT_ID] * cuda_graph_pad_size)
|
||||||
|
self.block_tables.extend([] * cuda_graph_pad_size)
|
||||||
|
num_decode_tokens = batch_size - self.num_prefill_tokens
|
||||||
|
block_tables = self._get_graph_runner_block_tables(
|
||||||
|
num_seqs, self.block_tables)
|
||||||
|
else:
|
||||||
|
block_tables = make_tensor_with_pad(
|
||||||
|
self.block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
assert max_query_len > 0, ("query_lens: {}".format(query_lens))
|
||||||
|
|
||||||
|
assert device is not None
|
||||||
|
context_lens_tensor = async_tensor_h2d(self.context_lens, torch.int,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
seq_lens_tensor = async_tensor_h2d(seq_lens, torch.int, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
slot_mapping_tensor = async_tensor_h2d(self.slot_mapping, torch.long,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
query_start_loc_tensor = async_tensor_h2d(query_start_loc, torch.int32,
|
||||||
|
device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
seq_start_loc_tensor = async_tensor_h2d(seq_start_loc, torch.int32,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
placeholder_index_maps = {
|
||||||
|
modality: placeholder_map.index_map()
|
||||||
|
for modality, placeholder_map in
|
||||||
|
self.multimodal_placeholder_maps.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlashAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
slot_mapping=slot_mapping_tensor,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=num_decode_tokens,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
multi_modal_placeholder_index_maps=placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=max_query_len,
|
||||||
|
max_decode_query_len=max_decode_query_len,
|
||||||
|
max_prefill_seq_len=max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=max_decode_seq_len,
|
||||||
|
query_start_loc=query_start_loc_tensor,
|
||||||
|
seq_start_loc=seq_start_loc_tensor,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=use_captured_graph,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashAttentionImpl(AttentionImpl):
|
||||||
|
"""
|
||||||
|
If the input tensors contain prompt tokens, the layout is as follows:
|
||||||
|
|<--------------- num_prefill_tokens ----------------->|
|
||||||
|
|<--prefill_0-->|<--prefill_1-->|...|<--prefill_N-1--->|
|
||||||
|
|
||||||
|
Otherwise, the layout is as follows:
|
||||||
|
|<----------------- num_decode_tokens ------------------>|
|
||||||
|
|<--decode_0-->|..........|<--decode_M-1-->|<--padding-->|
|
||||||
|
|
||||||
|
Generation tokens can contain padding when cuda-graph is used.
|
||||||
|
Currently, prompt tokens don't contain any padding.
|
||||||
|
|
||||||
|
The prompts might have different lengths, while the generation tokens
|
||||||
|
always have length 1.
|
||||||
|
|
||||||
|
If chunked prefill is enabled, prefill tokens and decode tokens can be
|
||||||
|
batched together in a flattened 1D query.
|
||||||
|
|
||||||
|
|<----- num_prefill_tokens ---->|<------- num_decode_tokens --------->|
|
||||||
|
|<-prefill_0->|...|<-prefill_N-1->|<--decode_0-->|...|<--decode_M-1-->|
|
||||||
|
|
||||||
|
Currently, cuda graph is disabled for chunked prefill, meaning there's no
|
||||||
|
padding between prefill and decode tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"FlashAttention does not support block-sparse attention.")
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.sliding_window = ((sliding_window - 1,
|
||||||
|
0) if sliding_window is not None else (-1, -1))
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
# self.vllm_flash_attn_version = get_flash_attn_version(
|
||||||
|
# requires_alibi=self.alibi_slopes is not None)
|
||||||
|
self.vllm_flash_attn_version = 2
|
||||||
|
if is_quantized_kv_cache(self.kv_cache_dtype) and (
|
||||||
|
not self.kv_cache_dtype.startswith("fp8")
|
||||||
|
or not flash_attn_supports_fp8()):
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"FlashAttention does not support {self.kv_cache_dtype} "
|
||||||
|
"kv-cache on this device "
|
||||||
|
f"(FA supports fp8 = {flash_attn_supports_fp8()}).")
|
||||||
|
if logits_soft_cap is None:
|
||||||
|
# In flash-attn, setting logits_soft_cap as 0 means no soft cap.
|
||||||
|
logits_soft_cap = 0
|
||||||
|
self.logits_soft_cap = logits_soft_cap
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
support_head_sizes = FlashAttentionBackend.get_supported_head_sizes()
|
||||||
|
if head_size not in support_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by FlashAttention. "
|
||||||
|
f"Supported head sizes are: {support_head_sizes}.")
|
||||||
|
self.attn_type = attn_type
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
kv_cache_scale: torch.Tensor,
|
||||||
|
attn_metadata: FlashAttentionMetadata,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
sqrt_alibi: bool = False,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with FlashAttention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads, head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads, head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads, head_size]
|
||||||
|
output: shape = [num_tokens, num_heads, head_size]
|
||||||
|
kv_cache = [2, num_blocks, num_kv_heads, block_size, head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
NOTE: It in-place updates the output tensor.
|
||||||
|
NOTE: FP8 quantization, flash-attn expect the size of
|
||||||
|
{q,k,v}_descale to be (num_sequences, num_kv_heads).
|
||||||
|
We use torch's .expand() to avoid duplicating values
|
||||||
|
"""
|
||||||
|
assert output is not None, "Output tensor must be provided."
|
||||||
|
|
||||||
|
# NOTE(woosuk): FlashAttention2 does not support FP8 KV cache.
|
||||||
|
if self.vllm_flash_attn_version < 3 or output.dtype != torch.bfloat16:
|
||||||
|
assert (
|
||||||
|
layer._k_scale_float == 1.0 and layer._v_scale_float == 1.0), (
|
||||||
|
"key/v_scale is only supported in FlashAttention 3 with "
|
||||||
|
"base dtype bfloat16")
|
||||||
|
|
||||||
|
attn_type = self.attn_type
|
||||||
|
if (attn_type == AttentionType.ENCODER
|
||||||
|
and (not attn_metadata.is_all_encoder_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder attention requires setting "
|
||||||
|
"encoder metadata attributes.")
|
||||||
|
elif (attn_type == AttentionType.ENCODER_DECODER
|
||||||
|
and (not attn_metadata.is_all_cross_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder/decoder cross-attention "
|
||||||
|
"requires setting cross-attention "
|
||||||
|
"metadata attributes.")
|
||||||
|
|
||||||
|
kv_cache_dtype: str = self.kv_cache_dtype
|
||||||
|
softmax_scale: float = self.scale
|
||||||
|
window_size = self.sliding_window
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = self.alibi_slopes
|
||||||
|
logits_soft_cap: Optional[float] = self.logits_soft_cap
|
||||||
|
fp8_attention = kv_cache_dtype.startswith("fp8")
|
||||||
|
|
||||||
|
if fp8_attention and not flash_attn_supports_fp8():
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FlashAttention does not support FP8 kv-cache on this device.")
|
||||||
|
|
||||||
|
if kv_cache.numel() > 0:
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
# We skip updating the KV cache under two conditions:
|
||||||
|
# a. When the Attention Type is ENCODER. In this phase, we compute
|
||||||
|
# only the encoder attention without updating the cache.
|
||||||
|
# b. When both Key and Value are None. This occurs during
|
||||||
|
# cross-attention computation in the decoding phase, where the
|
||||||
|
# KV cache is already populated with the cross-attention
|
||||||
|
# tensor. Thus, we skip cache updates during this time.
|
||||||
|
if (attn_type != AttentionType.ENCODER) and (key is not None) and (
|
||||||
|
value is not None):
|
||||||
|
if attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# Update cross-attention KV cache (prefill-only)
|
||||||
|
updated_slot_mapping = attn_metadata.cross_slot_mapping
|
||||||
|
else:
|
||||||
|
# Update self-attention KV cache (prefill/decode)
|
||||||
|
updated_slot_mapping = attn_metadata.slot_mapping
|
||||||
|
|
||||||
|
# Reshape the input keys and values and store them in the cache.
|
||||||
|
# If kv_cache is not provided, the new key and value tensors are
|
||||||
|
# not cached. This happens during the initial memory
|
||||||
|
# profiling run.
|
||||||
|
ops.reshape_and_cache_flash(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
kv_cache[0],
|
||||||
|
kv_cache[1],
|
||||||
|
updated_slot_mapping.flatten(), # type: ignore[union-attr]
|
||||||
|
kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if fp8_attention:
|
||||||
|
kv_cache = kv_cache.view(torch.float8_e4m3fn)
|
||||||
|
key_cache = key_cache.view(torch.float8_e4m3fn)
|
||||||
|
value_cache = value_cache.view(torch.float8_e4m3fn)
|
||||||
|
|
||||||
|
if fp8_attention:
|
||||||
|
num_tokens, num_heads, head_size = query.shape
|
||||||
|
query, _ = ops.scaled_fp8_quant(
|
||||||
|
query.reshape(
|
||||||
|
(num_tokens, num_heads * head_size)).contiguous(),
|
||||||
|
layer._q_scale)
|
||||||
|
query = query.reshape((num_tokens, num_heads, head_size))
|
||||||
|
|
||||||
|
(num_prefill_query_tokens, num_prefill_kv_tokens,
|
||||||
|
num_decode_query_tokens) = \
|
||||||
|
get_num_prefill_decode_query_kv_tokens(attn_metadata, attn_type)
|
||||||
|
decode_query = query[num_prefill_query_tokens:]
|
||||||
|
decode_output = output[num_prefill_query_tokens:]
|
||||||
|
# QKV for prefill.
|
||||||
|
query = query[:num_prefill_query_tokens]
|
||||||
|
prefill_output = output[:num_prefill_query_tokens]
|
||||||
|
assert query.shape[0] == num_prefill_query_tokens
|
||||||
|
assert decode_query.shape[0] == num_decode_query_tokens
|
||||||
|
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
# Prompt run.
|
||||||
|
if (kv_cache.numel() == 0 or prefill_meta.block_tables is None
|
||||||
|
or prefill_meta.block_tables.numel() == 0):
|
||||||
|
# normal attention
|
||||||
|
# When block_tables are not filled, it means q and k are the
|
||||||
|
# prompt, and they have the same length.
|
||||||
|
q_seq_start_loc, q_seq_len, k_seq_start_loc, k_seq_len = \
|
||||||
|
_get_query_key_seq_metadata(prefill_meta, True, attn_type)
|
||||||
|
|
||||||
|
key = key[:num_prefill_kv_tokens]
|
||||||
|
value = value[:num_prefill_kv_tokens]
|
||||||
|
|
||||||
|
if fp8_attention:
|
||||||
|
num_kv_tokens, num_kv_heads, head_size = key.shape
|
||||||
|
|
||||||
|
key, _ = ops.scaled_fp8_quant(
|
||||||
|
key.reshape((num_kv_tokens,
|
||||||
|
num_kv_heads * head_size)).contiguous(),
|
||||||
|
layer._k_scale)
|
||||||
|
key = key.reshape((num_kv_tokens, num_kv_heads, head_size))
|
||||||
|
|
||||||
|
value, _ = ops.scaled_fp8_quant(
|
||||||
|
value.reshape((num_kv_tokens,
|
||||||
|
num_kv_heads * head_size)).contiguous(),
|
||||||
|
layer._v_scale)
|
||||||
|
value = value.reshape(
|
||||||
|
(num_kv_tokens, num_kv_heads, head_size))
|
||||||
|
|
||||||
|
descale_shape = (q_seq_start_loc.shape[0] - 1, key.shape[1])
|
||||||
|
flash_attn_varlen_func(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
cu_seqlens_q=q_seq_start_loc,
|
||||||
|
cu_seqlens_k=k_seq_start_loc,
|
||||||
|
max_seqlen_q=q_seq_len,
|
||||||
|
max_seqlen_k=k_seq_len,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=_get_causal_option(attn_type),
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
sqrt_alibi=sqrt_alibi,
|
||||||
|
out=prefill_output,
|
||||||
|
# fa_version=self.fa_version,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
assert attn_type == AttentionType.DECODER, (
|
||||||
|
"Only decoder-only models support prefix caching")
|
||||||
|
assert prefill_meta.seq_lens is not None
|
||||||
|
assert prefill_meta.query_start_loc is not None
|
||||||
|
max_seq_len = max(prefill_meta.seq_lens)
|
||||||
|
descale_shape = (prefill_meta.query_start_loc.shape[0] - 1,
|
||||||
|
key.shape[1])
|
||||||
|
flash_attn_varlen_func( # noqa
|
||||||
|
q=query,
|
||||||
|
k=key_cache,
|
||||||
|
v=value_cache,
|
||||||
|
cu_seqlens_q=prefill_meta.query_start_loc,
|
||||||
|
max_seqlen_q=prefill_meta.max_query_len,
|
||||||
|
cu_seqlens_k=prefill_meta.seq_start_loc,
|
||||||
|
max_seqlen_k=max_seq_len,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=True,
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
block_table=prefill_meta.block_tables,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
sqrt_alibi=sqrt_alibi,
|
||||||
|
out=prefill_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
if decode_meta := attn_metadata.decode_metadata:
|
||||||
|
# Decoding run.
|
||||||
|
# Use flash_attn_varlen_func kernel for speculative decoding
|
||||||
|
# because different queries might have different lengths.
|
||||||
|
|
||||||
|
assert decode_meta.max_decode_query_len is not None
|
||||||
|
# use only for actual varlen decoding
|
||||||
|
if decode_meta.max_decode_query_len > 1:
|
||||||
|
assert attn_type == AttentionType.DECODER, (
|
||||||
|
"Only decoder-only models support max_decode_query_len > 1"
|
||||||
|
)
|
||||||
|
assert decode_meta.query_start_loc is not None
|
||||||
|
descale_shape = (decode_meta.query_start_loc.shape[0] - 1,
|
||||||
|
key.shape[1])
|
||||||
|
flash_attn_varlen_func(
|
||||||
|
q=decode_query,
|
||||||
|
k=key_cache,
|
||||||
|
v=value_cache,
|
||||||
|
cu_seqlens_q=decode_meta.query_start_loc,
|
||||||
|
max_seqlen_q=decode_meta.max_decode_query_len,
|
||||||
|
cu_seqlens_k=decode_meta.seq_start_loc,
|
||||||
|
max_seqlen_k=decode_meta.max_decode_seq_len,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=True,
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
block_table=decode_meta.block_tables,
|
||||||
|
sqrt_alibi=sqrt_alibi,
|
||||||
|
out=decode_output,
|
||||||
|
# fa_version=self.fa_version,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Use flash_attn_with_kvcache for normal decoding.
|
||||||
|
(
|
||||||
|
seq_lens_arg,
|
||||||
|
_,
|
||||||
|
block_tables_arg,
|
||||||
|
) = get_seq_len_block_table_args(decode_meta, False, attn_type)
|
||||||
|
descale_shape = (seq_lens_arg.shape[0], key_cache.shape[-2])
|
||||||
|
flash_attn_with_kvcache(
|
||||||
|
q=decode_query.unsqueeze(1),
|
||||||
|
k_cache=key_cache,
|
||||||
|
v_cache=value_cache,
|
||||||
|
block_table=block_tables_arg,
|
||||||
|
cache_seqlens=seq_lens_arg,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=True,
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
use_sqrt_alibi=sqrt_alibi,
|
||||||
|
out=decode_output.unsqueeze(1),
|
||||||
|
# fa_version=self.fa_version,
|
||||||
|
use_cuda_graph=decode_meta.use_cuda_graph,
|
||||||
|
max_context_len=decode_meta.block_tables.shape[-1] * 16 if attn_type == AttentionType.DECODER else decode_meta.max_encoder_seq_len,
|
||||||
|
)
|
||||||
|
return output.view(-1, self.num_heads * self.head_size)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_query_key_seq_metadata(
|
||||||
|
attn_metadata,
|
||||||
|
is_prompt: bool,
|
||||||
|
attn_type: str,
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Returns sequence metadata for key and query based on the specified
|
||||||
|
attention type and whether input is a prompt.
|
||||||
|
|
||||||
|
This function computes the starting locations and maximum sequence lengths
|
||||||
|
for key and query sequences for different attention types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attn_metadata: The attention metadata object
|
||||||
|
is_prompt (bool): A flag indicating if the input is a prompt
|
||||||
|
attn_type (AttentionType): The type of attention being used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing four integers:
|
||||||
|
- Starting location for the query sequence.
|
||||||
|
- Maximum sequence length for the query sequence.
|
||||||
|
- Starting location for the key sequence.
|
||||||
|
- Maximum sequence length for the key sequence.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: If an invalid attention type is provided.
|
||||||
|
"""
|
||||||
|
if attn_type == AttentionType.DECODER:
|
||||||
|
# Decoder self-attention
|
||||||
|
# Choose max_seq_len based on whether we are in prompt_run
|
||||||
|
if is_prompt:
|
||||||
|
max_seq_len = attn_metadata.max_prefill_seq_len
|
||||||
|
else:
|
||||||
|
max_seq_len = attn_metadata.max_decode_seq_len
|
||||||
|
return (attn_metadata.seq_start_loc, max_seq_len,
|
||||||
|
attn_metadata.seq_start_loc, max_seq_len)
|
||||||
|
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# This is cross attention between the where the key
|
||||||
|
# is the precomputed encoder attention and query
|
||||||
|
# is the input sequence.
|
||||||
|
# Choose query max length based on whether it is prompt
|
||||||
|
# or not.
|
||||||
|
if is_prompt:
|
||||||
|
max_seq_len = attn_metadata.max_prefill_seq_len
|
||||||
|
else:
|
||||||
|
max_seq_len = attn_metadata.max_decode_seq_len
|
||||||
|
return (attn_metadata.seq_start_loc, max_seq_len,
|
||||||
|
attn_metadata.encoder_seq_start_loc,
|
||||||
|
attn_metadata.max_encoder_seq_len)
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
# For encoder attention both the query and the key are same i.e the
|
||||||
|
# encoder sequence.
|
||||||
|
return (attn_metadata.encoder_seq_start_loc,
|
||||||
|
attn_metadata.max_encoder_seq_len,
|
||||||
|
attn_metadata.encoder_seq_start_loc,
|
||||||
|
attn_metadata.max_encoder_seq_len)
|
||||||
|
elif attn_type == AttentionType.ENCODER_ONLY:
|
||||||
|
assert is_prompt, "Should not have decode for encoder only model."
|
||||||
|
return (attn_metadata.seq_start_loc, attn_metadata.max_prefill_seq_len,
|
||||||
|
attn_metadata.seq_start_loc, attn_metadata.max_prefill_seq_len)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_causal_option(attn_type: str) -> bool:
|
||||||
|
"""
|
||||||
|
Determine whether the given attention type is suitable for causal
|
||||||
|
attention mechanisms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attn_type (AttentionType): The type of attention being evaluated
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Returns `True` if the attention type is suitable for causal
|
||||||
|
attention (i.e., not encoder, encoder-only, or encoder-decoder),
|
||||||
|
otherwise returns `False`.
|
||||||
|
"""
|
||||||
|
return not (attn_type == AttentionType.ENCODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY
|
||||||
|
or attn_type == AttentionType.ENCODER_DECODER)
|
||||||
1066
vllm/attention/backends/flashinfer.py
Normal file
1066
vllm/attention/backends/flashinfer.py
Normal file
File diff suppressed because it is too large
Load Diff
242
vllm/attention/backends/flashmla.py
Normal file
242
vllm/attention/backends/flashmla.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.mla.common import (MLACommonBackend,
|
||||||
|
MLACommonImpl,
|
||||||
|
MLACommonMetadata,
|
||||||
|
MLACommonMetadataBuilder,
|
||||||
|
MLACommonState)
|
||||||
|
from vllm.attention.ops.flashmla import (flash_mla_with_kvcache,
|
||||||
|
get_mla_metadata,
|
||||||
|
is_flashmla_supported)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import ModelInputForGPUWithSamplingMetadata
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMLABackend(MLACommonBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "FLASHMLA"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["FlashMLAImpl"]:
|
||||||
|
return FlashMLAImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["FlashMLAMetadata"]:
|
||||||
|
return FlashMLAMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["FlashMLAMetadataBuilder"]:
|
||||||
|
return FlashMLAMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["FlashMLAState"]:
|
||||||
|
return FlashMLAState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FlashMLAMetadata(MLACommonMetadata):
|
||||||
|
decode_tile_scheduler_metadata: Optional[Tuple[torch.Tensor,
|
||||||
|
torch.Tensor]] = None
|
||||||
|
decode_num_splits: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self):
|
||||||
|
decode_metadata = super().decode_metadata
|
||||||
|
# TODO: cache assignment?
|
||||||
|
if decode_metadata is not None:
|
||||||
|
decode_metadata.decode_tile_scheduler_metadata=\
|
||||||
|
self.decode_tile_scheduler_metadata
|
||||||
|
decode_metadata.decode_num_splits=\
|
||||||
|
self.decode_num_splits
|
||||||
|
return decode_metadata
|
||||||
|
|
||||||
|
def advance_step(self,
|
||||||
|
model_input: "ModelInputForGPUWithSamplingMetadata",
|
||||||
|
sampled_token_ids: Optional[torch.Tensor],
|
||||||
|
block_size: int,
|
||||||
|
num_seqs: int,
|
||||||
|
num_queries: int,
|
||||||
|
turn_prefills_into_decodes: bool = False):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"advance_step is not implemented for FlashMLA")
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMLAMetadataBuilder(MLACommonMetadataBuilder[FlashMLAMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.num_q_heads = self.runner.model_config.get_num_attention_heads(
|
||||||
|
self.runner.parallel_config)
|
||||||
|
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int):
|
||||||
|
m = super().build(seq_lens, query_lens, cuda_graph_pad_size,
|
||||||
|
batch_size)
|
||||||
|
|
||||||
|
if m.num_decode_tokens > 0:
|
||||||
|
m.decode_tile_scheduler_metadata, m.decode_num_splits = \
|
||||||
|
get_mla_metadata(
|
||||||
|
m.seq_lens_tensor[m.num_prefills:],
|
||||||
|
self.num_q_heads,
|
||||||
|
1, # MQA for the decode path
|
||||||
|
)
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMLAState(MLACommonState[FlashMLAMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
super().__init__(*args, **kwds)
|
||||||
|
|
||||||
|
self.num_q_heads = self.runner.model_config.get_num_attention_heads(
|
||||||
|
self.runner.parallel_config)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def graph_capture(self, max_batch_size: int):
|
||||||
|
# Run a dummy `get_mla_metadata` so we can get the right shapes
|
||||||
|
self._graph_decoder_tile_scheduler_metadata, \
|
||||||
|
self._graph_decode_num_splits = get_mla_metadata(
|
||||||
|
torch.ones(
|
||||||
|
max_batch_size, dtype=torch.int32, device=self.runner.device),
|
||||||
|
self.num_q_heads,
|
||||||
|
1, # MQA for the decode path
|
||||||
|
)
|
||||||
|
|
||||||
|
with super().graph_capture(max_batch_size):
|
||||||
|
yield
|
||||||
|
|
||||||
|
del self._graph_decoder_tile_scheduler_metadata
|
||||||
|
del self._graph_decode_num_splits
|
||||||
|
|
||||||
|
def graph_capture_get_metadata_for_batch(
|
||||||
|
self, batch_size: int, is_encoder_decoder_model: bool = False):
|
||||||
|
metadata = super().graph_capture_get_metadata_for_batch(
|
||||||
|
batch_size, is_encoder_decoder_model)
|
||||||
|
assert metadata.num_decode_tokens > 0
|
||||||
|
|
||||||
|
decoder_tile_scheduler_metadata, decode_num_splits = get_mla_metadata(
|
||||||
|
self._graph_seq_lens[:batch_size],
|
||||||
|
self.num_q_heads,
|
||||||
|
1, # MQA for the decode path
|
||||||
|
)
|
||||||
|
|
||||||
|
self._graph_decoder_tile_scheduler_metadata.copy_(
|
||||||
|
decoder_tile_scheduler_metadata)
|
||||||
|
self._graph_decode_num_splits[:batch_size + 1].copy_(decode_num_splits)
|
||||||
|
|
||||||
|
metadata.decode_tile_scheduler_metadata=\
|
||||||
|
self._graph_decoder_tile_scheduler_metadata
|
||||||
|
metadata.decode_num_splits=\
|
||||||
|
self._graph_decode_num_splits[:batch_size + 1]
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def get_graph_input_buffers(self,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False):
|
||||||
|
input_buffers = super().get_graph_input_buffers(
|
||||||
|
attn_metadata, is_encoder_decoder_model)
|
||||||
|
input_buffers["decode_tile_scheduler_metadata"] = \
|
||||||
|
attn_metadata.decode_metadata.decode_tile_scheduler_metadata
|
||||||
|
input_buffers["decode_num_splits"] = \
|
||||||
|
attn_metadata.decode_metadata.decode_num_splits
|
||||||
|
|
||||||
|
return input_buffers
|
||||||
|
|
||||||
|
def prepare_graph_input_buffers(self,
|
||||||
|
input_buffers,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False):
|
||||||
|
super().prepare_graph_input_buffers(input_buffers, attn_metadata,
|
||||||
|
is_encoder_decoder_model)
|
||||||
|
|
||||||
|
input_buffers["decode_tile_scheduler_metadata"].copy_(
|
||||||
|
attn_metadata.decode_metadata.decode_tile_scheduler_metadata)
|
||||||
|
input_buffers["decode_num_splits"].copy_(
|
||||||
|
attn_metadata.decode_metadata.decode_num_splits)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashMLAImpl(MLACommonImpl[FlashMLAMetadata]):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]],
|
||||||
|
logits_soft_cap: Optional[float],
|
||||||
|
attn_type: str,
|
||||||
|
# MLA Specific Arguments
|
||||||
|
**mla_args) -> None:
|
||||||
|
super().__init__(num_heads, head_size, scale, num_kv_heads,
|
||||||
|
alibi_slopes, sliding_window, kv_cache_dtype,
|
||||||
|
blocksparse_params, logits_soft_cap, attn_type,
|
||||||
|
**mla_args)
|
||||||
|
|
||||||
|
assert is_flashmla_supported(), \
|
||||||
|
"FlashMLA is not supported on this device"
|
||||||
|
|
||||||
|
unsupported_features = [
|
||||||
|
alibi_slopes, sliding_window, blocksparse_params, logits_soft_cap
|
||||||
|
]
|
||||||
|
if any(unsupported_features):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FlashMLAImpl does not support one of the following: "
|
||||||
|
"alibi_slopes, sliding_window, blocksparse_params, "
|
||||||
|
"logits_soft_cap")
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"FlashMLAImpl")
|
||||||
|
|
||||||
|
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FlashMLA with FP8 KV cache not yet supported")
|
||||||
|
|
||||||
|
def _forward_decode(
|
||||||
|
self,
|
||||||
|
q_nope: torch.Tensor,
|
||||||
|
q_pe: torch.Tensor,
|
||||||
|
kv_c_and_k_pe_cache: torch.Tensor,
|
||||||
|
attn_metadata: FlashMLAMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
assert kv_c_and_k_pe_cache.numel() > 0
|
||||||
|
|
||||||
|
decode_meta = attn_metadata.decode_metadata
|
||||||
|
assert decode_meta is not None
|
||||||
|
|
||||||
|
q = torch.cat([q_nope, q_pe], dim=-1)\
|
||||||
|
.unsqueeze(1) # Add seqlen dim of 1 (decode)
|
||||||
|
|
||||||
|
o, _ = flash_mla_with_kvcache(
|
||||||
|
q=q,
|
||||||
|
k_cache=kv_c_and_k_pe_cache.unsqueeze(-2), # Add head dim of 1
|
||||||
|
block_table=decode_meta.block_tables,
|
||||||
|
cache_seqlens=decode_meta.seq_lens_tensor,
|
||||||
|
head_dim_v=self.kv_lora_rank,
|
||||||
|
tile_scheduler_metadata=decode_meta.decode_tile_scheduler_metadata,
|
||||||
|
num_splits=decode_meta.decode_num_splits,
|
||||||
|
softmax_scale=self.scale,
|
||||||
|
causal=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._v_up_proj_and_o_proj(o)
|
||||||
294
vllm/attention/backends/hpu_attn.py
Normal file
294
vllm/attention/backends/hpu_attn.py
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Copyright (C) 2024 Habana Labs, Ltd. an Intel Company
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import vllm_hpu_extension.ops as ops
|
||||||
|
from vllm_hpu_extension.utils import (Matmul, ModuleFusedSDPA, Softmax,
|
||||||
|
VLLMKVCache)
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.utils import CommonAttentionState
|
||||||
|
from vllm.attention.ops.hpu_paged_attn import (HPUPagedAttention,
|
||||||
|
HPUPagedAttentionMetadata)
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HPUAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "HPU_ATTN"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["HPUAttentionImpl"]:
|
||||||
|
return HPUAttentionImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return HPUAttentionMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return HPUPagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: Dict[int, int],
|
||||||
|
) -> None:
|
||||||
|
HPUPagedAttention.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: Dict[int, List[int]],
|
||||||
|
) -> None:
|
||||||
|
HPUPagedAttention.copy_blocks(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HPUAttentionMetadata(HPUPagedAttentionMetadata, AttentionMetadata):
|
||||||
|
"""Metadata for HPUAttentionbackend."""
|
||||||
|
# Currently, input sequences can only contain all prompts
|
||||||
|
# or all decoding. True if all sequences are prompts.
|
||||||
|
is_prompt: bool
|
||||||
|
attn_bias: Optional[torch.Tensor]
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
|
||||||
|
class HPUAttentionImpl(AttentionImpl, torch.nn.Module):
|
||||||
|
"""
|
||||||
|
If the input tensors contain prompt tokens, the layout is as follows:
|
||||||
|
|<--------------- num_prefill_tokens ----------------->|
|
||||||
|
|<--prefill_0-->|<--prefill_1-->|...|<--prefill_N-1--->|
|
||||||
|
|
||||||
|
Otherwise, the layout is as follows:
|
||||||
|
|<----------------- num_decode_tokens ------------------>|
|
||||||
|
|<--decode_0-->|..........|<--decode_M-1-->|<--padding-->|
|
||||||
|
|
||||||
|
Generation tokens can contain padding when cuda-graph is used.
|
||||||
|
Currently, prompt tokens don't contain any padding.
|
||||||
|
|
||||||
|
The prompts might have different lengths, while the generation tokens
|
||||||
|
always have length 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
max_seq_len: int = 4096,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
super(AttentionImpl, self).__init__()
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.matmul_qk = Matmul()
|
||||||
|
self.softmax = Softmax()
|
||||||
|
self.matmul_av = Matmul()
|
||||||
|
self.batch2block_matmul = Matmul()
|
||||||
|
self.block2batch_matmul = Matmul()
|
||||||
|
self.k_cache = VLLMKVCache()
|
||||||
|
self.v_cache = VLLMKVCache()
|
||||||
|
ops.pa_impl = ops.pa
|
||||||
|
|
||||||
|
self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes_tensor = torch.tensor(alibi_slopes,
|
||||||
|
dtype=torch.bfloat16)
|
||||||
|
self.alibi_slopes = alibi_slopes_tensor
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
self.prefill_usefusedsdpa = os.getenv('VLLM_PROMPT_USE_FUSEDSDPA',
|
||||||
|
'0').lower() in ['1', 'true']
|
||||||
|
self.fused_scaled_dot_product_attention = None
|
||||||
|
if self.prefill_usefusedsdpa:
|
||||||
|
assert alibi_slopes is None, \
|
||||||
|
'Prefill with FusedSDPA not supported with alibi slopes!'
|
||||||
|
try:
|
||||||
|
from habana_frameworks.torch.hpex.kernels import FusedSDPA
|
||||||
|
self.fused_scaled_dot_product_attention = ModuleFusedSDPA(
|
||||||
|
FusedSDPA)
|
||||||
|
except ImportError:
|
||||||
|
logger().warning("Could not import HPU FusedSDPA kernel. "
|
||||||
|
"vLLM will use native implementation.")
|
||||||
|
|
||||||
|
suppored_head_sizes = HPUPagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in suppored_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {suppored_head_sizes}.")
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"HPUAttentionImpl")
|
||||||
|
|
||||||
|
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"HPUAttention with FP8 KV cache not yet supported")
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: HPUAttentionMetadata,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with xFormers and PagedAttention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
batch_size, seq_len, hidden_size = query.shape
|
||||||
|
_, seq_len_kv, _ = key.shape
|
||||||
|
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
block_indices = attn_metadata.block_indices
|
||||||
|
block_offsets = attn_metadata.block_offsets
|
||||||
|
if attn_metadata.is_prompt:
|
||||||
|
key = key.unflatten(0, (block_indices.size(0), -1))
|
||||||
|
value = value.unflatten(0, (block_indices.size(0), -1))
|
||||||
|
if kv_cache is not None:
|
||||||
|
key_cache, value_cache = HPUPagedAttention.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
# Reshape the input keys and values and store them in the cache.
|
||||||
|
# If kv_cache is not provided, the new key and value tensors are
|
||||||
|
# not cached. This happens during the initial memory profiling run.
|
||||||
|
key_cache = self.k_cache(key, key_cache, block_indices,
|
||||||
|
block_offsets)
|
||||||
|
value_cache = self.v_cache(value, value_cache, block_indices,
|
||||||
|
block_offsets)
|
||||||
|
|
||||||
|
if attn_metadata.is_prompt:
|
||||||
|
# Prompt run.
|
||||||
|
if not self.prefill_usefusedsdpa:
|
||||||
|
# TODO: move this outside of model
|
||||||
|
assert attn_metadata.attn_bias is not None, \
|
||||||
|
'attn_bias must be set before calling model.forward!'
|
||||||
|
attn_bias = attn_metadata.attn_bias
|
||||||
|
if self.alibi_slopes is not None:
|
||||||
|
position_bias = _make_alibi_bias(self.alibi_slopes,
|
||||||
|
self.num_kv_heads,
|
||||||
|
attn_bias.dtype,
|
||||||
|
attn_bias.shape[-1])
|
||||||
|
attn_bias = attn_bias.tile((1, self.num_kv_heads, 1, 1))
|
||||||
|
attn_bias.add_(position_bias)
|
||||||
|
else:
|
||||||
|
attn_bias = None
|
||||||
|
|
||||||
|
query_shape = (batch_size, seq_len, self.num_heads, self.head_size)
|
||||||
|
kv_shape = (batch_size, seq_len_kv, self.num_kv_heads,
|
||||||
|
self.head_size)
|
||||||
|
out = ops.prompt_attention(
|
||||||
|
query.view(query_shape),
|
||||||
|
key.view(kv_shape),
|
||||||
|
value.view(kv_shape),
|
||||||
|
attn_bias=attn_bias,
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
matmul_qk_op=self.matmul_qk,
|
||||||
|
softmax_op=self.softmax,
|
||||||
|
matmul_av_op=self.matmul_av,
|
||||||
|
fsdpa_op=self.fused_scaled_dot_product_attention,
|
||||||
|
)
|
||||||
|
output = out.reshape(batch_size, seq_len, hidden_size)
|
||||||
|
else:
|
||||||
|
# Decoding run.
|
||||||
|
output = HPUPagedAttention.forward_decode(
|
||||||
|
query=query,
|
||||||
|
key_cache=key_cache,
|
||||||
|
value_cache=value_cache,
|
||||||
|
block_list=attn_metadata.block_list,
|
||||||
|
block_mapping=attn_metadata.block_mapping,
|
||||||
|
block_bias=attn_metadata.attn_bias,
|
||||||
|
block_scales=attn_metadata.block_scales,
|
||||||
|
block_groups=attn_metadata.block_groups,
|
||||||
|
scale=self.scale,
|
||||||
|
matmul_qk_op=self.matmul_qk,
|
||||||
|
matmul_av_op=self.matmul_av,
|
||||||
|
batch2block_matmul_op=self.batch2block_matmul,
|
||||||
|
block2batch_matmul_op=self.block2batch_matmul,
|
||||||
|
keys_fetch_func=self.k_cache.fetch_from_cache,
|
||||||
|
values_fetch_func=self.v_cache.fetch_from_cache)
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(batch_size, seq_len, hidden_size)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_alibi_bias(
|
||||||
|
alibi_slopes: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seq_len: int,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(seq_len, 1)`
|
||||||
|
# here. We find that both biases give the same results, but
|
||||||
|
# the bias below more accurately follows the original ALiBi
|
||||||
|
# paper.
|
||||||
|
# Calculate a matrix where each element represents ith element- jth
|
||||||
|
# element.
|
||||||
|
bias = bias[None, :] - bias[:, None]
|
||||||
|
|
||||||
|
padded_len = (seq_len + 7) // 8 * 8
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = torch.empty(
|
||||||
|
1, # batch size
|
||||||
|
num_heads,
|
||||||
|
seq_len,
|
||||||
|
padded_len,
|
||||||
|
device=alibi_slopes.device,
|
||||||
|
dtype=dtype,
|
||||||
|
)[:, :, :, :seq_len].copy_(bias)
|
||||||
|
bias.mul_(alibi_slopes[:, None, None])
|
||||||
|
if num_heads != num_kv_heads:
|
||||||
|
bias = bias.unflatten(1, (num_kv_heads, num_heads // num_kv_heads))
|
||||||
|
return bias
|
||||||
388
vllm/attention/backends/ipex_attn.py
Normal file
388
vllm/attention/backends/ipex_attn.py
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
""" Attention layer with torch scaled_dot_product_attention
|
||||||
|
and PagedAttention."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm._ipex_ops import ipex_ops
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.utils import CommonAttentionState
|
||||||
|
from vllm.attention.ops.paged_attn import (PagedAttention,
|
||||||
|
PagedAttentionMetadata)
|
||||||
|
|
||||||
|
_PARTITION_SIZE = 512
|
||||||
|
|
||||||
|
|
||||||
|
class IpexAttnBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "IPEX"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["IpexAttnBackendImpl"]:
|
||||||
|
return IpexAttnBackendImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["IpexAttnMetadata"]:
|
||||||
|
return IpexAttnMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return PagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
from vllm._ipex_ops import ipex_ops as ops
|
||||||
|
ops.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
from vllm._ipex_ops import ipex_ops as ops
|
||||||
|
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||||||
|
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||||||
|
ops.copy_blocks(key_caches, value_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IpexAttnMetadata(AttentionMetadata, PagedAttentionMetadata):
|
||||||
|
"""Metadata for IpexAttnBackend.
|
||||||
|
"""
|
||||||
|
# Currently, input sequences can only contain all prompts
|
||||||
|
# or all decoding. True if all sequences are prompts.
|
||||||
|
is_prompt: bool
|
||||||
|
slot_mapping: torch.Tensor
|
||||||
|
seq_lens: Optional[List[int]]
|
||||||
|
seqlen_q: Optional[torch.Tensor]
|
||||||
|
max_seqlen: Optional[int]
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# Set during the execution of the first attention op.
|
||||||
|
# It is a list because it is needed to set per prompt
|
||||||
|
# when alibi slopes is used. It is because of the limitation
|
||||||
|
# from xformer API.
|
||||||
|
# will not appear in the __repr__ and __init__
|
||||||
|
self.attn_bias: Optional[List[torch.Tensor]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["IpexAttnMetadata"]:
|
||||||
|
# Currently chunked prefill is not supported
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
assert self.num_prefills > 0
|
||||||
|
return self
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["IpexAttnMetadata"]:
|
||||||
|
# Currently chunked prefill is not supported
|
||||||
|
if self.num_prefills > 0:
|
||||||
|
assert self.num_decode_tokens == 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class IpexAttnBackendImpl(AttentionImpl[IpexAttnMetadata]):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"IPEX backend does not support block-sparse attention.")
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
self.need_mask = (self.alibi_slopes is not None
|
||||||
|
or self.sliding_window is not None)
|
||||||
|
if logits_soft_cap is None:
|
||||||
|
logits_soft_cap = 0
|
||||||
|
self.logits_soft_cap = logits_soft_cap
|
||||||
|
|
||||||
|
supported_head_sizes = PagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in supported_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {supported_head_sizes}.")
|
||||||
|
if is_quantized_kv_cache(kv_cache_dtype):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"IPEX backend does not support FP8 KV cache. "
|
||||||
|
"Please use xFormers backend instead.")
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"IpexAttnBackendImpl")
|
||||||
|
|
||||||
|
def split_kv_cache(
|
||||||
|
self,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
x = 1
|
||||||
|
num_blocks = kv_cache.shape[1]
|
||||||
|
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
key_cache = key_cache.view(num_blocks, num_kv_heads, head_size // x,
|
||||||
|
-1, x)
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
value_cache = value_cache.view(num_blocks, num_kv_heads, head_size, -1)
|
||||||
|
return key_cache, value_cache
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: IpexAttnMetadata, # type: ignore
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with IPEX varlen_attention and PagedAttention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
assert layer._k_scale_float == 1.0 and layer._v_scale_float == 1.0
|
||||||
|
num_tokens, hidden_size = query.shape
|
||||||
|
# Reshape the query, key, and value tensors.
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
if kv_cache.numel() > 0:
|
||||||
|
key_cache, value_cache = self.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
ipex_ops.reshape_and_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.slot_mapping.flatten(),
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if attn_metadata.is_prompt:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
if (kv_cache.numel() == 0
|
||||||
|
or attn_metadata.block_tables.numel() == 0):
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
key = key.repeat_interleave(self.num_queries_per_kv, dim=1)
|
||||||
|
value = value.repeat_interleave(self.num_queries_per_kv,
|
||||||
|
dim=1)
|
||||||
|
|
||||||
|
if attn_metadata.attn_bias is None:
|
||||||
|
if self.alibi_slopes is not None:
|
||||||
|
att_masks = _make_alibi_bias(
|
||||||
|
self.alibi_slopes, query.dtype,
|
||||||
|
attn_metadata.seq_lens) # type: ignore
|
||||||
|
elif self.sliding_window is not None:
|
||||||
|
att_masks = _make_sliding_window_bias(
|
||||||
|
attn_metadata.seq_lens, self.sliding_window,
|
||||||
|
query.dtype) # type: ignore
|
||||||
|
else:
|
||||||
|
att_masks = _make_sliding_window_bias(
|
||||||
|
attn_metadata.seq_lens, None, dtype=query.dtype)
|
||||||
|
attn_metadata.attn_bias = att_masks
|
||||||
|
|
||||||
|
output = torch.empty(
|
||||||
|
(num_tokens, self.num_heads, self.head_size),
|
||||||
|
dtype=query.dtype,
|
||||||
|
device=query.device)
|
||||||
|
ipex_ops.varlen_attention(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
output,
|
||||||
|
attn_metadata.seqlen_q,
|
||||||
|
attn_metadata.seqlen_q,
|
||||||
|
attn_metadata.max_seqlen,
|
||||||
|
attn_metadata.max_seqlen,
|
||||||
|
pdropout=0.0,
|
||||||
|
softmax_scale=self.scale,
|
||||||
|
zero_tensors=False,
|
||||||
|
is_causal=True,
|
||||||
|
return_softmax=False,
|
||||||
|
gen_=None,
|
||||||
|
logits_soft_cap=self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
raise RuntimeError(
|
||||||
|
"IPEX backend doesn't support prefix decoding.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Decoding run.
|
||||||
|
max_seq_len = attn_metadata.max_decode_seq_len
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
num_seqs, num_heads, head_size = query.shape
|
||||||
|
max_num_partitions = ((max_seq_len + _PARTITION_SIZE - 1) //
|
||||||
|
_PARTITION_SIZE)
|
||||||
|
# NOTE(woosuk): We use a simple heuristic to decide whether to use
|
||||||
|
# PagedAttention V1 or V2. If the number of partitions is 1, we use
|
||||||
|
# V1 to avoid the overhead of reduction. Also, if the number of
|
||||||
|
# sequences or heads is large, we use V1 since there is enough work
|
||||||
|
# to parallelize.
|
||||||
|
# TODO(woosuk): Tune this heuristic.
|
||||||
|
# For context len > 8192, use V2 kernel to avoid shared memory
|
||||||
|
# shortage.
|
||||||
|
use_v1 = (max_seq_len <= 8192 and
|
||||||
|
(max_num_partitions == 1 or num_seqs * num_heads > 512))
|
||||||
|
if use_v1:
|
||||||
|
# Run PagedAttention V1.
|
||||||
|
ipex_ops.paged_attention_v1(
|
||||||
|
output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
attn_metadata.block_tables,
|
||||||
|
attn_metadata.seq_lens_tensor,
|
||||||
|
block_size,
|
||||||
|
max_seq_len,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Run PagedAttention V2.
|
||||||
|
assert _PARTITION_SIZE % block_size == 0
|
||||||
|
tmp_output = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions, head_size),
|
||||||
|
dtype=output.dtype,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
exp_sums = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
max_logits = torch.empty_like(exp_sums)
|
||||||
|
ipex_ops.paged_attention_v2(
|
||||||
|
output,
|
||||||
|
exp_sums,
|
||||||
|
max_logits,
|
||||||
|
tmp_output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
attn_metadata.block_tables,
|
||||||
|
attn_metadata.seq_lens_tensor,
|
||||||
|
block_size,
|
||||||
|
max_seq_len,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(-1, self.num_heads * self.head_size)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_alibi_bias(
|
||||||
|
alibi_slopes: torch.Tensor,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seq_lens: List[int],
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
attn_biases = []
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype, device=alibi_slopes.device)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(seq_len, 1)`
|
||||||
|
# here. We find that both biases give the same results, but
|
||||||
|
# the bias below more accurately follows the original ALiBi
|
||||||
|
# paper.
|
||||||
|
bias = bias[None, :] - bias[:, None]
|
||||||
|
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = bias[None, :].repeat((num_heads, 1, 1))
|
||||||
|
bias.mul_(alibi_slopes[:, None, None])
|
||||||
|
inf_mask = torch.empty(
|
||||||
|
(1, seq_len, seq_len),
|
||||||
|
dtype=bias.dtype,
|
||||||
|
device=alibi_slopes.device).fill_(-torch.inf).triu_(diagonal=1)
|
||||||
|
attn_biases.append((bias + inf_mask).to(dtype))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
|
|
||||||
|
|
||||||
|
def _make_sliding_window_bias(
|
||||||
|
seq_lens: List[int],
|
||||||
|
window_size: Optional[int],
|
||||||
|
dtype: torch.dtype,
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
attn_biases = []
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
tensor = torch.full(
|
||||||
|
(1, seq_len, seq_len),
|
||||||
|
dtype=dtype,
|
||||||
|
fill_value=1,
|
||||||
|
)
|
||||||
|
shift = 0
|
||||||
|
mask = torch.tril(tensor, diagonal=shift).to(dtype) # type: ignore
|
||||||
|
if window_size is not None:
|
||||||
|
mask = torch.triu(mask, diagonal=shift - window_size + 1)
|
||||||
|
mask = torch.log(mask)
|
||||||
|
attn_biases.append(mask.to(dtype))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
0
vllm/attention/backends/mla/__init__.py
Normal file
0
vllm/attention/backends/mla/__init__.py
Normal file
1698
vllm/attention/backends/mla/common.py
Normal file
1698
vllm/attention/backends/mla/common.py
Normal file
File diff suppressed because it is too large
Load Diff
338
vllm/attention/backends/pallas.py
Normal file
338
vllm/attention/backends/pallas.py
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch_xla.experimental.custom_kernel # Required to register custom ops.
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.utils import CommonAttentionState
|
||||||
|
|
||||||
|
|
||||||
|
class PallasAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "PALLAS"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["PallasAttentionBackendImpl"]:
|
||||||
|
return PallasAttentionBackendImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["PallasMetadata"]:
|
||||||
|
return PallasMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (num_kv_heads, num_blocks, block_size, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
raise RuntimeError("swap_blocks is not used for the TPU backend.")
|
||||||
|
|
||||||
|
@torch.compile(backend="openxla")
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[Tuple[torch.Tensor, torch.Tensor]],
|
||||||
|
src_to_dists: Tuple[torch.Tensor, torch.Tensor],
|
||||||
|
) -> None:
|
||||||
|
src_indices, dst_indices = src_to_dists
|
||||||
|
for k_cache, v_cache in kv_caches:
|
||||||
|
torch.ops.xla.dynamo_set_buffer_donor_(k_cache, True)
|
||||||
|
k_cache[:, dst_indices] = k_cache[:, src_indices]
|
||||||
|
torch.ops.xla.dynamo_set_buffer_donor_(v_cache, True)
|
||||||
|
v_cache[:, dst_indices] = v_cache[:, src_indices]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PallasMetadata(AttentionMetadata):
|
||||||
|
|
||||||
|
# Currently, input sequences can only contain all prefills
|
||||||
|
# or all decoding.
|
||||||
|
block_tables: Optional[torch.Tensor] = None
|
||||||
|
context_lens: Optional[torch.Tensor] = None
|
||||||
|
effective_query_lens: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["PallasMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert self.num_decode_tokens == 0
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["PallasMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert self.num_prefills == 0
|
||||||
|
assert self.num_prefill_tokens == 0
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.context_lens is not None
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class PallasAttentionBackendImpl(AttentionImpl):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
self.logits_soft_cap = logits_soft_cap
|
||||||
|
if head_size % 128 != 0:
|
||||||
|
raise NotImplementedError("Head size must be a multiple of 128.")
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
raise NotImplementedError("Alibi slopes is not supported.")
|
||||||
|
if sliding_window is not None:
|
||||||
|
raise NotImplementedError("Sliding window is not supported.")
|
||||||
|
if is_quantized_kv_cache(kv_cache_dtype):
|
||||||
|
raise NotImplementedError("FP8 KV cache dtype is not supported.")
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise NotImplementedError("Blocksparse is not supported.")
|
||||||
|
|
||||||
|
if torch_xla.tpu.version() < 4:
|
||||||
|
raise NotImplementedError("TPU version must be 4 or higher.")
|
||||||
|
|
||||||
|
self.megacore_mode = None
|
||||||
|
tpu_env = torch_xla.tpu.get_tpu_env()
|
||||||
|
tpu_type = (tpu_env.get("ACCELERATOR_TYPE", None)
|
||||||
|
or tpu_env.get("TYPE", None)
|
||||||
|
or tpu_env.get("TPU_ACCELERATOR_TYPE", None))
|
||||||
|
assert tpu_type is not None
|
||||||
|
tpu_type = tpu_type.lower()
|
||||||
|
|
||||||
|
if (("lite" not in tpu_type) and ("v6" not in tpu_type)):
|
||||||
|
if self.num_kv_heads % 2 == 0:
|
||||||
|
self.megacore_mode = "kv_head"
|
||||||
|
else:
|
||||||
|
# NOTE(woosuk): If the batch size is not a multiple of 2, the
|
||||||
|
# megacore mode will be None.
|
||||||
|
self.megacore_mode = "batch"
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"PallasAttentionBackendImpl")
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: Tuple[torch.Tensor, torch.Tensor],
|
||||||
|
attn_metadata: PallasMetadata,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with Pallas attention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [batch_size, seq_len, num_heads * head_size]
|
||||||
|
key: shape = [batch_size, seq_len, num_kv_heads * head_size]
|
||||||
|
value: shape = [batch_size, seq_len, num_kv_heads * head_size]
|
||||||
|
kv_cache[0] = [num_kv_heads, num_blocks, block_size, head_size]
|
||||||
|
kv_cache[1] = [num_kv_heads, num_blocks, block_size, head_size]
|
||||||
|
NOTE: kv_cache[0] and kv_cache[1] will be an empty tensor
|
||||||
|
with shape [0] for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
Returns:
|
||||||
|
shape = [batch_size, seq_len, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
assert layer._k_scale_float == 1.0 and layer._v_scale_float == 1.0
|
||||||
|
batch_size, seq_len, hidden_size = query.shape
|
||||||
|
query = query.view(batch_size, seq_len, self.num_heads, self.head_size)
|
||||||
|
key = key.view(batch_size, seq_len, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(batch_size, seq_len, self.num_kv_heads,
|
||||||
|
self.head_size)
|
||||||
|
|
||||||
|
if kv_cache[0].numel() > 0:
|
||||||
|
slot_mapping = attn_metadata.slot_mapping
|
||||||
|
key_cache, value_cache = kv_cache
|
||||||
|
write_to_kv_cache(key, value, key_cache, value_cache, slot_mapping)
|
||||||
|
|
||||||
|
query = query * self.scale
|
||||||
|
if attn_metadata.num_prefills > 0:
|
||||||
|
if attn_metadata.block_tables is None:
|
||||||
|
# Prefill without paged KV cache.
|
||||||
|
assert seq_len % 16 == 0, (
|
||||||
|
"Pallas FlashAttention kernel requires seq_len to be a "
|
||||||
|
f"multiple of 16 but got {seq_len}")
|
||||||
|
|
||||||
|
# Handle GQA/MQA.
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
key = key.repeat_interleave(self.num_queries_per_kv,
|
||||||
|
dim=-2)
|
||||||
|
key = key.view(batch_size, seq_len, self.num_heads,
|
||||||
|
self.head_size)
|
||||||
|
value = value.repeat_interleave(self.num_queries_per_kv,
|
||||||
|
dim=-2)
|
||||||
|
value = value.view(batch_size, seq_len, self.num_heads,
|
||||||
|
self.head_size)
|
||||||
|
# FlashAttention kernel requires the input shape to be
|
||||||
|
# [batch_size, num_heads, seq_len, d_model]
|
||||||
|
# while the input is [batch_size, seq_len, num_heads, d_model].
|
||||||
|
# Permute the input to match the required format.
|
||||||
|
output = torch.ops.xla.flash_attention(
|
||||||
|
query.permute(0, 2, 1, 3),
|
||||||
|
key.permute(0, 2, 1, 3),
|
||||||
|
value.permute(0, 2, 1, 3),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
output = output.permute(0, 2, 1, 3)
|
||||||
|
else:
|
||||||
|
# Prefill with paged KV cache.
|
||||||
|
# TODO(woosuk): Tune the below knobs.
|
||||||
|
num_kv_pages_per_compute_block = 16
|
||||||
|
num_queries_per_compute_block = 16
|
||||||
|
assert seq_len % num_queries_per_compute_block == 0
|
||||||
|
output = torch.ops.xla.multi_queries_paged_attention(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.context_lens,
|
||||||
|
attn_metadata.block_tables,
|
||||||
|
attn_metadata.effective_query_lens,
|
||||||
|
num_kv_pages_per_compute_block,
|
||||||
|
num_queries_per_compute_block,
|
||||||
|
use_kernel=True,
|
||||||
|
attn_logits_soft_cap=self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Decoding run.
|
||||||
|
assert kv_cache[0].numel() > 0
|
||||||
|
query = query.squeeze(dim=1)
|
||||||
|
pages_per_compute_block = 16 # TODO(woosuk): Tune this value.
|
||||||
|
|
||||||
|
assert attn_metadata.block_tables is not None
|
||||||
|
assert attn_metadata.context_lens is not None
|
||||||
|
# NOTE(woosuk): The PagedAttention Pallas kernel stores the entire
|
||||||
|
# block table in SMEM. Therefore, if the block table is too large,
|
||||||
|
# the kernel compilation will fail. To avoid this, we split the
|
||||||
|
# batch dimension into smaller chunks and run the kernel multiple
|
||||||
|
# times.
|
||||||
|
MAX_SMEM_USAGE = 512 * 1024
|
||||||
|
size_per_seq = 4 * attn_metadata.block_tables.shape[1]
|
||||||
|
max_num_seq = MAX_SMEM_USAGE // size_per_seq
|
||||||
|
|
||||||
|
if batch_size <= max_num_seq:
|
||||||
|
output = paged_attention(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.context_lens,
|
||||||
|
attn_metadata.block_tables,
|
||||||
|
pages_per_compute_block,
|
||||||
|
self.megacore_mode,
|
||||||
|
attn_logits_soft_cap=self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
chunk_size = max_num_seq
|
||||||
|
# Make sure the chunk size is a multiple of 2.
|
||||||
|
chunk_size = chunk_size // 2 * 2
|
||||||
|
num_chunks = (batch_size + chunk_size - 1) // chunk_size
|
||||||
|
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
for chunk_idx in range(num_chunks):
|
||||||
|
chunk_start = chunk_idx * chunk_size
|
||||||
|
chunk_end = chunk_start + chunk_size
|
||||||
|
# NOTE(woosuk): We skip this line because it causes Dynamo
|
||||||
|
# compilation error. Instead, we rely on the slice operation
|
||||||
|
# to handle the out-of-bound case.
|
||||||
|
# chunk_end = min(chunk_end, batch_size)
|
||||||
|
chunk_output = paged_attention(
|
||||||
|
query[chunk_start:chunk_end],
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.context_lens[chunk_start:chunk_end],
|
||||||
|
attn_metadata.block_tables[chunk_start:chunk_end],
|
||||||
|
pages_per_compute_block,
|
||||||
|
self.megacore_mode,
|
||||||
|
attn_logits_soft_cap=self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
output[chunk_start:chunk_end] = chunk_output
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.reshape(batch_size, seq_len, hidden_size)
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_kv_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
torch.ops.xla.dynamo_set_buffer_donor_(key_cache, True)
|
||||||
|
torch.ops.xla.dynamo_set_buffer_donor_(value_cache, True)
|
||||||
|
|
||||||
|
key = key.flatten(0, 2)
|
||||||
|
value = value.flatten(0, 2)
|
||||||
|
key_cache = key_cache.flatten(0, 2)
|
||||||
|
value_cache = value_cache.flatten(0, 2)
|
||||||
|
key_cache.index_copy_(0, slot_mapping, key)
|
||||||
|
value_cache.index_copy_(0, slot_mapping, value)
|
||||||
|
|
||||||
|
|
||||||
|
def paged_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
pages_per_compute_block: int,
|
||||||
|
megacore_mode: Optional[str],
|
||||||
|
*,
|
||||||
|
attn_logits_soft_cap: Optional[float],
|
||||||
|
) -> torch.Tensor:
|
||||||
|
batch_size = query.shape[0]
|
||||||
|
if megacore_mode == "batch" and batch_size % 2 != 0:
|
||||||
|
megacore_mode = None
|
||||||
|
else:
|
||||||
|
megacore_mode = megacore_mode
|
||||||
|
|
||||||
|
return torch.ops.xla.paged_attention(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
context_lens,
|
||||||
|
block_tables,
|
||||||
|
pages_per_compute_block,
|
||||||
|
megacore_mode=megacore_mode,
|
||||||
|
attn_logits_soft_cap=attn_logits_soft_cap,
|
||||||
|
)
|
||||||
399
vllm/attention/backends/placeholder_attn.py
Normal file
399
vllm/attention/backends/placeholder_attn.py
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from itertools import accumulate
|
||||||
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder)
|
||||||
|
from vllm.attention.backends.utils import CommonAttentionState
|
||||||
|
from vllm.multimodal import MultiModalPlaceholderMap
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import (ModelInputForGPUBuilder,
|
||||||
|
ModelInputForGPUWithSamplingMetadata)
|
||||||
|
from vllm.utils import async_tensor_h2d
|
||||||
|
|
||||||
|
# Placeholder attention backend for models like Mamba and pooling models that
|
||||||
|
# lack attention.
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderAttentionBackend(AttentionBackend):
|
||||||
|
"""Placeholder backend for when no attention is needed."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "NO_ATTENTION"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["PlaceholderAttentionImpl"]:
|
||||||
|
return PlaceholderAttentionImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["PlaceholderAttentionMetadataBuilder"]:
|
||||||
|
return PlaceholderAttentionMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["PlaceholderAttentionMetadata"]:
|
||||||
|
return PlaceholderAttentionMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (1, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlaceholderAttentionMetadata(AttentionMetadata):
|
||||||
|
"""Attention metadata for prefill and decode batched together."""
|
||||||
|
# (batch_size,). The sequence length per sequence. Sequence length means
|
||||||
|
# the computed tokens + new tokens None if it is a decoding.
|
||||||
|
seq_lens: Optional[List[int]]
|
||||||
|
# seq_lens stored as a tensor.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
# Maximum sequence length among decode batch. 0 if there are prefill
|
||||||
|
# requests only.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
# (batch_size,) A tensor of context lengths (tokens that are computed
|
||||||
|
# so far).
|
||||||
|
context_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# Whether or not if cuda graph is enabled.
|
||||||
|
# Cuda-graph is currently enabled for decoding only.
|
||||||
|
# TODO(woosuk): Move `use_cuda_graph` out since it's unrelated to attention.
|
||||||
|
use_cuda_graph: bool
|
||||||
|
|
||||||
|
# Maximum query length in the batch.
|
||||||
|
max_query_len: Optional[int]
|
||||||
|
|
||||||
|
# Max number of query tokens among request in the batch.
|
||||||
|
max_decode_query_len: Optional[int]
|
||||||
|
|
||||||
|
# (batch_size + 1,). The cumulative subquery lengths of the sequences in
|
||||||
|
# the batch, used to index into subquery. E.g., if the subquery length
|
||||||
|
# is [4, 6], it is [0, 4, 10].
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Placeholder.
|
||||||
|
block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
_cached_prefill_metadata: Optional["PlaceholderAttentionMetadata"] = None
|
||||||
|
_cached_decode_metadata: Optional["PlaceholderAttentionMetadata"] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["PlaceholderAttentionMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_prefill_metadata is not None:
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
# Compute some attn_metadata fields which default to None
|
||||||
|
query_start_loc = (None if self.query_start_loc is None else
|
||||||
|
self.query_start_loc[:self.num_prefills + 1])
|
||||||
|
seq_lens = (None if self.seq_lens is None else
|
||||||
|
self.seq_lens[:self.num_prefills])
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[:self.num_prefills])
|
||||||
|
seq_start_loc = (None if self.seq_start_loc is None else
|
||||||
|
self.seq_start_loc[:self.num_prefills + 1])
|
||||||
|
context_lens_tensor = (None if self.context_lens_tensor is None else
|
||||||
|
self.context_lens_tensor[:self.num_prefills])
|
||||||
|
|
||||||
|
# Placeholders
|
||||||
|
slot_mapping = torch.empty(0)
|
||||||
|
block_tables = torch.empty(0)
|
||||||
|
|
||||||
|
self._cached_prefill_metadata = PlaceholderAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=0,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=self.
|
||||||
|
multi_modal_placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=self.enable_kv_scales_calculation,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_decode_query_len=0,
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=self.max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=0,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
seq_start_loc=seq_start_loc,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=False,
|
||||||
|
)
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["PlaceholderAttentionMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_decode_metadata is not None:
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
|
||||||
|
# Placeholders
|
||||||
|
slot_mapping = torch.empty(0)
|
||||||
|
block_tables = torch.empty(0)
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[self.num_prefills:])
|
||||||
|
|
||||||
|
self._cached_decode_metadata = PlaceholderAttentionMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_decode_query_len=self.max_decode_query_len,
|
||||||
|
max_query_len=None,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
query_start_loc=(self.query_start_loc[self.num_prefills:] -
|
||||||
|
self.query_start_loc[self.num_prefills])
|
||||||
|
if self.query_start_loc is not None else None,
|
||||||
|
seq_start_loc=self.seq_start_loc[self.num_prefills:]
|
||||||
|
if self.seq_start_loc is not None else None,
|
||||||
|
context_lens_tensor=None,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
)
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
|
||||||
|
def advance_step(self,
|
||||||
|
model_input: "ModelInputForGPUWithSamplingMetadata",
|
||||||
|
sampled_token_ids: Optional[torch.Tensor],
|
||||||
|
block_size: int,
|
||||||
|
num_seqs: int,
|
||||||
|
num_queries: int,
|
||||||
|
turn_prefills_into_decodes: bool = False):
|
||||||
|
"""
|
||||||
|
Update metadata in-place to advance one decode step.
|
||||||
|
"""
|
||||||
|
# When using cudagraph, the num_seqs is padded to the next captured
|
||||||
|
# batch sized, but num_queries tracks the actual number of requests in
|
||||||
|
# the batch. For --enforce-eager mode, num_seqs == num_queries
|
||||||
|
if num_seqs != num_queries:
|
||||||
|
assert num_seqs > num_queries
|
||||||
|
assert self.use_cuda_graph
|
||||||
|
|
||||||
|
assert not turn_prefills_into_decodes, \
|
||||||
|
("Multi-Step + Chunked-Prefill is not supported for attention-free"
|
||||||
|
"models. turn_prefills_into_decodes is a "
|
||||||
|
"Multi-Step + Chunked-Prefill specific parameter.")
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert self.max_decode_seq_len == max(self.seq_lens)
|
||||||
|
|
||||||
|
assert self.num_prefills == 0
|
||||||
|
assert self.num_prefill_tokens == 0
|
||||||
|
assert self.num_decode_tokens == num_seqs
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert len(self.seq_lens) == num_seqs
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
assert self.seq_lens_tensor.shape == (num_seqs, )
|
||||||
|
assert self.max_query_len == 1
|
||||||
|
assert self.max_prefill_seq_len == 0
|
||||||
|
|
||||||
|
assert self.query_start_loc is not None
|
||||||
|
assert self.query_start_loc.shape == (num_queries + 1, )
|
||||||
|
assert self.seq_start_loc is not None
|
||||||
|
assert self.seq_start_loc.shape == (num_seqs + 1, )
|
||||||
|
|
||||||
|
assert self.context_lens_tensor is not None
|
||||||
|
assert self.context_lens_tensor.shape == (num_queries, )
|
||||||
|
|
||||||
|
# Update query lengths. Note that we update only queries and not seqs,
|
||||||
|
# since tensors may be padded due to captured cuda graph batch size
|
||||||
|
for i in range(num_queries):
|
||||||
|
self.seq_lens[i] += 1
|
||||||
|
self.max_decode_seq_len = max(self.seq_lens)
|
||||||
|
|
||||||
|
# Update sequences, masking off entries greater than num_queries
|
||||||
|
device = self.seq_lens_tensor.device
|
||||||
|
mask = torch.arange(self.seq_lens_tensor.size(0),
|
||||||
|
device=device) < num_queries
|
||||||
|
self.seq_lens_tensor += mask.to(self.seq_lens_tensor.dtype)
|
||||||
|
if sampled_token_ids is not None:
|
||||||
|
model_input.input_tokens.masked_scatter_(
|
||||||
|
mask, sampled_token_ids[:num_queries])
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderAttentionMetadataBuilder(
|
||||||
|
AttentionMetadataBuilder[PlaceholderAttentionMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, input_builder: "ModelInputForGPUBuilder"):
|
||||||
|
|
||||||
|
self.input_builder = input_builder
|
||||||
|
self.runner = input_builder.runner
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.prefill_seq_lens: List[int] = []
|
||||||
|
self.context_lens: List[int] = []
|
||||||
|
self.curr_seq_lens: List[int] = []
|
||||||
|
self.multimodal_placeholder_maps: Dict[
|
||||||
|
str,
|
||||||
|
MultiModalPlaceholderMap] = defaultdict(MultiModalPlaceholderMap)
|
||||||
|
self.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.num_decode_tokens = 0
|
||||||
|
|
||||||
|
def _add_seq_group(
|
||||||
|
self, inter_data: "ModelInputForGPUBuilder.InterDataForSeqGroup",
|
||||||
|
chunked_prefill_enabled: bool):
|
||||||
|
"""Add a sequence group to the metadata. Specifically update/append
|
||||||
|
1. context length.
|
||||||
|
"""
|
||||||
|
is_prompt = inter_data.is_prompt
|
||||||
|
|
||||||
|
for (seq_id, token_len, seq_len, curr_seq_len, query_len, context_len,
|
||||||
|
curr_sliding_window_block) in zip(
|
||||||
|
inter_data.seq_ids, [len(t) for t in inter_data.input_tokens],
|
||||||
|
inter_data.orig_seq_lens, inter_data.seq_lens,
|
||||||
|
inter_data.query_lens, inter_data.context_lens,
|
||||||
|
inter_data.curr_sliding_window_blocks):
|
||||||
|
self.context_lens.append(context_len)
|
||||||
|
|
||||||
|
if is_prompt:
|
||||||
|
mm_maps = inter_data.multi_modal_placeholder_maps
|
||||||
|
if mm_maps:
|
||||||
|
for modality, placeholders in mm_maps.items():
|
||||||
|
self.multimodal_placeholder_maps[modality].extend(
|
||||||
|
placeholders)
|
||||||
|
|
||||||
|
self.num_prefills += 1
|
||||||
|
self.num_prefill_tokens += token_len
|
||||||
|
self.prefill_seq_lens.append(seq_len)
|
||||||
|
else:
|
||||||
|
self.num_decode_tokens += query_len
|
||||||
|
self.curr_seq_lens.append(curr_seq_len)
|
||||||
|
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int):
|
||||||
|
"""Build attention metadata with on-device tensors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_lens: The maybe padded sequence lengths of the input sequences.
|
||||||
|
query_lens: The query lengths of the input sequences.
|
||||||
|
cuda_graph_pad_size: The padding size for cuda graph.
|
||||||
|
-1 if cuda graph is not used.
|
||||||
|
batch_size: The maybe padded batch size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Some input builders such as ModelInputForCPUBuilder do not have the
|
||||||
|
# "inter_data_list" attribute.
|
||||||
|
# Let's check inter_data_list exists before we reference it.
|
||||||
|
if hasattr(self.input_builder, "inter_data_list"):
|
||||||
|
for inter_data in self.input_builder.inter_data_list:
|
||||||
|
self._add_seq_group(inter_data,
|
||||||
|
self.input_builder.chunked_prefill_enabled)
|
||||||
|
|
||||||
|
device = self.runner.device
|
||||||
|
use_captured_graph = cuda_graph_pad_size != -1
|
||||||
|
|
||||||
|
max_query_len = max(query_lens)
|
||||||
|
decode_query_lens = query_lens[self.num_prefills:]
|
||||||
|
if len(decode_query_lens) > 0:
|
||||||
|
max_decode_query_len = max(decode_query_lens)
|
||||||
|
else:
|
||||||
|
max_decode_query_len = 1
|
||||||
|
max_prefill_seq_len = max(self.prefill_seq_lens, default=0)
|
||||||
|
max_decode_seq_len = max(self.curr_seq_lens, default=0)
|
||||||
|
num_decode_tokens = self.num_decode_tokens
|
||||||
|
query_start_loc = list(accumulate(query_lens, initial=0))
|
||||||
|
seq_start_loc = list(accumulate(seq_lens, initial=0))
|
||||||
|
|
||||||
|
if use_captured_graph:
|
||||||
|
num_decode_tokens = batch_size - self.num_prefill_tokens
|
||||||
|
assert max_query_len > 0, ("query_lens: {}".format(query_lens))
|
||||||
|
|
||||||
|
assert device is not None
|
||||||
|
context_lens_tensor = async_tensor_h2d(self.context_lens, torch.int,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
seq_lens_tensor = async_tensor_h2d(seq_lens, torch.int, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
query_start_loc_tensor = async_tensor_h2d(query_start_loc, torch.int32,
|
||||||
|
device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
seq_start_loc_tensor = async_tensor_h2d(seq_start_loc, torch.int32,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
|
||||||
|
placeholder_index_maps = {
|
||||||
|
modality: placeholder_map.index_map()
|
||||||
|
for modality, placeholder_map in
|
||||||
|
self.multimodal_placeholder_maps.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Placeholders
|
||||||
|
slot_mapping_tensor = torch.empty(0)
|
||||||
|
block_tables = torch.empty(0)
|
||||||
|
|
||||||
|
return PlaceholderAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
slot_mapping=slot_mapping_tensor,
|
||||||
|
multi_modal_placeholder_index_maps=placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=num_decode_tokens,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=max_query_len,
|
||||||
|
max_decode_query_len=max_decode_query_len,
|
||||||
|
max_prefill_seq_len=max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=max_decode_seq_len,
|
||||||
|
query_start_loc=query_start_loc_tensor,
|
||||||
|
seq_start_loc=seq_start_loc_tensor,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=use_captured_graph,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderAttentionImpl(AttentionImpl):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def forward(self, *args, **kwargs) -> torch.Tensor:
|
||||||
|
raise NotImplementedError
|
||||||
900
vllm/attention/backends/rocm_flash_attn.py
Normal file
900
vllm/attention/backends/rocm_flash_attn.py
Normal file
@@ -0,0 +1,900 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Attention layer ROCm GPUs."""
|
||||||
|
import itertools
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType)
|
||||||
|
from vllm.attention.backends.utils import (CommonAttentionState,
|
||||||
|
CommonMetadataBuilder)
|
||||||
|
from vllm.attention.ops.paged_attn import (PagedAttention,
|
||||||
|
PagedAttentionMetadata)
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
from vllm.platforms.rocm import use_rocm_custom_paged_attention
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import ModelInputForGPUWithSamplingMetadata
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
_PARTITION_SIZE_ROCM = 256
|
||||||
|
|
||||||
|
|
||||||
|
class ROCmFlashAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "ROCM_FLASH"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["ROCmFlashAttentionImpl"]:
|
||||||
|
return ROCmFlashAttentionImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return ROCmFlashAttentionMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["ROCmFlashAttentionMetadataBuilder"]:
|
||||||
|
return ROCmFlashAttentionMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return PagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.copy_blocks(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ROCmFlashAttentionMetadata(AttentionMetadata, PagedAttentionMetadata):
|
||||||
|
"""Metadata for FlashAttentionBackend.
|
||||||
|
|
||||||
|
NOTE: Any python object stored here is not updated when it is
|
||||||
|
cuda-graph replayed. If you have values that need to be changed
|
||||||
|
dynamically, it should be stored in tensor. The tensor has to be
|
||||||
|
updated from `CUDAGraphRunner.forward` API.
|
||||||
|
"""
|
||||||
|
# (batch_size,). The sequence length per sequence. Sequence length means
|
||||||
|
# the computed tokens + new tokens None if it is a decoding.
|
||||||
|
seq_lens: Optional[List[int]]
|
||||||
|
# seq_lens stored as a tensor.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
# Maximum sequence length among decode batch. 0 if there are prefill
|
||||||
|
# requests only.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
|
||||||
|
# Whether or not if cuda graph is enabled.
|
||||||
|
# Cuda-graph is currently enabled for decoding only.
|
||||||
|
# TODO(woosuk): Move `use_cuda_graph` out since it's unrelated to attention.
|
||||||
|
use_cuda_graph: bool
|
||||||
|
|
||||||
|
# NOTE(sang): Definition of context_len, query_len, and seq_len.
|
||||||
|
# |---------- N-1 iteration --------|
|
||||||
|
# |---------------- N iteration ---------------------|
|
||||||
|
# |- tokenA -|......................|-- newTokens ---|
|
||||||
|
# |---------- context_len ----------|
|
||||||
|
# |-------------------- seq_len ----------------------|
|
||||||
|
# |-- query_len ---|
|
||||||
|
|
||||||
|
# Maximum query length in the batch. None for decoding.
|
||||||
|
max_query_len: Optional[int] = None
|
||||||
|
# (batch_size + 1,). The cumulative subquery lengths of the sequences in
|
||||||
|
# the batch, used to index into subquery. E.g., if the subquery length
|
||||||
|
# is [4, 6], it is [0, 4, 10].
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
# (batch_size,) A tensor of context lengths (tokens that are computed
|
||||||
|
# so far).
|
||||||
|
context_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Max number of query tokens among request in the batch.
|
||||||
|
max_decode_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
_cached_prefill_metadata: Optional["ROCmFlashAttentionMetadata"] = None
|
||||||
|
_cached_decode_metadata: Optional["ROCmFlashAttentionMetadata"] = None
|
||||||
|
|
||||||
|
# Begin encoder attn & enc/dec cross-attn fields...
|
||||||
|
|
||||||
|
# Encoder sequence lengths representation
|
||||||
|
encoder_seq_lens: Optional[List[int]] = None
|
||||||
|
encoder_seq_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Maximum sequence length among encoder sequences
|
||||||
|
max_encoder_seq_len: Optional[int] = None
|
||||||
|
|
||||||
|
# Number of tokens input to encoder
|
||||||
|
num_encoder_tokens: Optional[int] = None
|
||||||
|
|
||||||
|
# Cross-attention memory-mapping data structures: slot mapping
|
||||||
|
# and block tables
|
||||||
|
cross_slot_mapping: Optional[torch.Tensor] = None
|
||||||
|
cross_block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["ROCmFlashAttentionMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_prefill_metadata is not None:
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
assert self.block_tables is not None
|
||||||
|
|
||||||
|
self._cached_prefill_metadata = ROCmFlashAttentionMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=0,
|
||||||
|
slot_mapping=self.slot_mapping[:self.num_prefill_tokens],
|
||||||
|
multi_modal_placeholder_index_maps=self.
|
||||||
|
multi_modal_placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=self.enable_kv_scales_calculation,
|
||||||
|
seq_lens=self.seq_lens[:self.num_prefills],
|
||||||
|
seq_lens_tensor=self.seq_lens_tensor[:self.num_prefills],
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=self.max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=0,
|
||||||
|
query_start_loc=None if self.query_start_loc is None else
|
||||||
|
self.query_start_loc[:self.num_prefills + 1],
|
||||||
|
seq_start_loc=None if self.seq_start_loc is None else
|
||||||
|
self.seq_start_loc[:self.num_prefills + 1],
|
||||||
|
context_lens_tensor=None if self.context_lens_tensor is None else
|
||||||
|
self.context_lens_tensor[:self.num_prefills],
|
||||||
|
block_tables=self.block_tables[:self.num_prefills],
|
||||||
|
use_cuda_graph=False,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["ROCmFlashAttentionMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_decode_metadata is not None:
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
|
||||||
|
self._cached_decode_metadata = ROCmFlashAttentionMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=self.slot_mapping[self.num_prefill_tokens:],
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=self.seq_lens_tensor[self.num_prefills:],
|
||||||
|
max_query_len=None,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
query_start_loc=None,
|
||||||
|
seq_start_loc=None,
|
||||||
|
context_lens_tensor=None,
|
||||||
|
block_tables=self.block_tables[self.num_prefills:],
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
# Batch may be composed of prefill|decodes, adjust query start indices
|
||||||
|
# to refer to the start of decodes when the two are split apart.
|
||||||
|
# E.g. in tokens:[3 prefills|6 decodes], query_start_loc=[3,9] => [0,6].
|
||||||
|
if self._cached_decode_metadata.query_start_loc is not None:
|
||||||
|
qs = self._cached_decode_metadata.query_start_loc
|
||||||
|
self._cached_decode_metadata.query_start_loc = qs - qs[0]
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
|
||||||
|
def advance_step(self,
|
||||||
|
model_input: "ModelInputForGPUWithSamplingMetadata",
|
||||||
|
sampled_token_ids: Optional[torch.Tensor],
|
||||||
|
block_size: int,
|
||||||
|
num_seqs: int,
|
||||||
|
num_queries: int,
|
||||||
|
turn_prefills_into_decodes: bool = False):
|
||||||
|
"""
|
||||||
|
Update metadata in-place to advance one decode step.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert not turn_prefills_into_decodes, \
|
||||||
|
("Chunked prefill is not supported with rocm_flash_attn yet."
|
||||||
|
"turn_prefills_into_decodes is a Multi-Step + Chunked-Prefill "
|
||||||
|
"specific parameter.")
|
||||||
|
|
||||||
|
# When using cudagraph, the num_seqs is padded to the next captured
|
||||||
|
# batch sized, but num_queries tracks the actual number of requests in
|
||||||
|
# the batch. For --enforce-eager mode, num_seqs == num_queries
|
||||||
|
if num_seqs != num_queries:
|
||||||
|
assert num_seqs > num_queries
|
||||||
|
assert self.use_cuda_graph
|
||||||
|
|
||||||
|
assert self.num_prefills == 0
|
||||||
|
assert self.num_prefill_tokens == 0
|
||||||
|
assert self.num_decode_tokens == num_seqs
|
||||||
|
assert self.slot_mapping.shape == (num_seqs, )
|
||||||
|
|
||||||
|
assert self.seq_lens is not None
|
||||||
|
assert len(self.seq_lens) == num_seqs
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
assert self.seq_lens_tensor.shape == (num_seqs, )
|
||||||
|
assert self.max_query_len == 1
|
||||||
|
assert self.max_prefill_seq_len == 0
|
||||||
|
assert self.max_decode_seq_len == max(self.seq_lens)
|
||||||
|
|
||||||
|
assert self.query_start_loc is not None
|
||||||
|
assert self.query_start_loc.shape == (num_queries + 1, )
|
||||||
|
assert self.seq_start_loc is not None
|
||||||
|
assert self.seq_start_loc.shape == (num_seqs + 1, )
|
||||||
|
|
||||||
|
assert self.context_lens_tensor is not None
|
||||||
|
assert self.context_lens_tensor.shape == (num_queries, )
|
||||||
|
|
||||||
|
assert self.block_tables is not None
|
||||||
|
assert self.block_tables.shape[0] == num_seqs
|
||||||
|
|
||||||
|
# Update query lengths. Note that we update only queries and not seqs,
|
||||||
|
# since tensors may be padded due to captured cuda graph batch size
|
||||||
|
for i in range(num_queries):
|
||||||
|
self.seq_lens[i] += 1
|
||||||
|
self.max_decode_seq_len = max(self.seq_lens)
|
||||||
|
|
||||||
|
ops.advance_step_flashattn(num_seqs=num_seqs,
|
||||||
|
num_queries=num_queries,
|
||||||
|
block_size=block_size,
|
||||||
|
input_tokens=model_input.input_tokens,
|
||||||
|
sampled_token_ids=sampled_token_ids,
|
||||||
|
input_positions=model_input.input_positions,
|
||||||
|
seq_lens=self.seq_lens_tensor,
|
||||||
|
slot_mapping=self.slot_mapping,
|
||||||
|
block_tables=self.block_tables)
|
||||||
|
|
||||||
|
|
||||||
|
class ROCmFlashAttentionMetadataBuilder(
|
||||||
|
CommonMetadataBuilder[ROCmFlashAttentionMetadata]):
|
||||||
|
|
||||||
|
_metadata_cls = ROCmFlashAttentionMetadata
|
||||||
|
|
||||||
|
|
||||||
|
def _make_alibi_bias(alibi_slopes: torch.Tensor,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seq_lens: Optional[List[int]],
|
||||||
|
make_attn_mask: bool = True) -> List[torch.Tensor]:
|
||||||
|
attn_biases = []
|
||||||
|
if seq_lens:
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(seq_len, 1)`
|
||||||
|
# here. We find that both biases give the same results, but
|
||||||
|
# the bias below more accurately follows the original ALiBi
|
||||||
|
# paper.
|
||||||
|
bias = bias[None, :] - bias[:, None]
|
||||||
|
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = bias[None, :].repeat(
|
||||||
|
(num_heads, 1, 1)).to(alibi_slopes.device)
|
||||||
|
bias.mul_(alibi_slopes[:, None, None])
|
||||||
|
if make_attn_mask:
|
||||||
|
inf_mask = torch.empty(
|
||||||
|
(1, seq_len, seq_len),
|
||||||
|
dtype=bias.dtype).fill_(-torch.inf).triu_(diagonal=1).to(
|
||||||
|
alibi_slopes.device)
|
||||||
|
attn_biases.append((bias + inf_mask).to(dtype))
|
||||||
|
else:
|
||||||
|
attn_biases.append(bias.to(dtype))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
|
|
||||||
|
|
||||||
|
def _get_seq_len_block_table_args(
|
||||||
|
attn_metadata: ROCmFlashAttentionMetadata,
|
||||||
|
attn_type: str,
|
||||||
|
) -> tuple:
|
||||||
|
'''
|
||||||
|
The particular choice of sequence-length
|
||||||
|
attributes which should be extracted from attn_metadata is dependent
|
||||||
|
on the type of attention operation.
|
||||||
|
|
||||||
|
Decoder attn -> select entirely decoder self-attention-related fields
|
||||||
|
Encoder/decoder cross-attn -> select encoder sequence lengths
|
||||||
|
Encoder attn -> select encoder sequence lengths fields
|
||||||
|
Encoder-only attn -> select prefill sequence lengths with
|
||||||
|
bidirectional attention
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention op
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention, encoder-only
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* Appropriate sequence-lengths tensors for query and key
|
||||||
|
* Appropriate max sequence-length scalar
|
||||||
|
* Causal masking flag
|
||||||
|
'''
|
||||||
|
|
||||||
|
if attn_type == AttentionType.ENCODER:
|
||||||
|
assert attn_metadata.encoder_seq_lens is not None
|
||||||
|
assert attn_metadata.encoder_seq_lens_tensor is not None
|
||||||
|
query_seq_start_loc = torch.tensor(
|
||||||
|
list(itertools.accumulate([0] + attn_metadata.encoder_seq_lens)),
|
||||||
|
device=attn_metadata.encoder_seq_lens_tensor.device,
|
||||||
|
dtype=attn_metadata.encoder_seq_lens_tensor.dtype)
|
||||||
|
causal_mask = False
|
||||||
|
|
||||||
|
# No block tables associated with encoder attention
|
||||||
|
return (query_seq_start_loc, attn_metadata.max_encoder_seq_len,
|
||||||
|
query_seq_start_loc, attn_metadata.max_encoder_seq_len,
|
||||||
|
attn_metadata.encoder_seq_lens, causal_mask)
|
||||||
|
|
||||||
|
elif attn_type == AttentionType.ENCODER_ONLY:
|
||||||
|
# For encoder-only models, we use the prefill sequence lengths
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
assert attn_metadata.seq_lens_tensor is not None
|
||||||
|
query_seq_start_loc = torch.tensor(
|
||||||
|
list(itertools.accumulate([0] + attn_metadata.seq_lens)),
|
||||||
|
device=attn_metadata.seq_lens_tensor.device,
|
||||||
|
dtype=attn_metadata.seq_lens_tensor.dtype)
|
||||||
|
max_seq_len = attn_metadata.max_prefill_seq_len
|
||||||
|
# Encoder-only models typically use bidirectional attention
|
||||||
|
causal_mask = False
|
||||||
|
|
||||||
|
return (query_seq_start_loc, max_seq_len, query_seq_start_loc,
|
||||||
|
max_seq_len, attn_metadata.seq_lens, causal_mask)
|
||||||
|
|
||||||
|
elif attn_type == AttentionType.DECODER:
|
||||||
|
# Decoder self-attention
|
||||||
|
# Choose max_seq_len based on whether we are in prompt_run
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
assert attn_metadata.seq_lens_tensor is not None
|
||||||
|
query_seq_start_loc = torch.tensor(
|
||||||
|
list(itertools.accumulate([0] + attn_metadata.seq_lens)),
|
||||||
|
device=attn_metadata.seq_lens_tensor.device,
|
||||||
|
dtype=attn_metadata.seq_lens_tensor.dtype)
|
||||||
|
max_seq_len = attn_metadata.max_prefill_seq_len
|
||||||
|
causal_mask = True
|
||||||
|
|
||||||
|
return (query_seq_start_loc, max_seq_len, query_seq_start_loc,
|
||||||
|
max_seq_len, attn_metadata.seq_lens, causal_mask)
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
assert attn_metadata.encoder_seq_lens_tensor is not None
|
||||||
|
query_start_loc = torch.tensor(
|
||||||
|
list(itertools.accumulate([0] + attn_metadata.seq_lens)),
|
||||||
|
device=attn_metadata.encoder_seq_lens_tensor.device,
|
||||||
|
dtype=attn_metadata.encoder_seq_lens_tensor.dtype)
|
||||||
|
|
||||||
|
assert attn_metadata.encoder_seq_lens is not None
|
||||||
|
assert attn_metadata.seq_lens_tensor is not None
|
||||||
|
key_seq_start_loc = torch.tensor(
|
||||||
|
list(itertools.accumulate([0] + attn_metadata.encoder_seq_lens)),
|
||||||
|
device=attn_metadata.seq_lens_tensor.device,
|
||||||
|
dtype=attn_metadata.seq_lens_tensor.dtype)
|
||||||
|
causal_mask = False
|
||||||
|
|
||||||
|
# Enc/dec cross-attention KVs match encoder sequence length;
|
||||||
|
# cross-attention utilizes special "cross" block tables
|
||||||
|
return (query_start_loc, attn_metadata.max_prefill_seq_len,
|
||||||
|
key_seq_start_loc, attn_metadata.max_encoder_seq_len,
|
||||||
|
attn_metadata.seq_lens, causal_mask)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
class ROCmFlashAttentionImpl(AttentionImpl):
|
||||||
|
"""
|
||||||
|
If the input tensors contain prompt tokens, the layout is as follows:
|
||||||
|
|<--------------- num_prompt_tokens -------------->|
|
||||||
|
|<--prompt_0-->|<--prompt_1-->|...|<--prompt_N-1-->|
|
||||||
|
|
||||||
|
Otherwise, the layout is as follows:
|
||||||
|
|<------------------ num_generation_tokens (M) ----------------->|
|
||||||
|
|<--generation_0-->|..........|<--generation_M-1-->|<--padding-->|
|
||||||
|
|
||||||
|
Generation tokens can contain padding when cuda-graph is used.
|
||||||
|
Currently, prompt tokens don't contain any padding.
|
||||||
|
|
||||||
|
The prompts might have different lengths, while the generation tokens
|
||||||
|
always have length 1.
|
||||||
|
|
||||||
|
If chunked prefill is enabled, prefill tokens and decode tokens can be
|
||||||
|
batched together in a flattened 1D query.
|
||||||
|
|
||||||
|
|<----- num_prefill_tokens ---->|<------- num_decode_tokens ----------->|
|
||||||
|
|<-prompt_0->|...|<-prompt_N-1->|<-generation_0->|...|<-generation_M-1->|
|
||||||
|
|
||||||
|
Currently, cuda graph is disabled for chunked prefill, meaning there's no
|
||||||
|
padding between prefill and decode tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"ROCmFlashAttention does not support blocksparse attention.")
|
||||||
|
|
||||||
|
if logits_soft_cap is None:
|
||||||
|
# In flash-attn, setting logits_soft_cap as 0 means no soft cap.
|
||||||
|
self.logits_soft_cap = 0.0
|
||||||
|
else:
|
||||||
|
self.logits_soft_cap = logits_soft_cap
|
||||||
|
self.attn_type = attn_type
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.sliding_window = ((sliding_window, sliding_window)
|
||||||
|
if sliding_window is not None else (-1, -1))
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
supported_head_sizes = PagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in supported_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {supported_head_sizes}.")
|
||||||
|
|
||||||
|
self.use_naive_attn = False
|
||||||
|
# NOTE: Allow for switching between Triton and CK. Defaulting to triton.
|
||||||
|
self.use_triton_flash_attn = envs.VLLM_USE_TRITON_FLASH_ATTN
|
||||||
|
if self.use_triton_flash_attn:
|
||||||
|
if logits_soft_cap is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"ROCm Triton FlashAttention does not support attention"
|
||||||
|
" logits soft capping."
|
||||||
|
" please try using the ROCm CK "
|
||||||
|
"FA backend instead by setting the env var "
|
||||||
|
"`VLLM_USE_TRITON_FLASH_ATTN=0`")
|
||||||
|
|
||||||
|
from vllm.attention.ops.triton_flash_attention import ( # noqa: F401
|
||||||
|
triton_attention)
|
||||||
|
self.attn_func = triton_attention
|
||||||
|
logger.debug("Using Triton FA in ROCmBackend")
|
||||||
|
if self.sliding_window != (-1, -1):
|
||||||
|
logger.warning("ROCm Triton FA does not currently support "
|
||||||
|
"sliding window attention. If using half "
|
||||||
|
"precision, please try using the ROCm CK "
|
||||||
|
"FA backend instead by setting the env var "
|
||||||
|
"`VLLM_USE_TRITON_FLASH_ATTN=0`")
|
||||||
|
else:
|
||||||
|
# if not using triton, navi3x/navi21/navi10 do not use flash-attn
|
||||||
|
# either
|
||||||
|
if not current_platform.has_device_capability(90):
|
||||||
|
self.use_naive_attn = True
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from flash_attn import flash_attn_varlen_func # noqa: F401
|
||||||
|
self.attn_func = flash_attn_varlen_func
|
||||||
|
logger.debug("Using CK FA in ROCmBackend")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
self.use_naive_attn = True
|
||||||
|
|
||||||
|
if self.use_naive_attn:
|
||||||
|
if logits_soft_cap is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"ROCm Naive FlashAttention does not support "
|
||||||
|
"attention logits soft capping.")
|
||||||
|
|
||||||
|
self.attn_func = _sdpa_attention
|
||||||
|
logger.debug("Using naive (SDPA) attention in ROCmBackend")
|
||||||
|
|
||||||
|
def repeat_kv(self, x: torch.Tensor, n_rep: int) -> torch.Tensor:
|
||||||
|
"""torch.repeat_interleave(x, dim=1, repeats=n_rep)"""
|
||||||
|
tokens, n_kv_heads, head_dim = x.shape
|
||||||
|
return (x[:, :,
|
||||||
|
None, :].expand(tokens, n_kv_heads, n_rep,
|
||||||
|
head_dim).reshape(tokens, n_kv_heads * n_rep,
|
||||||
|
head_dim))
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: ROCmFlashAttentionMetadata,
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with FlashAttention and PagedAttention.
|
||||||
|
|
||||||
|
For decoder-only models: query, key and value must be non-None.
|
||||||
|
|
||||||
|
For encoder/decoder models:
|
||||||
|
* ROCmFlashAttentionImpl.forward() may be invoked for both self- and
|
||||||
|
cross-attention layers.
|
||||||
|
* For self-attention: query, key and value must be non-None.
|
||||||
|
* For cross-attention:
|
||||||
|
* Query must be non-None
|
||||||
|
* During prefill, key and value must be non-None; key and value
|
||||||
|
get cached for use during decode.
|
||||||
|
* During decode, key and value may be None, since:
|
||||||
|
(1) key and value tensors were cached during prefill, and
|
||||||
|
(2) cross-attention key and value tensors do not grow during
|
||||||
|
decode
|
||||||
|
|
||||||
|
A note on how the attn_type (attention type enum) argument impacts
|
||||||
|
attention forward() behavior:
|
||||||
|
|
||||||
|
* DECODER: normal decoder-only behavior;
|
||||||
|
use decoder self-attention block table
|
||||||
|
* ENCODER: no KV caching; pass encoder sequence
|
||||||
|
attributes (encoder_seq_lens/encoder_seq_lens_tensor/
|
||||||
|
max_encoder_seq_len) to kernel, in lieu of decoder
|
||||||
|
sequence attributes (seq_lens/seq_lens_tensor/max_seq_len)
|
||||||
|
* ENCODER_DECODER: cross-attention behavior;
|
||||||
|
use cross-attention block table for caching KVs derived
|
||||||
|
from encoder hidden states; since KV sequence lengths
|
||||||
|
will match encoder sequence lengths, pass encoder sequence
|
||||||
|
attributes to kernel (encoder_seq_lens/encoder_seq_lens_tensor/
|
||||||
|
max_encoder_seq_len)
|
||||||
|
* ENCODER_ONLY: bidirectional attention with no KV caching;
|
||||||
|
use prefill sequence attributes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
attn_type: Select attention type, between encoder attention,
|
||||||
|
decoder self-attention, or encoder/decoder cross-
|
||||||
|
attention. Defaults to decoder self-attention,
|
||||||
|
which is the vLLM default generally
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
if key is not None:
|
||||||
|
assert value is not None
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
else:
|
||||||
|
assert value is None
|
||||||
|
|
||||||
|
# Only update KV cache for decoder self-attention
|
||||||
|
# and encoder-decoder cross-attention
|
||||||
|
if self.attn_type not in [
|
||||||
|
AttentionType.ENCODER, AttentionType.ENCODER_ONLY
|
||||||
|
] and kv_cache.numel() > 0:
|
||||||
|
key_cache, value_cache = PagedAttention.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
if key is not None and value is not None:
|
||||||
|
# Reshape the input keys and values and store them in the
|
||||||
|
# cache. If kv_cache is not provided, the new key and value
|
||||||
|
# tensors are not cached. This happens during the initial
|
||||||
|
# memory profiling run.
|
||||||
|
PagedAttention.write_to_paged_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
attn_metadata.slot_mapping
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
attn_metadata.cross_slot_mapping,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.attn_type != AttentionType.ENCODER:
|
||||||
|
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
elif self.attn_type == AttentionType.ENCODER_ONLY:
|
||||||
|
# For encoder-only models, all tokens are processed in one go
|
||||||
|
num_prefill_tokens = query.shape[0]
|
||||||
|
else:
|
||||||
|
assert attn_metadata.num_encoder_tokens is not None
|
||||||
|
num_prefill_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
# Query for decode. KV is not needed because it is already cached.
|
||||||
|
decode_query = query[num_prefill_tokens:]
|
||||||
|
# QKV for prefill.
|
||||||
|
query = query[:num_prefill_tokens]
|
||||||
|
|
||||||
|
# For encoder-only and encoder models,
|
||||||
|
# we process all tokens at once
|
||||||
|
# For decoder and encoder-decoder,
|
||||||
|
# we may need to limit key/value to prefill tokens
|
||||||
|
if key is not None and value is not None \
|
||||||
|
and self.attn_type not in [AttentionType.ENCODER_DECODER,
|
||||||
|
AttentionType.ENCODER_ONLY]:
|
||||||
|
key = key[:num_prefill_tokens]
|
||||||
|
value = value[:num_prefill_tokens]
|
||||||
|
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
# Prompt run.
|
||||||
|
# normal attention and DECODER
|
||||||
|
if self.attn_type == AttentionType.DECODER and (
|
||||||
|
kv_cache.numel() == 0 or prefill_meta.block_tables is None
|
||||||
|
or prefill_meta.block_tables.numel() == 0):
|
||||||
|
(query_seq_start_loc, query_max_seq_len, key_seq_start_loc,
|
||||||
|
key_max_seq_len, seq_lens,
|
||||||
|
causal_mask) = (prefill_meta.seq_start_loc,
|
||||||
|
prefill_meta.max_prefill_seq_len,
|
||||||
|
prefill_meta.seq_start_loc,
|
||||||
|
prefill_meta.max_prefill_seq_len,
|
||||||
|
attn_metadata.seq_lens, True)
|
||||||
|
# prefix-enabled attention and ENCODER/ENCODER_DECODER
|
||||||
|
else:
|
||||||
|
(query_seq_start_loc, query_max_seq_len, key_seq_start_loc,
|
||||||
|
key_max_seq_len, seq_lens,
|
||||||
|
causal_mask) = _get_seq_len_block_table_args(
|
||||||
|
prefill_meta, self.attn_type)
|
||||||
|
# Prompt run.
|
||||||
|
if kv_cache.numel() == 0 or prefill_meta.block_tables.numel() == 0:
|
||||||
|
# triton attention
|
||||||
|
# When block_tables are not filled, it means q and k are the
|
||||||
|
# prompt, and they have the same length.
|
||||||
|
attn_masks = None
|
||||||
|
if self.use_triton_flash_attn:
|
||||||
|
if self.alibi_slopes is not None:
|
||||||
|
attn_masks = _make_alibi_bias(
|
||||||
|
self.alibi_slopes,
|
||||||
|
query.dtype,
|
||||||
|
seq_lens,
|
||||||
|
make_attn_mask=causal_mask) # type: ignore
|
||||||
|
out, _ = self.attn_func(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
None,
|
||||||
|
query_seq_start_loc,
|
||||||
|
key_seq_start_loc,
|
||||||
|
query_max_seq_len,
|
||||||
|
key_max_seq_len,
|
||||||
|
causal_mask,
|
||||||
|
self.scale,
|
||||||
|
attn_masks[0][None]
|
||||||
|
if attn_masks is not None else None,
|
||||||
|
)
|
||||||
|
elif self.use_naive_attn:
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
# Interleave for MQA workaround.
|
||||||
|
key = self.repeat_kv(key, self.num_queries_per_kv)
|
||||||
|
value = self.repeat_kv(value, self.num_queries_per_kv)
|
||||||
|
if self.alibi_slopes is not None:
|
||||||
|
attn_masks = _make_alibi_bias(
|
||||||
|
self.alibi_slopes,
|
||||||
|
query.dtype,
|
||||||
|
attn_metadata.seq_lens,
|
||||||
|
make_attn_mask=causal_mask) # type: ignore
|
||||||
|
query = query.movedim(0, query.dim() - 2)
|
||||||
|
key = key.movedim(0, key.dim() - 2)
|
||||||
|
value = value.movedim(0, value.dim() - 2)
|
||||||
|
# sdpa math backend attention
|
||||||
|
out = self.attn_func(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
query_seq_start_loc,
|
||||||
|
num_prefill_tokens,
|
||||||
|
self.num_heads,
|
||||||
|
self.head_size,
|
||||||
|
self.scale,
|
||||||
|
attn_masks,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
out = self.attn_func(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
cu_seqlens_q=query_seq_start_loc,
|
||||||
|
cu_seqlens_k=key_seq_start_loc,
|
||||||
|
max_seqlen_q=prefill_meta.max_prefill_seq_len,
|
||||||
|
max_seqlen_k=key_max_seq_len,
|
||||||
|
softmax_scale=self.scale,
|
||||||
|
causal=causal_mask,
|
||||||
|
window_size=self.sliding_window,
|
||||||
|
alibi_slopes=self.alibi_slopes,
|
||||||
|
softcap=self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
|
||||||
|
# common code for prefill
|
||||||
|
assert output[:num_prefill_tokens].shape == out.shape
|
||||||
|
if output.shape[0] > num_prefill_tokens:
|
||||||
|
output[:num_prefill_tokens] = out
|
||||||
|
else:
|
||||||
|
output = out
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention -
|
||||||
|
# not applicable for encoder-only models
|
||||||
|
if self.attn_type != AttentionType.ENCODER_ONLY:
|
||||||
|
output[:
|
||||||
|
num_prefill_tokens] = PagedAttention.forward_prefix(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
prefill_meta.block_tables,
|
||||||
|
prefill_meta.query_start_loc,
|
||||||
|
prefill_meta.seq_lens_tensor,
|
||||||
|
prefill_meta.max_query_len,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.sliding_window[0],
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
# Skip decode phase for encoder-only models
|
||||||
|
if (decode_meta := attn_metadata.decode_metadata) and (
|
||||||
|
self.attn_type != AttentionType.ENCODER_ONLY):
|
||||||
|
# Decoding run.
|
||||||
|
# Whether to use rocm custom paged attention or not
|
||||||
|
num_seqs, num_heads, head_size = decode_query.shape
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
gqa_ratio = num_heads // self.num_kv_heads
|
||||||
|
use_custom = use_rocm_custom_paged_attention(
|
||||||
|
decode_query.dtype, head_size, block_size, gqa_ratio,
|
||||||
|
decode_meta.max_decode_seq_len, self.sliding_window)
|
||||||
|
if use_custom:
|
||||||
|
max_seq_len = (decode_meta.max_decode_seq_len if self.attn_type
|
||||||
|
!= AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.max_encoder_seq_len)
|
||||||
|
assert max_seq_len is not None
|
||||||
|
max_num_partitions = (
|
||||||
|
(max_seq_len + _PARTITION_SIZE_ROCM - 1) //
|
||||||
|
_PARTITION_SIZE_ROCM)
|
||||||
|
assert _PARTITION_SIZE_ROCM % block_size == 0
|
||||||
|
tmp_output = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions, head_size),
|
||||||
|
dtype=output.dtype,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
exp_sums = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
max_logits = torch.empty_like(exp_sums)
|
||||||
|
if num_prefill_tokens > 0:
|
||||||
|
out = output[num_prefill_tokens:]
|
||||||
|
else:
|
||||||
|
out = output
|
||||||
|
|
||||||
|
query_start_loc = None
|
||||||
|
ops.paged_attention_rocm(
|
||||||
|
out,
|
||||||
|
exp_sums,
|
||||||
|
max_logits,
|
||||||
|
tmp_output,
|
||||||
|
decode_query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
decode_meta.block_tables
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.cross_block_tables,
|
||||||
|
decode_meta.seq_lens_tensor
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.encoder_seq_lens_tensor,
|
||||||
|
query_start_loc,
|
||||||
|
block_size,
|
||||||
|
max_seq_len,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
output[num_prefill_tokens:] = PagedAttention.forward_decode(
|
||||||
|
decode_query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
decode_meta.block_tables
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.cross_block_tables,
|
||||||
|
decode_meta.seq_lens_tensor
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.encoder_seq_lens_tensor,
|
||||||
|
decode_meta.max_decode_seq_len
|
||||||
|
if self.attn_type != AttentionType.ENCODER_DECODER else
|
||||||
|
decode_meta.max_encoder_seq_len,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(-1, self.num_heads * self.head_size)
|
||||||
|
|
||||||
|
|
||||||
|
def _sdpa_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
seq_lens: List[int],
|
||||||
|
num_tokens: int,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
attn_masks: Optional[List[torch.Tensor]] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
start = 0
|
||||||
|
output = torch.empty((num_tokens, num_heads, head_size),
|
||||||
|
dtype=query.dtype,
|
||||||
|
device=query.device)
|
||||||
|
|
||||||
|
for i, seq_len in enumerate(seq_lens):
|
||||||
|
end = start + seq_len
|
||||||
|
with torch.nn.attention.sdpa_kernel(
|
||||||
|
torch.nn.attention.SDPBackend.MATH):
|
||||||
|
sub_out = torch.nn.functional.scaled_dot_product_attention(
|
||||||
|
query[:, start:end, :],
|
||||||
|
key[:, start:end, :],
|
||||||
|
value[:, start:end, :],
|
||||||
|
dropout_p=0.0,
|
||||||
|
is_causal=attn_masks is None,
|
||||||
|
attn_mask=attn_masks[i] if attn_masks else None,
|
||||||
|
scale=scale).movedim(query.dim() - 2, 0)
|
||||||
|
output[start:end, :, :] = sub_out
|
||||||
|
start = end
|
||||||
|
|
||||||
|
return output
|
||||||
686
vllm/attention/backends/torch_sdpa.py
Normal file
686
vllm/attention/backends/torch_sdpa.py
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
""" Attention layer with torch scaled_dot_product_attention
|
||||||
|
and PagedAttention."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch.nn.functional import scaled_dot_product_attention
|
||||||
|
|
||||||
|
# yapf conflicts with isort for this block
|
||||||
|
# yapf: disable
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
# yapf: enable
|
||||||
|
from vllm.attention.backends.utils import CommonAttentionState
|
||||||
|
from vllm.attention.ops.ipex_attn import PagedAttention, _use_ipex
|
||||||
|
from vllm.attention.ops.paged_attn import PagedAttentionMetadata
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.utils import make_tensor_with_pad
|
||||||
|
from vllm.worker.cpu_model_runner import ModelInputForCPUBuilder
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TorchSDPABackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "TORCH_SDPA"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["TorchSDPABackendImpl"]:
|
||||||
|
return TorchSDPABackendImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return TorchSDPAMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["TorchSDPAMetadataBuilder"]:
|
||||||
|
return TorchSDPAMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return PagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.copy_blocks(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TorchSDPAMetadata(AttentionMetadata, PagedAttentionMetadata):
|
||||||
|
"""Metadata for TorchSDPABackend.
|
||||||
|
"""
|
||||||
|
# Currently, input sequences can only contain all prompts
|
||||||
|
# or all decoding. True if all sequences are prompts.
|
||||||
|
chunked_prefill: bool
|
||||||
|
seq_lens: Optional[List[int]] = None # For non-chunked prefill
|
||||||
|
|
||||||
|
# For chunked prefill only
|
||||||
|
max_query_len: Optional[int] = None
|
||||||
|
max_kv_len: Optional[int] = None
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
kv_start_loc: Optional[torch.Tensor] = None
|
||||||
|
prefill_block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Begin encoder attn & enc/dec cross-attn fields...
|
||||||
|
# Encoder sequence lengths representation
|
||||||
|
encoder_seq_lens: Optional[List[int]] = None
|
||||||
|
encoder_seq_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Maximum sequence length among encoder sequences
|
||||||
|
max_encoder_seq_len: Optional[int] = None
|
||||||
|
|
||||||
|
# Number of tokens input to encoder
|
||||||
|
num_encoder_tokens: Optional[int] = None
|
||||||
|
|
||||||
|
# Cross-attention memory-mapping data structures: slot mapping
|
||||||
|
# and block tables
|
||||||
|
cross_slot_mapping: Optional[torch.Tensor] = None
|
||||||
|
cross_block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# Set during the execution of the first attention op.
|
||||||
|
# It is a list because it is needed to set per prompt
|
||||||
|
# when alibi slopes is used. It is because of the limitation
|
||||||
|
# from xformer API.
|
||||||
|
# will not appear in the __repr__ and __init__
|
||||||
|
self.attn_bias: Optional[List[torch.Tensor]] = None
|
||||||
|
self.encoder_attn_bias: Optional[List[torch.Tensor]] = None
|
||||||
|
self.cross_attn_bias: Optional[List[torch.Tensor]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_encoder_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for encoder attention is set.
|
||||||
|
'''
|
||||||
|
return ((self.encoder_seq_lens is not None)
|
||||||
|
and (self.encoder_seq_lens_tensor is not None)
|
||||||
|
and (self.max_encoder_seq_len is not None))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_cross_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for enc/dec cross-attention is set.
|
||||||
|
|
||||||
|
Superset of encoder attention required metadata.
|
||||||
|
'''
|
||||||
|
return (self.is_all_encoder_attn_metadata_set
|
||||||
|
and (self.cross_slot_mapping is not None)
|
||||||
|
and (self.cross_block_tables is not None))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["TorchSDPAMetadata"]:
|
||||||
|
if self.num_prefill_tokens == 0:
|
||||||
|
return None
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["TorchSDPAMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_seq_lens(
|
||||||
|
self,
|
||||||
|
attn_type: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Extract appropriate sequence lengths from attention metadata
|
||||||
|
according to attention type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* Appropriate sequence lengths tensor for query
|
||||||
|
* Appropriate sequence lengths tensor for key & value
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
seq_lens_q = self.seq_lens
|
||||||
|
seq_lens_kv = self.seq_lens
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
seq_lens_q = self.encoder_seq_lens
|
||||||
|
seq_lens_kv = self.encoder_seq_lens
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
seq_lens_q = self.seq_lens
|
||||||
|
seq_lens_kv = self.encoder_seq_lens
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
return seq_lens_q, seq_lens_kv
|
||||||
|
|
||||||
|
def get_attn_bias(
|
||||||
|
self,
|
||||||
|
attn_type: str,
|
||||||
|
) -> Optional[List[torch.Tensor]]:
|
||||||
|
'''
|
||||||
|
Extract appropriate attention bias from attention metadata
|
||||||
|
according to attention type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* Appropriate attention bias value given the attention type
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
return self.attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
return self.encoder_attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
return self.cross_attn_bias
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
def set_attn_bias(
|
||||||
|
self,
|
||||||
|
attn_bias: List[torch.Tensor],
|
||||||
|
attn_type: str,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Update appropriate attention bias field of attention metadata,
|
||||||
|
according to attention type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* attn_bias: The desired attention bias value
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
self.attn_bias = attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
self.encoder_attn_bias = attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
self.cross_attn_bias = attn_bias
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
def get_seq_len_block_table_args(
|
||||||
|
self,
|
||||||
|
attn_type: str,
|
||||||
|
) -> tuple:
|
||||||
|
'''
|
||||||
|
The particular choice of sequence-length- and block-table-related
|
||||||
|
attributes which should be extracted from attn_metadata is dependent
|
||||||
|
on the type of attention operation.
|
||||||
|
|
||||||
|
Decoder attn -> select entirely decoder self-attention-related fields
|
||||||
|
Encoder/decoder cross-attn -> select encoder sequence lengths &
|
||||||
|
cross-attn block-tables fields
|
||||||
|
Encoder attn -> select encoder sequence lengths fields & no block tables
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* is_prompt: True if prefill, False otherwise
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* Appropriate sequence-lengths tensor
|
||||||
|
* Appropriate max sequence-length scalar
|
||||||
|
* Appropriate block tables (or None)
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
# Decoder self-attention
|
||||||
|
# Choose max_seq_len based on whether we are in prompt_run
|
||||||
|
return (self.seq_lens_tensor, self.max_decode_seq_len,
|
||||||
|
self.block_tables)
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# Enc/dec cross-attention KVs match encoder sequence length;
|
||||||
|
# cross-attention utilizes special "cross" block tables
|
||||||
|
return (self.encoder_seq_lens_tensor, self.max_encoder_seq_len,
|
||||||
|
self.cross_block_tables)
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
# No block tables associated with encoder attention
|
||||||
|
return (self.encoder_seq_lens_tensor, self.max_encoder_seq_len,
|
||||||
|
None)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
class TorchSDPAMetadataBuilder(AttentionMetadataBuilder[TorchSDPAMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, input_builder: ModelInputForCPUBuilder) -> None:
|
||||||
|
self.chunked_prefill = input_builder.chunked_prefill
|
||||||
|
self.input_builder = input_builder
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.input_data = self.input_builder.input_data
|
||||||
|
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int) -> TorchSDPAMetadata:
|
||||||
|
input_data = self.input_data
|
||||||
|
prefill_seq_lens = seq_lens[0:input_data.num_prefills]
|
||||||
|
prefill_query_lens = query_lens[0:input_data.num_prefills]
|
||||||
|
slot_mapping = torch.tensor(input_data.slot_mapping,
|
||||||
|
dtype=torch.long,
|
||||||
|
device="cpu")
|
||||||
|
|
||||||
|
# For chunked-prefill
|
||||||
|
if self.chunked_prefill and input_data.num_prefill_tokens != 0:
|
||||||
|
prefill_block_tables = make_tensor_with_pad(
|
||||||
|
self.input_data.prefill_block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
query_lens_tensor = torch.tensor(prefill_query_lens,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
kv_lens_tensor = torch.tensor(prefill_seq_lens,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
query_start_loc = torch.zeros(input_data.num_prefills + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
kv_start_loc = torch.zeros(input_data.num_prefills + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu")
|
||||||
|
torch.cumsum(query_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
out=query_start_loc[1:])
|
||||||
|
torch.cumsum(kv_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
out=kv_start_loc[1:])
|
||||||
|
max_query_len = max(prefill_query_lens)
|
||||||
|
max_kv_len = max(prefill_seq_lens)
|
||||||
|
else:
|
||||||
|
prefill_block_tables = None
|
||||||
|
query_start_loc = None
|
||||||
|
kv_start_loc = None
|
||||||
|
max_query_len = None
|
||||||
|
max_kv_len = None
|
||||||
|
|
||||||
|
# For paged attention
|
||||||
|
if input_data.num_decode_tokens != 0:
|
||||||
|
seq_lens_tensor = torch.tensor(
|
||||||
|
input_data.seq_lens[input_data.num_prefills:],
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
block_tables = make_tensor_with_pad(
|
||||||
|
self.input_data.decode_block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
block_tables = torch.tensor([])
|
||||||
|
seq_lens_tensor = torch.tensor(
|
||||||
|
input_data.seq_lens[:input_data.num_prefills],
|
||||||
|
dtype=torch.int32,
|
||||||
|
device="cpu",
|
||||||
|
)
|
||||||
|
|
||||||
|
# For multi-modal models
|
||||||
|
placeholder_index_maps = None
|
||||||
|
if len(input_data.multi_modal_inputs_list) != 0:
|
||||||
|
placeholder_index_maps = {
|
||||||
|
modality: placeholder_map.index_map()
|
||||||
|
for modality, placeholder_map in
|
||||||
|
input_data.multi_modal_placeholder_maps.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
attn_metadata = TorchSDPAMetadata(
|
||||||
|
chunked_prefill=self.chunked_prefill,
|
||||||
|
seq_lens=prefill_seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=max_query_len,
|
||||||
|
max_kv_len=max_kv_len,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
kv_start_loc=kv_start_loc,
|
||||||
|
max_decode_seq_len=input_data.max_decode_seq_len,
|
||||||
|
num_prefills=input_data.num_prefills,
|
||||||
|
num_prefill_tokens=input_data.num_prefill_tokens,
|
||||||
|
num_decode_tokens=input_data.num_decode_tokens,
|
||||||
|
block_tables=block_tables,
|
||||||
|
prefill_block_tables=prefill_block_tables,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return attn_metadata
|
||||||
|
|
||||||
|
|
||||||
|
class TorchSDPABackendImpl(AttentionImpl[TorchSDPAMetadata]):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"Torch SPDA does not support block-sparse attention.")
|
||||||
|
if logits_soft_cap is not None:
|
||||||
|
logger.warning_once("Torch SPDA does not support logits soft cap. "
|
||||||
|
"Outputs may be slightly off.")
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
self.need_mask = (self.alibi_slopes is not None
|
||||||
|
or self.sliding_window is not None)
|
||||||
|
|
||||||
|
supported_head_sizes = PagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in supported_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {supported_head_sizes}.")
|
||||||
|
|
||||||
|
if is_quantized_kv_cache(kv_cache_dtype) and not _use_ipex:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Torch SDPA backend FP8 KV cache requires "
|
||||||
|
"intel_extension_for_pytorch support.")
|
||||||
|
self.attn_type = attn_type
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: TorchSDPAMetadata, # type: ignore
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with torch SDPA and PagedAttention.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
attn_type = self.attn_type
|
||||||
|
if (attn_type == AttentionType.ENCODER
|
||||||
|
and (not attn_metadata.is_all_encoder_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder attention requires setting "
|
||||||
|
"encoder metadata attributes.")
|
||||||
|
elif (attn_type == AttentionType.ENCODER_DECODER
|
||||||
|
and (not attn_metadata.is_all_cross_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder/decoder cross-attention "
|
||||||
|
"requires setting cross-attention "
|
||||||
|
"metadata attributes.")
|
||||||
|
|
||||||
|
# Reshape the query, key, and value tensors.
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
if key is not None:
|
||||||
|
assert value is not None
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
else:
|
||||||
|
assert value is None
|
||||||
|
|
||||||
|
if (attn_type != AttentionType.ENCODER and kv_cache.numel() > 0):
|
||||||
|
# KV-cache during decoder-self- or
|
||||||
|
# encoder-decoder-cross-attention, but not
|
||||||
|
# during encoder attention.
|
||||||
|
#
|
||||||
|
# Even if there are no new key/value pairs to cache,
|
||||||
|
# we still need to break out key_cache and value_cache
|
||||||
|
# i.e. for later use by paged attention
|
||||||
|
key_cache, value_cache = PagedAttention.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
if (key is not None) and (value is not None):
|
||||||
|
if attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# Update cross-attention KV cache (prefill-only)
|
||||||
|
# During cross-attention decode, key & value will be None,
|
||||||
|
# preventing this IF-statement branch from running
|
||||||
|
updated_slot_mapping = attn_metadata.cross_slot_mapping
|
||||||
|
else:
|
||||||
|
# Update self-attention KV cache (prefill/decode)
|
||||||
|
updated_slot_mapping = attn_metadata.slot_mapping
|
||||||
|
|
||||||
|
PagedAttention.write_to_paged_cache(
|
||||||
|
key, value, key_cache, value_cache, updated_slot_mapping,
|
||||||
|
self.kv_cache_dtype, layer._k_scale, layer._v_scale)
|
||||||
|
|
||||||
|
if attn_type != AttentionType.ENCODER:
|
||||||
|
# Decoder self-attention supports chunked prefill.
|
||||||
|
# Encoder/decoder cross-attention requires no chunked
|
||||||
|
# prefill (100% prefill or 100% decode tokens, no mix)
|
||||||
|
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||||
|
else:
|
||||||
|
# Encoder attention - chunked prefill is not applicable;
|
||||||
|
# derive token-count from query shape & and treat them
|
||||||
|
# as 100% prefill tokens
|
||||||
|
assert attn_metadata.num_encoder_tokens is not None
|
||||||
|
num_prefill_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
num_decode_tokens = 0
|
||||||
|
|
||||||
|
if attn_type == AttentionType.DECODER:
|
||||||
|
# Only enforce this shape-constraint for decoder
|
||||||
|
# self-attention
|
||||||
|
assert key.shape[0] == num_prefill_tokens + num_decode_tokens
|
||||||
|
assert value.shape[0] == num_prefill_tokens + num_decode_tokens
|
||||||
|
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
if not prefill_meta.prefill_metadata.chunked_prefill: # type: ignore
|
||||||
|
self._run_sdpa_forward(output,
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
prefill_meta,
|
||||||
|
attn_type=attn_type)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
assert not self.need_mask
|
||||||
|
import intel_extension_for_pytorch.llm.modules as ipex_modules
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
ipex_modules.PagedAttention.flash_attn_varlen_func(
|
||||||
|
output[:prefill_meta.num_prefill_tokens, :, :],
|
||||||
|
query[:prefill_meta.num_prefill_tokens, :, :],
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
prefill_meta.query_start_loc,
|
||||||
|
prefill_meta.kv_start_loc,
|
||||||
|
prefill_meta.max_query_len,
|
||||||
|
prefill_meta.max_kv_len,
|
||||||
|
self.scale,
|
||||||
|
True,
|
||||||
|
prefill_meta.prefill_block_tables,
|
||||||
|
self.alibi_slopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
if decode_meta := attn_metadata.decode_metadata:
|
||||||
|
assert attn_type != AttentionType.ENCODER_ONLY, (
|
||||||
|
"Encoder-only models should not have decode metadata.")
|
||||||
|
# Decoding run.
|
||||||
|
(
|
||||||
|
seq_lens_arg,
|
||||||
|
max_seq_len_arg,
|
||||||
|
block_tables_arg,
|
||||||
|
) = decode_meta.get_seq_len_block_table_args(attn_type)
|
||||||
|
|
||||||
|
PagedAttention.forward_decode(
|
||||||
|
output[attn_metadata.num_prefill_tokens:, :, :],
|
||||||
|
query[attn_metadata.num_prefill_tokens:, :, :],
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
block_tables_arg,
|
||||||
|
seq_lens_arg,
|
||||||
|
max_seq_len_arg,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(-1, self.num_heads * self.head_size)
|
||||||
|
|
||||||
|
def _run_sdpa_forward(
|
||||||
|
self,
|
||||||
|
output: torch.Tensor,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: TorchSDPAMetadata,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
key = key.repeat_interleave(self.num_queries_per_kv, dim=1)
|
||||||
|
value = value.repeat_interleave(self.num_queries_per_kv, dim=1)
|
||||||
|
|
||||||
|
attn_masks = attn_metadata.get_attn_bias(attn_type)
|
||||||
|
if attn_masks is None:
|
||||||
|
if self.alibi_slopes is not None:
|
||||||
|
attn_masks = _make_alibi_bias(
|
||||||
|
self.alibi_slopes, query.dtype,
|
||||||
|
attn_metadata.seq_lens) # type: ignore
|
||||||
|
elif self.sliding_window is not None:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
attn_masks = _make_sliding_window_bias(
|
||||||
|
attn_metadata.seq_lens, self.sliding_window,
|
||||||
|
query.dtype) # type: ignore
|
||||||
|
else:
|
||||||
|
seq_lens, _ = attn_metadata.get_seq_lens(attn_type)
|
||||||
|
attn_masks = [None] * len(seq_lens)
|
||||||
|
attn_metadata.set_attn_bias(attn_masks, attn_type)
|
||||||
|
|
||||||
|
query = query.movedim(0, query.dim() - 2)
|
||||||
|
key = key.movedim(0, key.dim() - 2)
|
||||||
|
value = value.movedim(0, value.dim() - 2)
|
||||||
|
|
||||||
|
causal_attn = (attn_type == AttentionType.DECODER)
|
||||||
|
|
||||||
|
seq_lens_q, seq_lens_kv = attn_metadata.get_seq_lens(attn_type)
|
||||||
|
start_q, start_kv = 0, 0
|
||||||
|
for seq_len_q, seq_len_kv, mask in zip(seq_lens_q, seq_lens_kv,
|
||||||
|
attn_masks):
|
||||||
|
end_q = start_q + seq_len_q
|
||||||
|
end_kv = start_kv + seq_len_kv
|
||||||
|
sub_out = scaled_dot_product_attention(
|
||||||
|
query[None, :, start_q:end_q, :],
|
||||||
|
key[None, :, start_kv:end_kv, :],
|
||||||
|
value[None, :, start_kv:end_kv, :],
|
||||||
|
attn_mask=mask,
|
||||||
|
dropout_p=0.0,
|
||||||
|
is_causal=causal_attn and mask is None,
|
||||||
|
scale=self.scale).squeeze(0).movedim(query.dim() - 2, 0)
|
||||||
|
output[start_q:end_q, :, :] = sub_out
|
||||||
|
start_q, start_kv = end_q, end_kv
|
||||||
|
|
||||||
|
|
||||||
|
def _make_alibi_bias(
|
||||||
|
alibi_slopes: torch.Tensor,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seq_lens: List[int],
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
attn_biases: List[torch.Tensor] = []
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(seq_len, 1)`
|
||||||
|
# here. We find that both biases give the same results, but
|
||||||
|
# the bias below more accurately follows the original ALiBi
|
||||||
|
# paper.
|
||||||
|
bias = bias[None, :] - bias[:, None]
|
||||||
|
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = bias[None, :].repeat((num_heads, 1, 1))
|
||||||
|
bias.mul_(alibi_slopes[:, None, None]).unsqueeze_(0)
|
||||||
|
inf_mask = torch.empty(
|
||||||
|
(1, seq_len, seq_len),
|
||||||
|
dtype=bias.dtype).fill_(-torch.inf).triu_(diagonal=1)
|
||||||
|
attn_biases.append((bias + inf_mask).to(dtype))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
|
|
||||||
|
|
||||||
|
def _make_sliding_window_bias(
|
||||||
|
seq_lens: List[int],
|
||||||
|
window_size: Optional[int],
|
||||||
|
dtype: torch.dtype,
|
||||||
|
) -> List[torch.Tensor]:
|
||||||
|
attn_biases: List[torch.Tensor] = []
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
tensor = torch.full(
|
||||||
|
(1, seq_len, seq_len),
|
||||||
|
dtype=dtype,
|
||||||
|
fill_value=1,
|
||||||
|
)
|
||||||
|
shift = 0
|
||||||
|
mask = torch.tril(tensor, diagonal=shift).to(dtype) # type: ignore
|
||||||
|
if window_size is not None:
|
||||||
|
mask = torch.triu(mask, diagonal=shift - window_size + 1)
|
||||||
|
mask = torch.log(mask)
|
||||||
|
attn_biases.append(mask.to(dtype))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
141
vllm/attention/backends/triton_mla.py
Normal file
141
vllm/attention/backends/triton_mla.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionType,
|
||||||
|
is_quantized_kv_cache)
|
||||||
|
from vllm.attention.backends.mla.common import (MLACommonBackend,
|
||||||
|
MLACommonImpl,
|
||||||
|
MLACommonMetadata)
|
||||||
|
from vllm.attention.ops.triton_decode_attention import decode_attention_fwd
|
||||||
|
import ixformer.inference.functions as ixf_ops
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
|
||||||
|
|
||||||
|
class TritonMLABackend(MLACommonBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "TRITON_MLA"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["TritonMLAImpl"]:
|
||||||
|
return TritonMLAImpl
|
||||||
|
|
||||||
|
|
||||||
|
class TritonMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]],
|
||||||
|
logits_soft_cap: Optional[float],
|
||||||
|
attn_type: str,
|
||||||
|
# MLA Specific Arguments
|
||||||
|
**mla_args) -> None:
|
||||||
|
super().__init__(num_heads, head_size, scale, num_kv_heads,
|
||||||
|
alibi_slopes, sliding_window, kv_cache_dtype,
|
||||||
|
blocksparse_params, logits_soft_cap, attn_type,
|
||||||
|
**mla_args)
|
||||||
|
|
||||||
|
unsupported_features = [
|
||||||
|
alibi_slopes, sliding_window, blocksparse_params, logits_soft_cap
|
||||||
|
]
|
||||||
|
if any(unsupported_features):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"TritonMLAImpl does not support one of the following: "
|
||||||
|
"alibi_slopes, sliding_window, blocksparse_params, "
|
||||||
|
"logits_soft_cap")
|
||||||
|
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"TritonMLAImpl")
|
||||||
|
|
||||||
|
if is_quantized_kv_cache(self.kv_cache_dtype):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"TritonMLA with FP8 KV cache not yet supported")
|
||||||
|
self._k_scale = torch.tensor(1.0, dtype=torch.float32)
|
||||||
|
|
||||||
|
def _forward_decode(
|
||||||
|
self,
|
||||||
|
q_nope: torch.Tensor,
|
||||||
|
q_pe: torch.Tensor,
|
||||||
|
kv_c_and_k_pe_cache: torch.Tensor,
|
||||||
|
kv_c_and_k_pe_cache_scale: torch.Tensor,
|
||||||
|
attn_metadata: MLACommonMetadata,
|
||||||
|
k_c_normed: torch.Tensor=None,
|
||||||
|
k_pe: torch.Tensor=None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
assert kv_c_and_k_pe_cache.numel() > 0
|
||||||
|
|
||||||
|
decode_meta = attn_metadata.decode_metadata
|
||||||
|
assert decode_meta is not None
|
||||||
|
B = q_nope.shape[0]
|
||||||
|
q = torch.cat([q_nope, q_pe], dim=-1)
|
||||||
|
|
||||||
|
o = torch.empty(B,
|
||||||
|
self.num_heads,
|
||||||
|
self.kv_lora_rank,
|
||||||
|
dtype=q_nope.dtype,
|
||||||
|
device=q_nope.device)
|
||||||
|
|
||||||
|
# num_kv_splits = 4 # TODO: heuristic
|
||||||
|
|
||||||
|
# # TODO(lucas) Allocate ahead of time
|
||||||
|
# attn_logits = torch.empty(
|
||||||
|
# (
|
||||||
|
# B,
|
||||||
|
# self.num_heads,
|
||||||
|
# num_kv_splits,
|
||||||
|
# # NOTE(lucas) idk why the +1 is here but sglang has it so we
|
||||||
|
# # just mirror that
|
||||||
|
# self.kv_lora_rank + 1,
|
||||||
|
# ),
|
||||||
|
# dtype=torch.float32,
|
||||||
|
# device=q.device,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # Add a head dim of 1
|
||||||
|
# kv_c_and_k_pe_cache = kv_c_and_k_pe_cache.unsqueeze(1)
|
||||||
|
# kv_c_cache = kv_c_and_k_pe_cache[..., :self.kv_lora_rank]
|
||||||
|
# PAGE_SIZE = kv_c_and_k_pe_cache.size(2)
|
||||||
|
|
||||||
|
# # Run MQA
|
||||||
|
# decode_attention_fwd(q, kv_c_and_k_pe_cache, kv_c_cache, o,
|
||||||
|
# decode_meta.block_tables,
|
||||||
|
# decode_meta.seq_lens_tensor,
|
||||||
|
# num_kv_splits, self.scale, PAGE_SIZE)
|
||||||
|
|
||||||
|
if envs.VLLM_USE_INT8_MLA:
|
||||||
|
q_int8, q_scale,_ = ops.scaled_int8_quant(q)
|
||||||
|
ixf_ops.vllm_paged_attention_mla_int8(
|
||||||
|
o, q_int8, q_scale[...,0].view(-1,q_int8.shape[-2]), kv_c_and_k_pe_cache,kv_c_and_k_pe_cache_scale, self.scale, decode_meta.block_tables, decode_meta.seq_lens_tensor, decode_meta.max_decode_seq_len,decode_meta.use_cuda_graph
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# fused q concat & cache write
|
||||||
|
ixf_ops.vllm_paged_attention_mla_fused(
|
||||||
|
output=o,
|
||||||
|
q_nope=q_nope,
|
||||||
|
q_pe=q_pe.contiguous(),
|
||||||
|
kv_cache=kv_c_and_k_pe_cache,
|
||||||
|
scale=self.scale,
|
||||||
|
block_tables=decode_meta.block_tables,
|
||||||
|
context_lens=decode_meta.seq_lens_tensor,
|
||||||
|
max_context_len=decode_meta.max_decode_seq_len,
|
||||||
|
k_c_normed=k_c_normed,
|
||||||
|
k_pe=k_pe,
|
||||||
|
use_cuda_graph=decode_meta.use_cuda_graph
|
||||||
|
)
|
||||||
|
return self._v_up_proj_and_o_proj(o)
|
||||||
585
vllm/attention/backends/utils.py
Normal file
585
vllm/attention/backends/utils.py
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Attention backend utils"""
|
||||||
|
from collections import defaultdict
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from itertools import accumulate
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention import (AttentionMetadata, AttentionMetadataBuilder,
|
||||||
|
AttentionState)
|
||||||
|
from vllm.attention.backends.abstract import AttentionType
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.multimodal import MultiModalPlaceholderMap
|
||||||
|
from vllm.utils import async_tensor_h2d, make_tensor_with_pad
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner_base import ModelRunnerBase
|
||||||
|
|
||||||
|
# Error string(s) for encoder/decoder
|
||||||
|
# unsupported attention scenarios
|
||||||
|
STR_NOT_IMPL_ENC_DEC_ROCM_HIP = ("ROCm/HIP is not currently supported "
|
||||||
|
"with encoder/decoder models.")
|
||||||
|
|
||||||
|
PAD_SLOT_ID = -1
|
||||||
|
|
||||||
|
# Switch to numpy implementation of compute_slot_mapping
|
||||||
|
# if we have at least this many elements. Could be tuned further.
|
||||||
|
_COMPUTE_SLOT_MAPPING_NUMPY_NUMEL = 256
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import ModelInputForGPUBuilder
|
||||||
|
|
||||||
|
|
||||||
|
def is_block_tables_empty(block_tables: Union[None, Dict]):
|
||||||
|
"""
|
||||||
|
Check if block_tables is None or a dictionary with all None values.
|
||||||
|
"""
|
||||||
|
if block_tables is None:
|
||||||
|
return True
|
||||||
|
return (isinstance(block_tables, dict)
|
||||||
|
and all(value is None for value in block_tables.values()))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_slot_mapping_start_idx(is_prompt: bool, query_len: int,
|
||||||
|
context_len: int, sliding_window: int):
|
||||||
|
"""
|
||||||
|
Compute the start index of slot mapping.
|
||||||
|
"""
|
||||||
|
start_idx = 0
|
||||||
|
if is_prompt and sliding_window is not None:
|
||||||
|
start_idx = max(0, query_len - sliding_window)
|
||||||
|
return start_idx
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_slot_mapping_python(slot_mapping: List[int],
|
||||||
|
block_table: List[int], range_start: int,
|
||||||
|
range_end: int, block_size: int):
|
||||||
|
for i in range(range_start, range_end):
|
||||||
|
block_number = block_table[i // block_size]
|
||||||
|
block_offset = i % block_size
|
||||||
|
slot = block_number * block_size + block_offset
|
||||||
|
slot_mapping.append(slot)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_slot_mapping_numpy(slot_mapping: List[int],
|
||||||
|
block_table: List[int], range_start: int,
|
||||||
|
range_end: int, block_size: int):
|
||||||
|
block_table_array = np.array(block_table)
|
||||||
|
idx = np.arange(range_start, range_end)
|
||||||
|
block_offset = idx % block_size
|
||||||
|
idx //= block_size
|
||||||
|
seq_slot_mapping_array = block_table_array[idx]
|
||||||
|
seq_slot_mapping_array *= block_size
|
||||||
|
seq_slot_mapping_array += block_offset
|
||||||
|
slot_mapping.extend(seq_slot_mapping_array)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_slot_mapping(is_profile_run: bool, slot_mapping: List[int],
|
||||||
|
seq_id: int, seq_len: int, context_len: int,
|
||||||
|
start_idx: int, block_size: int,
|
||||||
|
block_tables: Dict[int, List[int]]):
|
||||||
|
"""
|
||||||
|
Compute slot mapping.
|
||||||
|
"""
|
||||||
|
if is_profile_run:
|
||||||
|
# During memory profiling, the block tables are not
|
||||||
|
# initialized yet. In this case, we just use a dummy
|
||||||
|
# slot mapping.
|
||||||
|
# In embeddings, the block tables are {seq_id: None}.
|
||||||
|
slot_mapping.extend([PAD_SLOT_ID] * seq_len)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mask the [0, start_idx) tokens of the prompt with
|
||||||
|
# PAD_SLOT_ID, where start_idx is max(0, seq_len -
|
||||||
|
# sliding_window). For example, if the prompt len is 10,
|
||||||
|
# sliding window is 8, and block size is 4, the first two
|
||||||
|
# tokens are masked and the slot mapping will be
|
||||||
|
# [-1, -1, 2, 3, 4, 5, 6, 7, 0, 1].
|
||||||
|
padding_mask_len = max(0, start_idx - context_len)
|
||||||
|
slot_mapping.extend([PAD_SLOT_ID] * padding_mask_len)
|
||||||
|
|
||||||
|
range_start = max(start_idx, context_len)
|
||||||
|
range_end = seq_len
|
||||||
|
numel = range_end - range_start
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
|
||||||
|
# numpy implementation will be faster than python if we have
|
||||||
|
# many elements, otherwise it will be slower.
|
||||||
|
if numel < _COMPUTE_SLOT_MAPPING_NUMPY_NUMEL:
|
||||||
|
_compute_slot_mapping_python(slot_mapping, block_table, range_start,
|
||||||
|
range_end, block_size)
|
||||||
|
else:
|
||||||
|
_compute_slot_mapping_numpy(slot_mapping, block_table, range_start,
|
||||||
|
range_end, block_size)
|
||||||
|
|
||||||
|
|
||||||
|
TAttentionMetadata = TypeVar("TAttentionMetadata", bound='AttentionMetadata')
|
||||||
|
|
||||||
|
|
||||||
|
class CommonMetadataBuilder(AttentionMetadataBuilder[TAttentionMetadata]):
|
||||||
|
|
||||||
|
_metadata_cls: Type[TAttentionMetadata]
|
||||||
|
|
||||||
|
def __init__(self, input_builder: "ModelInputForGPUBuilder"):
|
||||||
|
self.input_builder = input_builder
|
||||||
|
self.runner = input_builder.runner
|
||||||
|
|
||||||
|
self.sliding_window = input_builder.sliding_window
|
||||||
|
self.block_size = input_builder.block_size
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
self.slot_mapping: List[int] = []
|
||||||
|
self.prefill_seq_lens: List[int] = []
|
||||||
|
self.context_lens: List[int] = []
|
||||||
|
self.block_tables: List[List[int]] = []
|
||||||
|
self.curr_seq_lens: List[int] = []
|
||||||
|
self.multimodal_placeholder_maps: Dict[
|
||||||
|
str,
|
||||||
|
MultiModalPlaceholderMap] = defaultdict(MultiModalPlaceholderMap)
|
||||||
|
self.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.num_decode_tokens = 0
|
||||||
|
|
||||||
|
def _add_seq_group(
|
||||||
|
self, inter_data: "ModelInputForGPUBuilder.InterDataForSeqGroup",
|
||||||
|
chunked_prefill_enabled: bool):
|
||||||
|
is_prompt = inter_data.is_prompt
|
||||||
|
block_tables = inter_data.block_tables
|
||||||
|
|
||||||
|
for (seq_id, token_len, seq_len, curr_seq_len, query_len, context_len,
|
||||||
|
curr_sliding_window_block) in zip(
|
||||||
|
inter_data.seq_ids, [len(t) for t in inter_data.input_tokens],
|
||||||
|
inter_data.orig_seq_lens, inter_data.seq_lens,
|
||||||
|
inter_data.query_lens, inter_data.context_lens,
|
||||||
|
inter_data.curr_sliding_window_blocks):
|
||||||
|
self.context_lens.append(context_len)
|
||||||
|
if is_prompt:
|
||||||
|
mm_maps = inter_data.multi_modal_placeholder_maps
|
||||||
|
if mm_maps:
|
||||||
|
for modality, placeholders in mm_maps.items():
|
||||||
|
self.multimodal_placeholder_maps[modality].extend(
|
||||||
|
placeholders)
|
||||||
|
|
||||||
|
self.num_prefills += 1
|
||||||
|
self.num_prefill_tokens += token_len
|
||||||
|
self.prefill_seq_lens.append(seq_len)
|
||||||
|
else:
|
||||||
|
assert query_len == 1, (
|
||||||
|
"seq_len: {}, context_len: {}, query_len: {}".format(
|
||||||
|
seq_len, context_len, query_len))
|
||||||
|
self.num_decode_tokens += query_len
|
||||||
|
self.curr_seq_lens.append(curr_seq_len)
|
||||||
|
|
||||||
|
# Compute block table.
|
||||||
|
# TODO(sang): Combine chunked prefill and prefix caching by
|
||||||
|
# only allowing multiple of block_size chunk size.
|
||||||
|
# NOTE: This only works for oooooooxxx style attention.
|
||||||
|
block_table = []
|
||||||
|
if inter_data.prefix_cache_hit:
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
elif ((chunked_prefill_enabled or not is_prompt)
|
||||||
|
and block_tables is not None):
|
||||||
|
if curr_sliding_window_block == 0:
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
else:
|
||||||
|
block_table = block_tables[seq_id][
|
||||||
|
-curr_sliding_window_block:]
|
||||||
|
self.block_tables.append(block_table)
|
||||||
|
|
||||||
|
# Compute slot mapping.
|
||||||
|
is_profile_run = is_block_tables_empty(block_tables)
|
||||||
|
start_idx = compute_slot_mapping_start_idx(is_prompt, query_len,
|
||||||
|
context_len,
|
||||||
|
self.sliding_window)
|
||||||
|
compute_slot_mapping(is_profile_run, self.slot_mapping, seq_id,
|
||||||
|
seq_len, context_len, start_idx,
|
||||||
|
self.block_size, inter_data.block_tables)
|
||||||
|
|
||||||
|
def build(self, seq_lens: List[int], query_lens: List[int],
|
||||||
|
cuda_graph_pad_size: int, batch_size: int):
|
||||||
|
"""Build attention metadata with on-device tensors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_lens: The maybe padded sequence lengths of the input sequences.
|
||||||
|
query_lens: The query lengths of the input sequences.
|
||||||
|
cuda_graph_pad_size: The padding size for cuda graph.
|
||||||
|
-1 if cuda graph is not used.
|
||||||
|
batch_size: The maybe padded batch size.
|
||||||
|
"""
|
||||||
|
for inter_data in self.input_builder.inter_data_list:
|
||||||
|
self._add_seq_group(inter_data,
|
||||||
|
self.input_builder.chunked_prefill_enabled)
|
||||||
|
|
||||||
|
device = self.runner.device
|
||||||
|
use_captured_graph = cuda_graph_pad_size != -1
|
||||||
|
|
||||||
|
max_query_len = max(query_lens)
|
||||||
|
max_prefill_seq_len = max(self.prefill_seq_lens, default=0)
|
||||||
|
max_decode_seq_len = max(self.curr_seq_lens, default=0)
|
||||||
|
num_decode_tokens = self.num_decode_tokens
|
||||||
|
query_start_loc = list(accumulate(query_lens, initial=0))
|
||||||
|
seq_start_loc = list(accumulate(seq_lens, initial=0))
|
||||||
|
|
||||||
|
if use_captured_graph:
|
||||||
|
self.slot_mapping.extend([PAD_SLOT_ID] * cuda_graph_pad_size)
|
||||||
|
self.block_tables.extend([] * cuda_graph_pad_size)
|
||||||
|
num_decode_tokens = batch_size
|
||||||
|
|
||||||
|
# The shape of graph_block_tables is
|
||||||
|
# [max batch size, max context len // block size].
|
||||||
|
input_block_tables = self.runner.graph_block_tables[:batch_size]
|
||||||
|
for i, block_table in enumerate(self.block_tables):
|
||||||
|
if block_table:
|
||||||
|
input_block_tables[i, :len(block_table)] = block_table
|
||||||
|
block_tables = torch.from_numpy(input_block_tables).to(
|
||||||
|
device, non_blocking=True)
|
||||||
|
else:
|
||||||
|
block_tables = make_tensor_with_pad(
|
||||||
|
self.block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
assert max_query_len > 0, "query_lens: {}".format(query_lens)
|
||||||
|
|
||||||
|
assert device is not None
|
||||||
|
context_lens_tensor = async_tensor_h2d(self.context_lens, torch.int,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
seq_lens_tensor = async_tensor_h2d(seq_lens, torch.int, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
slot_mapping_tensor = async_tensor_h2d(self.slot_mapping, torch.long,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
query_start_loc_tensor = async_tensor_h2d(query_start_loc, torch.int32,
|
||||||
|
device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
seq_start_loc_tensor = async_tensor_h2d(seq_start_loc, torch.int32,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
placeholder_index_maps = {
|
||||||
|
modality: placeholder_map.index_map()
|
||||||
|
for modality, placeholder_map in
|
||||||
|
self.multimodal_placeholder_maps.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._metadata_cls( # type: ignore
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
slot_mapping=slot_mapping_tensor,
|
||||||
|
multi_modal_placeholder_index_maps=placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=num_decode_tokens,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=max_query_len,
|
||||||
|
max_prefill_seq_len=max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=max_decode_seq_len,
|
||||||
|
query_start_loc=query_start_loc_tensor,
|
||||||
|
seq_start_loc=seq_start_loc_tensor,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=use_captured_graph,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CommonAttentionState(AttentionState):
|
||||||
|
|
||||||
|
def __init__(self, runner: "ModelRunnerBase"):
|
||||||
|
self.runner = runner
|
||||||
|
self._is_graph_capturing = False
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def graph_capture(self, max_batch_size: int):
|
||||||
|
|
||||||
|
self._is_graph_capturing = True
|
||||||
|
|
||||||
|
self._graph_slot_mapping = torch.full((max_batch_size, ),
|
||||||
|
PAD_SLOT_ID,
|
||||||
|
dtype=torch.long,
|
||||||
|
device=self.runner.device)
|
||||||
|
self._graph_seq_lens = torch.ones(max_batch_size,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=self.runner.device)
|
||||||
|
self._graph_block_tables = torch.from_numpy(
|
||||||
|
self.runner.graph_block_tables).to(device=self.runner.device)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
self._is_graph_capturing = False
|
||||||
|
del self._graph_slot_mapping
|
||||||
|
del self._graph_seq_lens
|
||||||
|
del self._graph_block_tables
|
||||||
|
|
||||||
|
def graph_clone(self, batch_size: int) -> "CommonAttentionState":
|
||||||
|
assert self._is_graph_capturing
|
||||||
|
return self.__class__(self.runner)
|
||||||
|
|
||||||
|
def graph_capture_get_metadata_for_batch(
|
||||||
|
self, batch_size: int, is_encoder_decoder_model: bool = False):
|
||||||
|
assert self._is_graph_capturing
|
||||||
|
attn_metadata = self.runner.attn_backend.make_metadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=batch_size,
|
||||||
|
slot_mapping=self._graph_slot_mapping[:batch_size],
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=self._graph_seq_lens[:batch_size],
|
||||||
|
max_query_len=1,
|
||||||
|
max_decode_query_len=1,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.runner.max_seq_len_to_capture,
|
||||||
|
query_start_loc=None,
|
||||||
|
seq_start_loc=None,
|
||||||
|
context_lens_tensor=None,
|
||||||
|
block_tables=self._graph_block_tables[:batch_size],
|
||||||
|
use_cuda_graph=True,
|
||||||
|
)
|
||||||
|
if is_encoder_decoder_model:
|
||||||
|
# The encoder decoder model works only with XFormers and
|
||||||
|
# Flash Attention backend. Assert the same.
|
||||||
|
assert self.runner.attn_backend.get_name() in\
|
||||||
|
["XFORMERS", "FLASH_ATTN"], \
|
||||||
|
f"Expected attn_backend name to be either 'XFORMERS' or " \
|
||||||
|
f"'FLASH_ATTN', but "\
|
||||||
|
f"got '{self.runner.attn_backend.get_name()}'"
|
||||||
|
self._update_captured_metadata_for_enc_dec_model(
|
||||||
|
batch_size=batch_size, attn_metadata=attn_metadata)
|
||||||
|
|
||||||
|
return attn_metadata
|
||||||
|
|
||||||
|
def get_graph_input_buffers(
|
||||||
|
self,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False) -> Dict[str, Any]:
|
||||||
|
input_buffers = {
|
||||||
|
"slot_mapping": attn_metadata.slot_mapping,
|
||||||
|
"seq_lens_tensor": attn_metadata.decode_metadata.seq_lens_tensor,
|
||||||
|
"block_tables": attn_metadata.decode_metadata.block_tables,
|
||||||
|
}
|
||||||
|
if is_encoder_decoder_model:
|
||||||
|
# The encoder decoder model works only with XFormers and
|
||||||
|
# Flash Attention backend. Assert the same.
|
||||||
|
assert self.runner.attn_backend.get_name() in\
|
||||||
|
["XFORMERS", "FLASH_ATTN"], \
|
||||||
|
f"Expected attn_backend name to be either 'XFORMERS' or "\
|
||||||
|
f"'FLASH_ATTN', but "\
|
||||||
|
f"got '{self.runner.attn_backend.get_name()}'"
|
||||||
|
self._add_additonal_input_buffers_for_enc_dec_model(
|
||||||
|
attn_metadata=attn_metadata, input_buffers=input_buffers)
|
||||||
|
return input_buffers
|
||||||
|
|
||||||
|
def prepare_graph_input_buffers(
|
||||||
|
self,
|
||||||
|
input_buffers,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False) -> None:
|
||||||
|
input_buffers["seq_lens_tensor"].copy_(
|
||||||
|
attn_metadata.decode_metadata.seq_lens_tensor, non_blocking=True)
|
||||||
|
input_buffers["block_tables"].copy_(
|
||||||
|
attn_metadata.decode_metadata.block_tables, non_blocking=True)
|
||||||
|
if is_encoder_decoder_model:
|
||||||
|
# The encoder decoder model works only with XFormers and
|
||||||
|
# Flash Attention backend. Assert the same.
|
||||||
|
assert self.runner.attn_backend.get_name() in\
|
||||||
|
["XFORMERS", "FLASH_ATTN"], \
|
||||||
|
f"Expected attn_backend name to be either 'XFORMERS' or "\
|
||||||
|
f"'FLASH_ATTN', but "\
|
||||||
|
f"got '{self.runner.attn_backend.get_name()}'"
|
||||||
|
self._prepare_input_buffers_for_enc_dec_model(
|
||||||
|
attn_metadata, input_buffers)
|
||||||
|
|
||||||
|
def begin_forward(self, model_input) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _update_captured_metadata_for_enc_dec_model(self, batch_size: int,
|
||||||
|
attn_metadata):
|
||||||
|
"""
|
||||||
|
Updates the attention metadata parameters for CUDA graph capture in an
|
||||||
|
encoder-decoder model.
|
||||||
|
|
||||||
|
This method modifies attention-related tensors and metadata required
|
||||||
|
for CUDA graph capture in encoder-decoder models. Specifically, it
|
||||||
|
updates the cross-attention and encoder sequence tensors in the
|
||||||
|
AttentionMetadata object.
|
||||||
|
"""
|
||||||
|
# During decode phase the cross_slot_mapping will be empty. Hence set
|
||||||
|
# an empty tensor for CUDA Graph capture.
|
||||||
|
attn_metadata.cross_slot_mapping = torch.tensor(
|
||||||
|
[], dtype=torch.int).cuda()
|
||||||
|
attn_metadata.cross_block_tables = torch.full(
|
||||||
|
(batch_size, self.runner.get_max_block_per_batch()),
|
||||||
|
1,
|
||||||
|
dtype=torch.int).cuda()
|
||||||
|
attn_metadata.encoder_seq_lens = torch.full((batch_size, ),
|
||||||
|
1,
|
||||||
|
dtype=torch.int).cuda()
|
||||||
|
attn_metadata.encoder_seq_lens_tensor = torch.full(
|
||||||
|
(batch_size, ), 1, dtype=torch.int).cuda()
|
||||||
|
attn_metadata.max_encoder_seq_len = self.runner.max_seq_len_to_capture
|
||||||
|
attn_metadata.num_encoder_tokens = 0
|
||||||
|
|
||||||
|
def _add_additonal_input_buffers_for_enc_dec_model(
|
||||||
|
self, attn_metadata, input_buffers: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Saves additional input buffers specific to the encoder-decoder model
|
||||||
|
from the attention metadata.
|
||||||
|
|
||||||
|
This method extracts and stores encoder-decoder related input buffers
|
||||||
|
from the `attn_metadata` into the `input_buffers` dictionary. The
|
||||||
|
buffers include encoder sequence lengths, cross-slot mappings, and
|
||||||
|
cross-block tables, which are essential for the encoder-decoder model
|
||||||
|
during CUDA graph replay.
|
||||||
|
"""
|
||||||
|
input_buffers["encoder_seq_lens_tensor"] = (
|
||||||
|
attn_metadata.decode_metadata.encoder_seq_lens_tensor)
|
||||||
|
input_buffers["cross_slot_mapping"] = (
|
||||||
|
attn_metadata.decode_metadata.cross_slot_mapping)
|
||||||
|
input_buffers["cross_block_tables"] = (
|
||||||
|
attn_metadata.decode_metadata.cross_block_tables)
|
||||||
|
|
||||||
|
def _prepare_input_buffers_for_enc_dec_model(self, attn_metadata,
|
||||||
|
input_buffers: Dict[str,
|
||||||
|
Any]):
|
||||||
|
"""
|
||||||
|
Populates input buffers with data from the encoder-decoder model's
|
||||||
|
attention metadata.
|
||||||
|
|
||||||
|
This method fills the input buffers with encoder-decoder specific
|
||||||
|
tensors. It copies data from the `attn_metadata` and keyword arguments
|
||||||
|
(`kwargs`) into corresponding buffers in the `input_buffers` dictionary.
|
||||||
|
The copied data includes attention-related metadata as well as input
|
||||||
|
IDs and positional information for the encoder.
|
||||||
|
"""
|
||||||
|
input_buffers["encoder_seq_lens_tensor"].copy_(
|
||||||
|
attn_metadata.decode_metadata.encoder_seq_lens_tensor,
|
||||||
|
non_blocking=True)
|
||||||
|
input_buffers["cross_slot_mapping"].copy_(
|
||||||
|
attn_metadata.decode_metadata.cross_slot_mapping,
|
||||||
|
non_blocking=True)
|
||||||
|
input_buffers["cross_block_tables"].copy_(
|
||||||
|
attn_metadata.decode_metadata.cross_block_tables,
|
||||||
|
non_blocking=True)
|
||||||
|
|
||||||
|
|
||||||
|
def is_all_encoder_attn_metadata_set(attn_metadata):
|
||||||
|
'''
|
||||||
|
All attention metadata required for encoder attention is set.
|
||||||
|
'''
|
||||||
|
return ((attn_metadata.encoder_seq_lens is not None)
|
||||||
|
and (attn_metadata.encoder_seq_lens_tensor is not None)
|
||||||
|
and (attn_metadata.max_encoder_seq_len is not None))
|
||||||
|
|
||||||
|
|
||||||
|
def is_all_cross_attn_metadata_set(attn_metadata):
|
||||||
|
'''
|
||||||
|
All attention metadata required for enc/dec cross-attention is set.
|
||||||
|
|
||||||
|
Superset of encoder attention required metadata.
|
||||||
|
'''
|
||||||
|
return (attn_metadata.is_all_encoder_attn_metadata_set
|
||||||
|
and (attn_metadata.cross_slot_mapping is not None)
|
||||||
|
and (attn_metadata.cross_block_tables is not None))
|
||||||
|
|
||||||
|
|
||||||
|
def get_seq_len_block_table_args(
|
||||||
|
attn_metadata,
|
||||||
|
is_prompt: bool,
|
||||||
|
attn_type: str,
|
||||||
|
) -> tuple:
|
||||||
|
'''
|
||||||
|
The particular choice of sequence-length- and block-table-related
|
||||||
|
attributes which should be extracted from attn_metadata is dependent
|
||||||
|
on the type of attention operation.
|
||||||
|
|
||||||
|
Decoder attn -> select entirely decoder self-attention-related fields
|
||||||
|
Encoder/decoder cross-attn -> select encoder sequence lengths &
|
||||||
|
cross-attn block-tables fields
|
||||||
|
Encoder attn -> select encoder sequence lengths fields & no block tables
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention op
|
||||||
|
* is_prompt: True if prefill, False otherwise
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* Appropriate sequence-lengths tensor
|
||||||
|
* Appropriate max sequence-length scalar
|
||||||
|
* Appropriate block tables (or None)
|
||||||
|
'''
|
||||||
|
|
||||||
|
if attn_type == AttentionType.DECODER:
|
||||||
|
# Decoder self-attention
|
||||||
|
# Choose max_seq_len based on whether we are in prompt_run
|
||||||
|
if is_prompt:
|
||||||
|
max_seq_len = attn_metadata.max_prefill_seq_len
|
||||||
|
else:
|
||||||
|
max_seq_len = attn_metadata.max_decode_seq_len
|
||||||
|
return (attn_metadata.seq_lens_tensor, max_seq_len,
|
||||||
|
attn_metadata.block_tables)
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# Enc/dec cross-attention KVs match encoder sequence length;
|
||||||
|
# cross-attention utilizes special "cross" block tables
|
||||||
|
return (attn_metadata.encoder_seq_lens_tensor,
|
||||||
|
attn_metadata.max_encoder_seq_len,
|
||||||
|
attn_metadata.cross_block_tables)
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
# No block tables associated with encoder attention
|
||||||
|
return (attn_metadata.encoder_seq_lens_tensor,
|
||||||
|
attn_metadata.max_encoder_seq_len, None)
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_num_prefill_decode_query_kv_tokens(
|
||||||
|
attn_metadata,
|
||||||
|
attn_type: str,
|
||||||
|
) -> Tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Calculate the number of prefill and decode tokens for query, key/value
|
||||||
|
based on the attention metadata and the specified attention type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
attn_metadata (FlashAttentionMetadata): Attention Metadata object.
|
||||||
|
attn_type (AttentionType): The type of attention being used.
|
||||||
|
Returns:
|
||||||
|
Tuple[int, int, int]: A tuple containing three integers:
|
||||||
|
- The number of prefill query tokens.
|
||||||
|
- The number of prefill key/value tokens.
|
||||||
|
- The number of decode query tokens.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If the number of encoder tokens in `attn_metadata`
|
||||||
|
is `None` when required for the calculations.
|
||||||
|
"""
|
||||||
|
num_prefill_query_tokens = 0
|
||||||
|
num_decode_query_tokens = 0
|
||||||
|
num_prefill_kv_tokens = 0
|
||||||
|
if attn_type == AttentionType.ENCODER:
|
||||||
|
# Encoder attention is only invoked during prefill phase.
|
||||||
|
# The same input servers a both query and key.
|
||||||
|
assert attn_metadata.num_encoder_tokens is not None
|
||||||
|
num_prefill_query_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
num_prefill_kv_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
num_decode_query_tokens = 0
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
assert attn_metadata.num_encoder_tokens is not None
|
||||||
|
num_prefill_query_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
# The key is the encoder/cross-attention.
|
||||||
|
num_prefill_kv_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
num_decode_query_tokens = attn_metadata.num_decode_tokens
|
||||||
|
else: # attn_type == AttentionType.DECODER or
|
||||||
|
# attn_type == AttentionType.ENCODER_ONLY
|
||||||
|
num_prefill_query_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_prefill_kv_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_decode_query_tokens = attn_metadata.num_decode_tokens
|
||||||
|
|
||||||
|
return (num_prefill_query_tokens, num_prefill_kv_tokens,
|
||||||
|
num_decode_query_tokens)
|
||||||
793
vllm/attention/backends/xformers.py
Normal file
793
vllm/attention/backends/xformers.py
Normal file
@@ -0,0 +1,793 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Attention layer with xFormers and PagedAttention."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from xformers import ops as xops
|
||||||
|
from xformers.ops.fmha.attn_bias import (AttentionBias,
|
||||||
|
BlockDiagonalCausalMask,
|
||||||
|
BlockDiagonalMask,
|
||||||
|
LowerTriangularMaskWithTensorBias)
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionLayer,
|
||||||
|
AttentionMetadata, AttentionType)
|
||||||
|
from vllm.attention.backends.utils import (
|
||||||
|
CommonAttentionState, CommonMetadataBuilder,
|
||||||
|
get_num_prefill_decode_query_kv_tokens, get_seq_len_block_table_args,
|
||||||
|
is_all_cross_attn_metadata_set, is_all_encoder_attn_metadata_set)
|
||||||
|
from vllm.attention.ops.paged_attn import (PagedAttention,
|
||||||
|
PagedAttentionMetadata)
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class XFormersBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "XFORMERS"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["XFormersImpl"]:
|
||||||
|
return XFormersImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return XFormersMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["XFormersMetadataBuilder"]:
|
||||||
|
return XFormersMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["CommonAttentionState"]:
|
||||||
|
return CommonAttentionState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return PagedAttention.get_kv_cache_shape(num_blocks, block_size,
|
||||||
|
num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: Dict[int, int],
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.swap_blocks(src_kv_cache, dst_kv_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
PagedAttention.copy_blocks(kv_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class XFormersMetadata(AttentionMetadata, PagedAttentionMetadata):
|
||||||
|
"""Metadata for XFormersbackend.
|
||||||
|
|
||||||
|
NOTE: Any python object stored here is not updated when it is
|
||||||
|
cuda-graph replayed. If you have values that need to be changed
|
||||||
|
dynamically, it should be stored in tensor. The tensor has to be
|
||||||
|
updated from `CUDAGraphRunner.forward` API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# |---------- N-1 iteration --------|
|
||||||
|
# |---------------- N iteration ---------------------|
|
||||||
|
# |- tokenA -|......................|-- newTokens ---|
|
||||||
|
# |---------- context_len ----------|
|
||||||
|
# |-------------------- seq_len ----------------------|
|
||||||
|
# |-- query_len ---|
|
||||||
|
|
||||||
|
# seq_lens stored as a tensor.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
# FIXME: It is for flash attn.
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
# Maximum sequence length among decode batch. 0 if there are prefill
|
||||||
|
# requests only.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
|
||||||
|
# Whether or not if cuda graph is enabled.
|
||||||
|
# Cuda-graph is currently enabled for decoding only.
|
||||||
|
# TODO(woosuk): Move `use_cuda_graph` out since it's unrelated to attention.
|
||||||
|
use_cuda_graph: bool
|
||||||
|
|
||||||
|
# (batch_size,). The sequence length per sequence. Sequence length means
|
||||||
|
# the computed tokens + new tokens None if it is a decoding.
|
||||||
|
seq_lens: Optional[List[int]] = None
|
||||||
|
|
||||||
|
# FIXME: It is for flash attn.
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# (batch_size,) A tensor of context lengths (tokens that are computed
|
||||||
|
# so far).
|
||||||
|
context_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Maximum query length in the batch. None for decoding.
|
||||||
|
max_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
# Max number of query tokens among request in the batch.
|
||||||
|
max_decode_query_len: Optional[int] = None
|
||||||
|
|
||||||
|
# (batch_size + 1,). The cumulative subquery lengths of the sequences in
|
||||||
|
# the batch, used to index into subquery. E.g., if the subquery length
|
||||||
|
# is [4, 6], it is [0, 4, 10].
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Self-attention prefill/decode metadata cache
|
||||||
|
_cached_prefill_metadata: Optional["XFormersMetadata"] = None
|
||||||
|
_cached_decode_metadata: Optional["XFormersMetadata"] = None
|
||||||
|
|
||||||
|
# Begin encoder attn & enc/dec cross-attn fields...
|
||||||
|
|
||||||
|
# Encoder sequence lengths representation
|
||||||
|
encoder_seq_lens: Optional[List[int]] = None
|
||||||
|
encoder_seq_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
# FIXME: It is for flash attn.
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence. E.g., if the sequence length is
|
||||||
|
# [4, 6], it is [0, 4, 10].
|
||||||
|
encoder_seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Maximum sequence length among encoder sequences
|
||||||
|
max_encoder_seq_len: Optional[int] = None
|
||||||
|
|
||||||
|
# Number of tokens input to encoder
|
||||||
|
num_encoder_tokens: Optional[int] = None
|
||||||
|
|
||||||
|
# Cross-attention memory-mapping data structures: slot mapping
|
||||||
|
# and block tables
|
||||||
|
cross_slot_mapping: Optional[torch.Tensor] = None
|
||||||
|
cross_block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# Set during the execution of the first attention op.
|
||||||
|
# It is a list because it is needed to set per prompt
|
||||||
|
# when alibi slopes is used. It is because of the limitation
|
||||||
|
# from xformer API.
|
||||||
|
# will not appear in the __repr__ and __init__
|
||||||
|
self.attn_bias: Optional[List[AttentionBias]] = None
|
||||||
|
self.encoder_attn_bias: Optional[List[AttentionBias]] = None
|
||||||
|
self.cross_attn_bias: Optional[List[AttentionBias]] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_encoder_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for encoder attention is set.
|
||||||
|
'''
|
||||||
|
return is_all_encoder_attn_metadata_set(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_all_cross_attn_metadata_set(self):
|
||||||
|
'''
|
||||||
|
All attention metadata required for enc/dec cross-attention is set.
|
||||||
|
|
||||||
|
Superset of encoder attention required metadata.
|
||||||
|
'''
|
||||||
|
return is_all_cross_attn_metadata_set(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["XFormersMetadata"]:
|
||||||
|
if self.num_prefills == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_prefill_metadata is not None:
|
||||||
|
# Recover cached prefill-phase attention
|
||||||
|
# metadata structure
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
assert ((self.seq_lens is not None)
|
||||||
|
or (self.encoder_seq_lens is not None))
|
||||||
|
assert ((self.seq_lens_tensor is not None)
|
||||||
|
or (self.encoder_seq_lens_tensor is not None))
|
||||||
|
|
||||||
|
# Compute some attn_metadata fields which default to None
|
||||||
|
query_start_loc = (None if self.query_start_loc is None else
|
||||||
|
self.query_start_loc[:self.num_prefills + 1])
|
||||||
|
seq_start_loc = (None if self.seq_start_loc is None else
|
||||||
|
self.seq_start_loc[:self.num_prefills + 1])
|
||||||
|
slot_mapping = (None if self.slot_mapping is None else
|
||||||
|
self.slot_mapping[:self.num_prefill_tokens])
|
||||||
|
seq_lens = (None if self.seq_lens is None else
|
||||||
|
self.seq_lens[:self.num_prefills])
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[:self.num_prefills])
|
||||||
|
context_lens_tensor = (None if self.context_lens_tensor is None else
|
||||||
|
self.context_lens_tensor[:self.num_prefills])
|
||||||
|
block_tables = (None if self.block_tables is None else
|
||||||
|
self.block_tables[:self.num_prefills])
|
||||||
|
|
||||||
|
# Construct & cache prefill-phase attention metadata structure
|
||||||
|
self._cached_prefill_metadata = XFormersMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=0,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=self.
|
||||||
|
multi_modal_placeholder_index_maps,
|
||||||
|
enable_kv_scales_calculation=self.enable_kv_scales_calculation,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_query_len=self.max_query_len,
|
||||||
|
max_prefill_seq_len=self.max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=0,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
seq_start_loc=seq_start_loc,
|
||||||
|
context_lens_tensor=context_lens_tensor,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=False,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
return self._cached_prefill_metadata
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decode_metadata(self) -> Optional["XFormersMetadata"]:
|
||||||
|
if self.num_decode_tokens == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self._cached_decode_metadata is not None:
|
||||||
|
# Recover cached decode-phase attention
|
||||||
|
# metadata structure
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
assert ((self.seq_lens_tensor is not None)
|
||||||
|
or (self.encoder_seq_lens_tensor is not None))
|
||||||
|
|
||||||
|
# Compute some attn_metadata fields which default to None
|
||||||
|
slot_mapping = (None if self.slot_mapping is None else
|
||||||
|
self.slot_mapping[self.num_prefill_tokens:])
|
||||||
|
seq_lens_tensor = (None if self.seq_lens_tensor is None else
|
||||||
|
self.seq_lens_tensor[self.num_prefills:])
|
||||||
|
block_tables = (None if self.block_tables is None else
|
||||||
|
self.block_tables[self.num_prefills:])
|
||||||
|
|
||||||
|
# Construct & cache decode-phase attention metadata structure
|
||||||
|
self._cached_decode_metadata = XFormersMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=slot_mapping,
|
||||||
|
multi_modal_placeholder_index_maps=None,
|
||||||
|
enable_kv_scales_calculation=True,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
block_tables=block_tables,
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
max_encoder_seq_len=self.max_encoder_seq_len,
|
||||||
|
cross_slot_mapping=self.cross_slot_mapping,
|
||||||
|
cross_block_tables=self.cross_block_tables)
|
||||||
|
|
||||||
|
# Batch may be composed of prefill|decodes, adjust query start indices
|
||||||
|
# to refer to the start of decodes when the two are split apart.
|
||||||
|
# E.g. in tokens:[3 prefills|6 decodes], query_start_loc=[3,9] => [0,6].
|
||||||
|
if self._cached_decode_metadata.query_start_loc is not None:
|
||||||
|
qs = self._cached_decode_metadata.query_start_loc
|
||||||
|
self._cached_decode_metadata.query_start_loc = qs - qs[0]
|
||||||
|
return self._cached_decode_metadata
|
||||||
|
|
||||||
|
|
||||||
|
def _get_attn_bias(
|
||||||
|
attn_metadata: XFormersMetadata,
|
||||||
|
attn_type: str,
|
||||||
|
) -> Optional[AttentionBias]:
|
||||||
|
'''
|
||||||
|
Extract appropriate attention bias from attention metadata
|
||||||
|
according to attention type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* Appropriate attention bias value given the attention type
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
return attn_metadata.attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
return attn_metadata.encoder_attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
return attn_metadata.cross_attn_bias
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _set_attn_bias(
|
||||||
|
attn_metadata: XFormersMetadata,
|
||||||
|
attn_bias: List[Optional[AttentionBias]],
|
||||||
|
attn_type: str,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Update appropriate attention bias field of attention metadata,
|
||||||
|
according to attention type.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_metadata: Attention metadata structure associated with attention
|
||||||
|
* attn_bias: The desired attention bias value
|
||||||
|
* attn_type: encoder attention, decoder self-attention,
|
||||||
|
encoder/decoder cross-attention
|
||||||
|
'''
|
||||||
|
|
||||||
|
if (attn_type == AttentionType.DECODER
|
||||||
|
or attn_type == AttentionType.ENCODER_ONLY):
|
||||||
|
attn_metadata.attn_bias = attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
attn_metadata.encoder_attn_bias = attn_bias
|
||||||
|
elif attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
attn_metadata.cross_attn_bias = attn_bias
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"Invalid attention type {str(attn_type)}")
|
||||||
|
|
||||||
|
|
||||||
|
class XFormersMetadataBuilder(CommonMetadataBuilder[XFormersMetadata]):
|
||||||
|
|
||||||
|
_metadata_cls = XFormersMetadata
|
||||||
|
|
||||||
|
|
||||||
|
class XFormersImpl(AttentionImpl[XFormersMetadata]):
|
||||||
|
"""
|
||||||
|
If the input tensors contain prompt tokens, the layout is as follows:
|
||||||
|
|<--------------- num_prefill_tokens ----------------->|
|
||||||
|
|<--prefill_0-->|<--prefill_1-->|...|<--prefill_N-1--->|
|
||||||
|
|
||||||
|
Otherwise, the layout is as follows:
|
||||||
|
|<----------------- num_decode_tokens ------------------>|
|
||||||
|
|<--decode_0-->|..........|<--decode_M-1-->|<--padding-->|
|
||||||
|
|
||||||
|
Generation tokens can contain padding when cuda-graph is used.
|
||||||
|
Currently, prompt tokens don't contain any padding.
|
||||||
|
|
||||||
|
The prompts might have different lengths, while the generation tokens
|
||||||
|
always have length 1.
|
||||||
|
|
||||||
|
If chunked prefill is enabled, prefill tokens and decode tokens can be
|
||||||
|
batched together in a flattened 1D query.
|
||||||
|
|
||||||
|
|<----- num_prefill_tokens ---->|<------- num_decode_tokens --------->|
|
||||||
|
|<-prefill_0->|...|<-prefill_N-1->|<--decode_0-->|...|<--decode_M-1-->|
|
||||||
|
|
||||||
|
Currently, cuda graph is disabled for chunked prefill, meaning there's no
|
||||||
|
padding between prefill and decode tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: int,
|
||||||
|
alibi_slopes: Optional[List[float]],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"XFormers does not support block-sparse attention.")
|
||||||
|
if logits_soft_cap is not None:
|
||||||
|
logger.warning_once("XFormers does not support logits soft cap. "
|
||||||
|
"Outputs may be slightly off.")
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = float(scale)
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.alibi_slopes = alibi_slopes
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
suppored_head_sizes = PagedAttention.get_supported_head_sizes()
|
||||||
|
if head_size not in suppored_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Head size {head_size} is not supported by PagedAttention. "
|
||||||
|
f"Supported head sizes are: {suppored_head_sizes}.")
|
||||||
|
|
||||||
|
self.attn_type = attn_type
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
layer: AttentionLayer,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: Optional[torch.Tensor],
|
||||||
|
value: Optional[torch.Tensor],
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: "XFormersMetadata",
|
||||||
|
output: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Forward pass with xFormers and PagedAttention.
|
||||||
|
|
||||||
|
For decoder-only models: query, key and value must be non-None.
|
||||||
|
|
||||||
|
For encoder/decoder models:
|
||||||
|
* XFormersImpl.forward() may be invoked for both self- and cross-
|
||||||
|
attention layers.
|
||||||
|
* For self-attention: query, key and value must be non-None.
|
||||||
|
* For cross-attention:
|
||||||
|
* Query must be non-None
|
||||||
|
* During prefill, key and value must be non-None; key and value
|
||||||
|
get cached for use during decode.
|
||||||
|
* During decode, key and value may be None, since:
|
||||||
|
(1) key and value tensors were cached during prefill, and
|
||||||
|
(2) cross-attention key and value tensors do not grow during
|
||||||
|
decode
|
||||||
|
|
||||||
|
A note on how the attn_type (attention type enum) argument impacts
|
||||||
|
attention forward() behavior:
|
||||||
|
|
||||||
|
* DECODER: normal decoder-only behavior;
|
||||||
|
use decoder self-attention block table
|
||||||
|
* ENCODER: no KV caching; pass encoder sequence
|
||||||
|
attributes (encoder_seq_lens/encoder_seq_lens_tensor/
|
||||||
|
max_encoder_seq_len) to kernel, in lieu of decoder
|
||||||
|
sequence attributes (seq_lens/seq_lens_tensor/max_seq_len).
|
||||||
|
Used for encoder branch of encoder-decoder models.
|
||||||
|
* ENCODER_ONLY: no kv_caching, uses the normal attention
|
||||||
|
attributes (seq_lens/seq_lens_tensor/max_seq_len).
|
||||||
|
* ENCODER_DECODER: cross-attention behavior;
|
||||||
|
use cross-attention block table for caching KVs derived
|
||||||
|
from encoder hidden states; since KV sequence lengths
|
||||||
|
will match encoder sequence lengths, pass encoder sequence
|
||||||
|
attributes to kernel (encoder_seq_lens/encoder_seq_lens_tensor/
|
||||||
|
max_encoder_seq_len)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: shape = [num_tokens, num_heads * head_size]
|
||||||
|
key: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
value: shape = [num_tokens, num_kv_heads * head_size]
|
||||||
|
kv_cache = [2, num_blocks, block_size * num_kv_heads * head_size]
|
||||||
|
NOTE: kv_cache will be an empty tensor with shape [0]
|
||||||
|
for profiling run.
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
attn_type: Select attention type, between encoder attention,
|
||||||
|
decoder self-attention, or encoder/decoder cross-
|
||||||
|
attention. Defaults to decoder self-attention,
|
||||||
|
which is the vLLM default generally
|
||||||
|
Returns:
|
||||||
|
shape = [num_tokens, num_heads * head_size]
|
||||||
|
"""
|
||||||
|
attn_type = self.attn_type
|
||||||
|
# Check that appropriate attention metadata attributes are
|
||||||
|
# selected for the desired attention type
|
||||||
|
if (attn_type == AttentionType.ENCODER
|
||||||
|
and (not attn_metadata.is_all_encoder_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder attention requires setting "
|
||||||
|
"encoder metadata attributes.")
|
||||||
|
|
||||||
|
elif (attn_type == AttentionType.ENCODER_DECODER
|
||||||
|
and (not attn_metadata.is_all_cross_attn_metadata_set)):
|
||||||
|
raise AttributeError("Encoder/decoder cross-attention "
|
||||||
|
"requires setting cross-attention "
|
||||||
|
"metadata attributes.")
|
||||||
|
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
if key is not None:
|
||||||
|
assert value is not None
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
else:
|
||||||
|
assert value is None
|
||||||
|
|
||||||
|
# Self-attention vs. cross-attention will impact
|
||||||
|
# which KV cache memory-mapping & which
|
||||||
|
# seqlen datastructures we utilize
|
||||||
|
|
||||||
|
if (attn_type != AttentionType.ENCODER and kv_cache.numel() > 0):
|
||||||
|
# KV-cache during decoder-self- or
|
||||||
|
# encoder-decoder-cross-attention, but not
|
||||||
|
# during encoder attention.
|
||||||
|
#
|
||||||
|
# Even if there are no new key/value pairs to cache,
|
||||||
|
# we still need to break out key_cache and value_cache
|
||||||
|
# i.e. for later use by paged attention
|
||||||
|
key_cache, value_cache = PagedAttention.split_kv_cache(
|
||||||
|
kv_cache, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
if (key is not None) and (value is not None):
|
||||||
|
|
||||||
|
if attn_type == AttentionType.ENCODER_DECODER:
|
||||||
|
# Update cross-attention KV cache (prefill-only)
|
||||||
|
# During cross-attention decode, key & value will be None,
|
||||||
|
# preventing this IF-statement branch from running
|
||||||
|
updated_slot_mapping = attn_metadata.cross_slot_mapping
|
||||||
|
else:
|
||||||
|
# Update self-attention KV cache (prefill/decode)
|
||||||
|
updated_slot_mapping = attn_metadata.slot_mapping
|
||||||
|
|
||||||
|
# Reshape the input keys and values and store them in the cache.
|
||||||
|
# If kv_cache is not provided, the new key and value tensors are
|
||||||
|
# not cached. This happens during the initial memory
|
||||||
|
# profiling run.
|
||||||
|
PagedAttention.write_to_paged_cache(
|
||||||
|
key, value, key_cache, value_cache, updated_slot_mapping,
|
||||||
|
self.kv_cache_dtype, layer._k_scale, layer._v_scale)
|
||||||
|
(num_prefill_query_tokens, num_prefill_kv_tokens,
|
||||||
|
num_decode_query_tokens) = \
|
||||||
|
get_num_prefill_decode_query_kv_tokens(attn_metadata, attn_type)
|
||||||
|
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
# Query for decode. KV is not needed because it is already cached.
|
||||||
|
decode_query = query[num_prefill_query_tokens:]
|
||||||
|
# QKV for prefill.
|
||||||
|
query = query[:num_prefill_query_tokens]
|
||||||
|
if key is not None and value is not None:
|
||||||
|
key = key[:num_prefill_kv_tokens]
|
||||||
|
value = value[:num_prefill_kv_tokens]
|
||||||
|
|
||||||
|
assert query.shape[0] == num_prefill_query_tokens
|
||||||
|
assert decode_query.shape[0] == num_decode_query_tokens
|
||||||
|
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
# Prompt run.
|
||||||
|
if kv_cache.numel() == 0 or prefill_meta.block_tables.numel() == 0:
|
||||||
|
# normal attention.
|
||||||
|
# block tables are empty if the prompt does not have a cached
|
||||||
|
# prefix.
|
||||||
|
out = self._run_memory_efficient_xformers_forward(
|
||||||
|
query, key, value, prefill_meta, attn_type=attn_type)
|
||||||
|
assert out.shape == output[:num_prefill_query_tokens].shape
|
||||||
|
output[:num_prefill_query_tokens] = out
|
||||||
|
else:
|
||||||
|
assert attn_type != AttentionType.ENCODER_ONLY, (
|
||||||
|
"Encoder-only models should not have prefix attention.")
|
||||||
|
|
||||||
|
assert prefill_meta.query_start_loc is not None
|
||||||
|
assert prefill_meta.max_query_len is not None
|
||||||
|
|
||||||
|
# prefix-enabled attention
|
||||||
|
# TODO(Hai) this triton kernel has regression issue (broke) to
|
||||||
|
# deal with different data types between KV and FP8 KV cache,
|
||||||
|
# to be addressed separately.
|
||||||
|
out = PagedAttention.forward_prefix(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
prefill_meta.block_tables,
|
||||||
|
prefill_meta.query_start_loc,
|
||||||
|
prefill_meta.seq_lens_tensor,
|
||||||
|
prefill_meta.max_query_len,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.sliding_window,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
assert output[:num_prefill_query_tokens].shape == out.shape
|
||||||
|
output[:num_prefill_query_tokens] = out
|
||||||
|
|
||||||
|
if decode_meta := attn_metadata.decode_metadata:
|
||||||
|
assert attn_type != AttentionType.ENCODER_ONLY, (
|
||||||
|
"Encoder-only models should not have decode metadata.")
|
||||||
|
|
||||||
|
(
|
||||||
|
seq_lens_arg,
|
||||||
|
max_seq_len_arg,
|
||||||
|
block_tables_arg,
|
||||||
|
) = get_seq_len_block_table_args(decode_meta, False, attn_type)
|
||||||
|
|
||||||
|
output[num_prefill_query_tokens:] = PagedAttention.forward_decode(
|
||||||
|
decode_query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
block_tables_arg,
|
||||||
|
seq_lens_arg,
|
||||||
|
max_seq_len_arg,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
layer._k_scale,
|
||||||
|
layer._v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(-1, self.num_heads * self.head_size)
|
||||||
|
|
||||||
|
def _run_memory_efficient_xformers_forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: XFormersMetadata,
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Attention for 1D query of multiple prompts. Multiple prompt
|
||||||
|
tokens are flattened in to `query` input.
|
||||||
|
|
||||||
|
See https://facebookresearch.github.io/xformers/components/ops.html
|
||||||
|
for API spec.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output: shape = [num_prefill_tokens, num_heads, head_size]
|
||||||
|
query: shape = [num_prefill_tokens, num_heads, head_size]
|
||||||
|
key: shape = [num_prefill_tokens, num_kv_heads, head_size]
|
||||||
|
value: shape = [num_prefill_tokens, num_kv_heads, head_size]
|
||||||
|
attn_metadata: Metadata for attention.
|
||||||
|
attn_type: Select attention type, between encoder attention,
|
||||||
|
decoder self-attention, or encoder/decoder cross-
|
||||||
|
attention. Defaults to decoder self-attention,
|
||||||
|
which is the vLLM default generally
|
||||||
|
"""
|
||||||
|
|
||||||
|
original_query = query
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
# GQA/MQA requires the shape [B, M, G, H, K].
|
||||||
|
# Note that the output also has the same shape (which is different
|
||||||
|
# from a spec from the doc).
|
||||||
|
query = query.view(query.shape[0], self.num_kv_heads,
|
||||||
|
self.num_queries_per_kv, query.shape[-1])
|
||||||
|
key = key[:, :,
|
||||||
|
None, :].expand(key.shape[0], self.num_kv_heads,
|
||||||
|
self.num_queries_per_kv, key.shape[-1])
|
||||||
|
value = value[:, :,
|
||||||
|
None, :].expand(value.shape[0], self.num_kv_heads,
|
||||||
|
self.num_queries_per_kv,
|
||||||
|
value.shape[-1])
|
||||||
|
|
||||||
|
# Set attention bias if not provided. This typically happens at
|
||||||
|
# the very attention layer of every iteration.
|
||||||
|
# FIXME(woosuk): This is a hack.
|
||||||
|
attn_bias = _get_attn_bias(attn_metadata, attn_type)
|
||||||
|
if attn_bias is None:
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
|
||||||
|
# Cross attention block of decoder branch of encoder-decoder
|
||||||
|
# model uses seq_lens for dec / encoder_seq_lens for enc
|
||||||
|
if (attn_type == AttentionType.ENCODER_DECODER):
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
assert attn_metadata.encoder_seq_lens is not None
|
||||||
|
|
||||||
|
# Cross-attention mask is non-causal
|
||||||
|
attn_bias = BlockDiagonalMask.from_seqlens(
|
||||||
|
attn_metadata.seq_lens,
|
||||||
|
attn_metadata.encoder_seq_lens,
|
||||||
|
device=query.device)
|
||||||
|
|
||||||
|
# Encoder branch of encoder-decoder model uses
|
||||||
|
# attn_metadata.encoder_seq_lens
|
||||||
|
elif attn_type == AttentionType.ENCODER:
|
||||||
|
|
||||||
|
assert attn_metadata.encoder_seq_lens is not None
|
||||||
|
|
||||||
|
# Encoder self-attention mask is non-causal
|
||||||
|
attn_bias = BlockDiagonalMask.from_seqlens(
|
||||||
|
attn_metadata.encoder_seq_lens, device=query.device)
|
||||||
|
|
||||||
|
# Self-attention block of encoder-only model just
|
||||||
|
# uses the seq_lens directly.
|
||||||
|
elif attn_type == AttentionType.ENCODER_ONLY:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
|
||||||
|
# Encoder self-attention mask is non-causal
|
||||||
|
attn_bias = BlockDiagonalMask.from_seqlens(
|
||||||
|
attn_metadata.seq_lens, device=query.device)
|
||||||
|
|
||||||
|
# Self-attention block of decoder branch just
|
||||||
|
# uses the seq_lens directly
|
||||||
|
elif attn_type == AttentionType.DECODER:
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
|
||||||
|
# Decoder self-attention mask is causal
|
||||||
|
attn_bias = BlockDiagonalCausalMask.from_seqlens(
|
||||||
|
attn_metadata.seq_lens, device=query.device)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown AttentionType: %s", attn_type)
|
||||||
|
|
||||||
|
if self.sliding_window is not None:
|
||||||
|
attn_bias = attn_bias.make_local_attention(
|
||||||
|
self.sliding_window)
|
||||||
|
attn_bias = [attn_bias]
|
||||||
|
else:
|
||||||
|
assert attn_type == AttentionType.DECODER
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
attn_bias = _make_alibi_bias(self.alibi_slopes,
|
||||||
|
self.num_kv_heads, query.dtype,
|
||||||
|
attn_metadata.seq_lens)
|
||||||
|
|
||||||
|
_set_attn_bias(attn_metadata, attn_bias, attn_type)
|
||||||
|
|
||||||
|
# No alibi slopes.
|
||||||
|
# TODO(woosuk): Too many view operations. Let's try to reduce
|
||||||
|
# them in the future for code readability.
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
# Add the batch dimension.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=attn_bias[0],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale)
|
||||||
|
return out.view_as(original_query)
|
||||||
|
|
||||||
|
# Attention with alibi slopes.
|
||||||
|
# FIXME(woosuk): Because xformers does not support dynamic sequence
|
||||||
|
# lengths with custom attention bias, we process each prompt one by
|
||||||
|
# one. This is inefficient, especially when we have many short prompts.
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
output = torch.empty_like(original_query)
|
||||||
|
start = 0
|
||||||
|
for i, seq_len in enumerate(attn_metadata.seq_lens):
|
||||||
|
end = start + seq_len
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query[None, start:end],
|
||||||
|
key[None, start:end],
|
||||||
|
value[None, start:end],
|
||||||
|
attn_bias=attn_bias[i],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale)
|
||||||
|
# TODO(woosuk): Unnecessary copy. Optimize.
|
||||||
|
output[start:end].copy_(out.view_as(original_query[start:end]))
|
||||||
|
start += seq_len
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def _make_alibi_bias(
|
||||||
|
alibi_slopes: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
seq_lens: List[int],
|
||||||
|
) -> List[AttentionBias]:
|
||||||
|
attn_biases: List[AttentionBias] = []
|
||||||
|
for seq_len in seq_lens:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(seq_len, 1)`
|
||||||
|
# here. We find that both biases give the same results, but
|
||||||
|
# the bias below more accurately follows the original ALiBi
|
||||||
|
# paper.
|
||||||
|
# Calculate a matrix where each element represents ith element- jth
|
||||||
|
# element.
|
||||||
|
bias = bias[None, :] - bias[:, None]
|
||||||
|
|
||||||
|
padded_len = (seq_len + 7) // 8 * 8
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = torch.empty(
|
||||||
|
1, # batch size
|
||||||
|
num_heads,
|
||||||
|
seq_len,
|
||||||
|
padded_len,
|
||||||
|
device=alibi_slopes.device,
|
||||||
|
dtype=dtype,
|
||||||
|
)[:, :, :, :seq_len].copy_(bias)
|
||||||
|
bias.mul_(alibi_slopes[:, None, None])
|
||||||
|
attn_biases.append(LowerTriangularMaskWithTensorBias(bias))
|
||||||
|
|
||||||
|
return attn_biases
|
||||||
425
vllm/attention/layer.py
Normal file
425
vllm/attention/layer.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Attention layer."""
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.attention import AttentionType
|
||||||
|
from vllm.attention.selector import backend_name_to_enum, get_attn_backend
|
||||||
|
from vllm.config import CacheConfig, get_current_vllm_config
|
||||||
|
from vllm.forward_context import ForwardContext, get_forward_context
|
||||||
|
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
|
||||||
|
from vllm.model_executor.layers.quantization.base_config import (
|
||||||
|
QuantizationConfig)
|
||||||
|
from vllm.model_executor.layers.quantization.kv_cache import BaseKVCacheMethod
|
||||||
|
from vllm.platforms import _Backend, current_platform
|
||||||
|
from vllm.utils import direct_register_custom_op
|
||||||
|
|
||||||
|
import ixformer.contrib.vllm_flash_attn as ops
|
||||||
|
|
||||||
|
|
||||||
|
class Attention(nn.Module):
|
||||||
|
"""Attention layer.
|
||||||
|
|
||||||
|
This class takes query, key, and value tensors as input. The input tensors
|
||||||
|
can either contain prompt tokens or generation tokens.
|
||||||
|
The class does the following:
|
||||||
|
|
||||||
|
1. Store the input key and value tensors in the KV cache.
|
||||||
|
2. Perform (multi-head/multi-query/grouped-query) attention.
|
||||||
|
3. Return the output tensor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: Optional[int] = None,
|
||||||
|
alibi_slopes: Optional[List[float]] = None,
|
||||||
|
cache_config: Optional[CacheConfig] = None,
|
||||||
|
quant_config: Optional[QuantizationConfig] = None,
|
||||||
|
blocksparse_params: Optional[Dict[str, Any]] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
per_layer_sliding_window: Optional[int] = None,
|
||||||
|
use_mla: bool = False,
|
||||||
|
prefix: str = "",
|
||||||
|
attn_type: str = AttentionType.DECODER,
|
||||||
|
**extra_impl_args,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
The KV cache is stored inside this class and is accessed via
|
||||||
|
`self.kv_cache`.
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
if per_layer_sliding_window is not None:
|
||||||
|
# per-layer sliding window
|
||||||
|
sliding_window = per_layer_sliding_window
|
||||||
|
elif cache_config is not None:
|
||||||
|
# model-level sliding window
|
||||||
|
sliding_window = cache_config.sliding_window
|
||||||
|
else:
|
||||||
|
sliding_window = None
|
||||||
|
|
||||||
|
if cache_config is not None:
|
||||||
|
kv_cache_dtype = cache_config.cache_dtype
|
||||||
|
block_size = cache_config.block_size
|
||||||
|
is_attention_free = cache_config.is_attention_free
|
||||||
|
calculate_kv_scales = cache_config.calculate_kv_scales
|
||||||
|
else:
|
||||||
|
kv_cache_dtype = "auto"
|
||||||
|
block_size = 16
|
||||||
|
is_attention_free = False
|
||||||
|
calculate_kv_scales = False
|
||||||
|
if num_kv_heads is None:
|
||||||
|
num_kv_heads = num_heads
|
||||||
|
|
||||||
|
# The default k/v_scale is set to 1.0. This is ignored
|
||||||
|
# when kv-cache is not fp8, and should be used with
|
||||||
|
# kv-cache in fp8_e5m2. For kv-cache in fp8_e4m3, we
|
||||||
|
# expect the pre-quantized k/v_scale to be loaded along
|
||||||
|
# with the model weights.
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
self.calculate_kv_scales = calculate_kv_scales
|
||||||
|
self._k_scale = torch.tensor(1.0, dtype=torch.float32)
|
||||||
|
self._v_scale = torch.tensor(1.0, dtype=torch.float32)
|
||||||
|
# FlashAttn doesn't support quantizing the kv-cache only
|
||||||
|
# but requires q to be quantized as well.
|
||||||
|
self._q_scale = torch.tensor(1.0, dtype=torch.float32)
|
||||||
|
|
||||||
|
# We also keep the float32 versions of k/v_scale for attention
|
||||||
|
# backends that don't support tensors (Flashinfer)
|
||||||
|
self._k_scale_float = 1.0
|
||||||
|
self._v_scale_float = 1.0
|
||||||
|
|
||||||
|
self.use_mla = use_mla
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.num_kv_heads = num_kv_heads
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
|
||||||
|
quant_method = quant_config.get_quant_method(
|
||||||
|
self, prefix=prefix) if quant_config else None
|
||||||
|
if quant_method is not None and not isinstance(
|
||||||
|
quant_method, UnquantizedLinearMethod):
|
||||||
|
assert isinstance(quant_method, BaseKVCacheMethod)
|
||||||
|
# TODO (mgoin): kv cache dtype should be specified in the FP8
|
||||||
|
# checkpoint config and become the "auto" behavior
|
||||||
|
if self.kv_cache_dtype == "fp8_e5m2":
|
||||||
|
raise ValueError("fp8_e5m2 kv-cache is not supported with "
|
||||||
|
"fp8 checkpoints.")
|
||||||
|
# If quantization is enabled, we make "k_scale" and "v_scale"
|
||||||
|
# parameters so that it can be loaded from the model checkpoint.
|
||||||
|
# The k/v_scale will then be converted back to native float32
|
||||||
|
# values after weight loading.
|
||||||
|
self.quant_method = quant_method
|
||||||
|
self.quant_method.create_weights(self)
|
||||||
|
|
||||||
|
# During model initialization, the default dtype is set as the model
|
||||||
|
# weight and activation dtype.
|
||||||
|
dtype = torch.get_default_dtype()
|
||||||
|
attn_backend = get_attn_backend(head_size,
|
||||||
|
dtype,
|
||||||
|
kv_cache_dtype,
|
||||||
|
block_size,
|
||||||
|
is_attention_free,
|
||||||
|
blocksparse_params is not None,
|
||||||
|
use_mla=use_mla)
|
||||||
|
impl_cls = attn_backend.get_impl_cls()
|
||||||
|
self.impl = impl_cls(num_heads, head_size, scale, num_kv_heads,
|
||||||
|
alibi_slopes, sliding_window, kv_cache_dtype,
|
||||||
|
blocksparse_params, logits_soft_cap, attn_type,
|
||||||
|
**extra_impl_args)
|
||||||
|
self.backend = backend_name_to_enum(attn_backend.get_name())
|
||||||
|
self.dtype = dtype
|
||||||
|
|
||||||
|
# For cuda-alike (CUDA and ROCM) and cpu platforms, we control how
|
||||||
|
# torch.compile works by registering the attention as one giant
|
||||||
|
# opaque custom op. For other platforms, we directly call them
|
||||||
|
# and let torch.compile handle them.
|
||||||
|
self.use_direct_call = True
|
||||||
|
|
||||||
|
self.use_output = attn_backend.accept_output_buffer
|
||||||
|
compilation_config = get_current_vllm_config().compilation_config
|
||||||
|
if prefix in compilation_config.static_forward_context:
|
||||||
|
raise ValueError(f"Duplicate layer name: {prefix}")
|
||||||
|
compilation_config.static_forward_context[prefix] = self
|
||||||
|
self.layer_name = prefix
|
||||||
|
self.attn_type = attn_type
|
||||||
|
# use a placeholder kv cache tensor during init, which will be replaced
|
||||||
|
# by bind_kv_cache
|
||||||
|
# this variable will not be accessed if use_direct_call is True
|
||||||
|
self.kv_cache = [
|
||||||
|
torch.tensor([]) for _ in range(
|
||||||
|
get_current_vllm_config().parallel_config.num_virtual_engine)
|
||||||
|
]
|
||||||
|
self.kv_cache_scale = [
|
||||||
|
torch.tensor([]) for _ in range(
|
||||||
|
get_current_vllm_config().parallel_config.num_virtual_engine)
|
||||||
|
]
|
||||||
|
|
||||||
|
self.q_range = torch.tensor(envs.Q_SCALE_CONSTANT, dtype=torch.float32)
|
||||||
|
self.k_range = torch.tensor(envs.K_SCALE_CONSTANT, dtype=torch.float32)
|
||||||
|
self.v_range = torch.tensor(envs.V_SCALE_CONSTANT, dtype=torch.float32)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
# For some alternate attention backends like MLA the attention output
|
||||||
|
# shape does not match the query shape, so we optionally let the model
|
||||||
|
# definition specify the output tensor shape.
|
||||||
|
output_shape: Optional[torch.Size] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
The KV cache is stored inside this class and is accessed via
|
||||||
|
`self.kv_cache`.
|
||||||
|
|
||||||
|
Attention metadata (`attn_metadata`) is set using a context manager in
|
||||||
|
the model runner's `execute_model` method. It is accessed via forward
|
||||||
|
context using
|
||||||
|
`vllm.forward_context.get_forward_context().attn_metadata`.
|
||||||
|
"""
|
||||||
|
if self.calculate_kv_scales:
|
||||||
|
attn_metadata = get_forward_context().attn_metadata
|
||||||
|
if attn_metadata.enable_kv_scales_calculation:
|
||||||
|
self.calc_kv_scales(query, key, value)
|
||||||
|
if self.use_output:
|
||||||
|
output_shape = (output_shape
|
||||||
|
if output_shape is not None else query.shape)
|
||||||
|
output = torch.empty(output_shape,
|
||||||
|
dtype=query.dtype,
|
||||||
|
device=query.device)
|
||||||
|
# hidden_size = output_shape[-1]
|
||||||
|
# We skip reshaping query, key and value tensors for the MLA
|
||||||
|
# backend since these tensors have different semantics and are
|
||||||
|
# processed differently.
|
||||||
|
if not self.use_mla:
|
||||||
|
# Reshape the query, key, and value tensors.
|
||||||
|
# NOTE(woosuk): We do this outside the custom op to minimize the
|
||||||
|
# CPU overheads from the non-CUDA-graph regions.
|
||||||
|
query = query.view(-1, self.num_heads, self.head_size)
|
||||||
|
output = output.view(-1, self.num_heads, self.head_size)
|
||||||
|
if key is not None:
|
||||||
|
key = key.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
if value is not None:
|
||||||
|
value = value.view(-1, self.num_kv_heads, self.head_size)
|
||||||
|
if self.use_direct_call:
|
||||||
|
forward_context: ForwardContext = get_forward_context()
|
||||||
|
attn_metadata = forward_context.attn_metadata
|
||||||
|
self_kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||||
|
self_kv_cache_scale = self.kv_cache_scale[forward_context.virtual_engine]
|
||||||
|
return self.impl.forward(self,
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
self_kv_cache,
|
||||||
|
self_kv_cache_scale,
|
||||||
|
attn_metadata,
|
||||||
|
output=output)
|
||||||
|
else:
|
||||||
|
torch.ops.vllm.unified_attention_with_output(
|
||||||
|
query, key, value, output, self.layer_name)
|
||||||
|
# return output.view(-1, hidden_size)
|
||||||
|
else:
|
||||||
|
if self.use_direct_call:
|
||||||
|
forward_context = get_forward_context()
|
||||||
|
attn_metadata = forward_context.attn_metadata
|
||||||
|
self_kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||||
|
self_kv_cache_scale = self.kv_cache_scale[forward_context.virtual_engine]
|
||||||
|
return self.impl.forward(self, query, key, value,
|
||||||
|
self_kv_cache,self_kv_cache_scale, attn_metadata)
|
||||||
|
else:
|
||||||
|
return torch.ops.vllm.unified_attention(
|
||||||
|
query, key, value, self.layer_name)
|
||||||
|
|
||||||
|
def calc_kv_scales(self, query, key, value):
|
||||||
|
self._q_scale.copy_(torch.abs(query).max() / self.q_range)
|
||||||
|
self._k_scale.copy_(torch.abs(key).max() / self.k_range)
|
||||||
|
self._v_scale.copy_(torch.abs(value).max() / self.v_range)
|
||||||
|
self._k_scale_float = self._k_scale.item()
|
||||||
|
self._v_scale_float = self._v_scale.item()
|
||||||
|
# We only calculate the scales once
|
||||||
|
self.calculate_kv_scales = False
|
||||||
|
|
||||||
|
def extra_repr(self) -> str:
|
||||||
|
s = f"head_size={self.impl.head_size}" # type: ignore
|
||||||
|
s += f", num_heads={self.impl.num_heads}" # type: ignore
|
||||||
|
s += f", num_kv_heads={self.impl.num_kv_heads}" # type: ignore
|
||||||
|
s += f", scale={self.impl.scale}" # type: ignore
|
||||||
|
s += f", backend={self.impl.__class__.__name__}"
|
||||||
|
return s
|
||||||
|
|
||||||
|
def process_weights_after_loading(self, act_dtype: torch.dtype):
|
||||||
|
if hasattr(self.impl, "process_weights_after_loading"):
|
||||||
|
self.impl.process_weights_after_loading(act_dtype)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiHeadAttention(nn.Module):
|
||||||
|
"""Multi-headed attention without any cache, used for ViT."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
scale: float,
|
||||||
|
num_kv_heads: Optional[int] = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.head_size = head_size
|
||||||
|
self.scale = scale
|
||||||
|
self.num_kv_heads = num_heads if num_kv_heads is None else num_kv_heads
|
||||||
|
|
||||||
|
# assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
# self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
# dtype = torch.get_default_dtype()
|
||||||
|
# attn_backend = get_attn_backend(head_size,
|
||||||
|
# dtype,
|
||||||
|
# kv_cache_dtype=None,
|
||||||
|
# block_size=16,
|
||||||
|
# is_attention_free=False)
|
||||||
|
# backend = backend_name_to_enum(attn_backend.get_name())
|
||||||
|
# if backend in {_Backend.FLASH_ATTN, _Backend.FLASH_ATTN_VLLM_V1}:
|
||||||
|
# backend = _Backend.XFORMERS
|
||||||
|
|
||||||
|
# self.attn_backend = backend if backend in {
|
||||||
|
# _Backend.TORCH_SDPA, _Backend.XFORMERS, _Backend.PALLAS_VLLM_V1
|
||||||
|
# } else _Backend.TORCH_SDPA
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Input shape: batch_size x seq_len x hidden_size"""
|
||||||
|
# TODO(Isotr0py): Use existing backend implementations and support FA3
|
||||||
|
bsz, q_len, _ = query.size()
|
||||||
|
kv_len = key.size(1)
|
||||||
|
|
||||||
|
# query = query.view(bsz, q_len, self.num_heads, self.head_size)
|
||||||
|
# key = key.view(bsz, kv_len, self.num_kv_heads, self.head_size)
|
||||||
|
# value = value.view(bsz, kv_len, self.num_kv_heads, self.head_size)
|
||||||
|
|
||||||
|
# if (num_repeat := self.num_queries_per_kv) > 1:
|
||||||
|
# # Handle MQA and GQA
|
||||||
|
# key = torch.repeat_interleave(key, num_repeat, dim=2)
|
||||||
|
# value = torch.repeat_interleave(value, num_repeat, dim=2)
|
||||||
|
|
||||||
|
# if self.attn_backend == _Backend.XFORMERS:
|
||||||
|
# from xformers import ops as xops
|
||||||
|
|
||||||
|
# out = xops.memory_efficient_attention_forward(query,
|
||||||
|
# key,
|
||||||
|
# value,
|
||||||
|
# scale=self.scale)
|
||||||
|
# elif self.attn_backend == _Backend.TORCH_SDPA:
|
||||||
|
# query, key, value = (x.transpose(1, 2)
|
||||||
|
# for x in (query, key, value))
|
||||||
|
# out = F.scaled_dot_product_attention(query,
|
||||||
|
# key,
|
||||||
|
# value,
|
||||||
|
# scale=self.scale)
|
||||||
|
# out = out.transpose(1, 2)
|
||||||
|
# elif self.attn_backend == _Backend.PALLAS_VLLM_V1:
|
||||||
|
# query, key, value = (x.transpose(1, 2)
|
||||||
|
# for x in (query, key, value))
|
||||||
|
# from torch_xla.experimental.custom_kernel import flash_attention
|
||||||
|
# out = flash_attention(query, key, value, sm_scale=self.scale)
|
||||||
|
# out = out.transpose(1, 2)
|
||||||
|
|
||||||
|
# return out.reshape(bsz, q_len, -1)
|
||||||
|
query = query.view(bsz * q_len, self.num_heads, self.head_size)
|
||||||
|
key = key.view(bsz * kv_len, self.num_kv_heads, self.head_size)
|
||||||
|
value = value.view(bsz * kv_len, self.num_kv_heads, self.head_size)
|
||||||
|
cu_q = torch.tensor([0,] + [q_len for _ in range(bsz)], device=query.device, dtype=torch.int32).cumsum(dim=0, dtype=torch.int32)
|
||||||
|
cu_kv = torch.tensor([0,] + [kv_len for _ in range(bsz)], device=query.device, dtype=torch.int32).cumsum(dim=0, dtype=torch.int32)
|
||||||
|
out = ops.flash_attn_varlen_func(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
cu_q,
|
||||||
|
cu_kv,
|
||||||
|
q_len,
|
||||||
|
kv_len,
|
||||||
|
softmax_scale=self.scale,
|
||||||
|
causal=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
return out.view(bsz, q_len, -1)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
layer_name: str,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
forward_context: ForwardContext = get_forward_context()
|
||||||
|
attn_metadata = forward_context.attn_metadata
|
||||||
|
self = forward_context.no_compile_layers[layer_name]
|
||||||
|
kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||||
|
return self.impl.forward(self, query, key, value, kv_cache, attn_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_attention_fake(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
layer_name: str,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
return torch.empty_like(query).contiguous()
|
||||||
|
|
||||||
|
|
||||||
|
direct_register_custom_op(
|
||||||
|
op_name="unified_attention",
|
||||||
|
op_func=unified_attention,
|
||||||
|
mutates_args=[],
|
||||||
|
fake_impl=unified_attention_fake,
|
||||||
|
dispatch_key=current_platform.dispatch_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_attention_with_output(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
output: torch.Tensor,
|
||||||
|
layer_name: str,
|
||||||
|
) -> None:
|
||||||
|
forward_context: ForwardContext = get_forward_context()
|
||||||
|
attn_metadata = forward_context.attn_metadata
|
||||||
|
self = forward_context.no_compile_layers[layer_name]
|
||||||
|
kv_cache = self.kv_cache[forward_context.virtual_engine]
|
||||||
|
self.impl.forward(self,
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
kv_cache,
|
||||||
|
attn_metadata,
|
||||||
|
output=output)
|
||||||
|
|
||||||
|
|
||||||
|
def unified_attention_with_output_fake(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
output: torch.Tensor,
|
||||||
|
layer_name: str,
|
||||||
|
) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
direct_register_custom_op(
|
||||||
|
op_name="unified_attention_with_output",
|
||||||
|
op_func=unified_attention_with_output,
|
||||||
|
mutates_args=["output"],
|
||||||
|
fake_impl=unified_attention_with_output_fake,
|
||||||
|
dispatch_key=current_platform.dispatch_key,
|
||||||
|
)
|
||||||
0
vllm/attention/ops/__init__.py
Normal file
0
vllm/attention/ops/__init__.py
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
|
||||||
|
def blocksparse_flash_attn_varlen_fwd(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v, # (#tokens, n_heads, head_size)
|
||||||
|
cu_seqlens_k,
|
||||||
|
cu_seqlens_q,
|
||||||
|
sm_scale,
|
||||||
|
sparse_layout,
|
||||||
|
*,
|
||||||
|
block_size=64,
|
||||||
|
q_block_size=None,
|
||||||
|
max_seqlen=None):
|
||||||
|
# split q to blocks
|
||||||
|
|
||||||
|
assert isinstance(sparse_layout, (list, tuple))
|
||||||
|
|
||||||
|
_, n_heads, head_size = q.shape
|
||||||
|
batch_size = cu_seqlens_k.size(0) - 1
|
||||||
|
q_block_size = q_block_size or block_size
|
||||||
|
|
||||||
|
assert q.dim() == k.dim() == v.dim() == 3
|
||||||
|
assert q.size(1) % k.size(1) == 0
|
||||||
|
assert q.size(2) == k.size(2)
|
||||||
|
# TODO(linxihui): allow k, v to have different head_size
|
||||||
|
assert k.shape == v.shape
|
||||||
|
assert cu_seqlens_k.dim() == 1
|
||||||
|
|
||||||
|
q_k_ratio = q.size(1) // k.size(1)
|
||||||
|
|
||||||
|
if cu_seqlens_q is None:
|
||||||
|
if q.size(0) == batch_size: # decoding only
|
||||||
|
cu_seqlens_q = torch.arange(
|
||||||
|
0,
|
||||||
|
batch_size + 1,
|
||||||
|
dtype=cu_seqlens_k.dtype,
|
||||||
|
device=cu_seqlens_k.device,
|
||||||
|
)
|
||||||
|
elif q.size(0) == k.size(0):
|
||||||
|
cu_seqlens_q = cu_seqlens_k
|
||||||
|
else:
|
||||||
|
raise ValueError("cu_seqlens_q must be specified\
|
||||||
|
if it mix of prefilling and decoding.")
|
||||||
|
else:
|
||||||
|
assert cu_seqlens_k.size(0) == cu_seqlens_q.size(0)
|
||||||
|
|
||||||
|
# switch to use cpu to avoid too many kernel launches when iterated over
|
||||||
|
q_lens = (cu_seqlens_q[1:] - cu_seqlens_q[:-1]).cpu()
|
||||||
|
k_lens = (cu_seqlens_k[1:] - cu_seqlens_k[:-1]).cpu()
|
||||||
|
|
||||||
|
assert torch.logical_or(q_lens == 1, k_lens == q_lens).all(), (
|
||||||
|
"length of q should either be 1 (decoding) or same as k (prefilling).")
|
||||||
|
|
||||||
|
if max_seqlen:
|
||||||
|
assert k_lens.max() <= max_seqlen
|
||||||
|
|
||||||
|
n_blocks = (q_lens + q_block_size - 1) // q_block_size
|
||||||
|
|
||||||
|
q_batch_ids = torch.tensor(
|
||||||
|
[i for i, n in enumerate(n_blocks) for _ in range(n)],
|
||||||
|
dtype=cu_seqlens_q.dtype,
|
||||||
|
device=cu_seqlens_q.device,
|
||||||
|
)
|
||||||
|
q_start_sids = torch.tensor(
|
||||||
|
[i * q_block_size for n in n_blocks for i in range(n)],
|
||||||
|
dtype=cu_seqlens_q.dtype,
|
||||||
|
device=cu_seqlens_q.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
out = q.new_empty(q.shape)
|
||||||
|
cu_seqlens_q = cu_seqlens_q.contiguous()
|
||||||
|
cu_seqlens_k = cu_seqlens_k.contiguous()
|
||||||
|
|
||||||
|
layout_crow_indices, layout_col_indices = sparse_layout
|
||||||
|
block_d = triton.next_power_of_2(head_size)
|
||||||
|
|
||||||
|
decoding_only = (q_lens == 1).all().item()
|
||||||
|
grid = (len(q_start_sids), n_heads, 1)
|
||||||
|
|
||||||
|
_fwd_kernel_batch_inference[grid](
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
out,
|
||||||
|
sm_scale,
|
||||||
|
cu_seqlens_q[:-1],
|
||||||
|
cu_seqlens_q[1:],
|
||||||
|
cu_seqlens_k[:-1],
|
||||||
|
cu_seqlens_k[1:],
|
||||||
|
q_batch_ids,
|
||||||
|
q_start_sids,
|
||||||
|
0,
|
||||||
|
*q.stride(),
|
||||||
|
0,
|
||||||
|
*k.stride(),
|
||||||
|
0,
|
||||||
|
*v.stride(),
|
||||||
|
0,
|
||||||
|
*out.stride(),
|
||||||
|
layout_crow_indices,
|
||||||
|
layout_col_indices,
|
||||||
|
*layout_crow_indices.stride(),
|
||||||
|
*layout_col_indices.stride(),
|
||||||
|
q_k_ratio,
|
||||||
|
HAS_BATCH_DIM=False,
|
||||||
|
D_HEAD=head_size,
|
||||||
|
BLOCK_M=q_block_size,
|
||||||
|
BLOCK_N=block_size,
|
||||||
|
BLOCK_D=block_d,
|
||||||
|
BLOCK_M_LOADING=(16 if decoding_only else
|
||||||
|
q_block_size), # smaller for decoding
|
||||||
|
EVEN_D=block_d == head_size,
|
||||||
|
num_warps=1 if decoding_only else 4,
|
||||||
|
num_stages=3)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
Q,
|
||||||
|
k_block_col_idx,
|
||||||
|
layout_col_ptr,
|
||||||
|
layout_col_stride_h,
|
||||||
|
layout_col_stride_m,
|
||||||
|
k_ptrs,
|
||||||
|
v_ptrs,
|
||||||
|
off_h,
|
||||||
|
offs_m,
|
||||||
|
offs_n,
|
||||||
|
offs_d,
|
||||||
|
stride_kt,
|
||||||
|
stride_vt,
|
||||||
|
sm_scale,
|
||||||
|
k_seqlen,
|
||||||
|
past_len,
|
||||||
|
LAST_K_BLOCK: tl.constexpr,
|
||||||
|
BLOCK_M_LOADING: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
D_HEAD: tl.constexpr,
|
||||||
|
EVEN_D: tl.constexpr,
|
||||||
|
M_LT_N: tl.constexpr,
|
||||||
|
):
|
||||||
|
k_block_id = tl.load(layout_col_ptr + off_h * layout_col_stride_h +
|
||||||
|
k_block_col_idx * layout_col_stride_m).to(tl.int32)
|
||||||
|
start_n = k_block_id * BLOCK_N
|
||||||
|
if LAST_K_BLOCK:
|
||||||
|
if EVEN_D:
|
||||||
|
k = tl.load(
|
||||||
|
k_ptrs + start_n * stride_kt,
|
||||||
|
mask=offs_n[None, :] + start_n < k_seqlen,
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
k = tl.load(
|
||||||
|
k_ptrs + start_n * stride_kt,
|
||||||
|
mask=(offs_n[None, :] + start_n < k_seqlen) &
|
||||||
|
(offs_d[:, None] < D_HEAD),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if EVEN_D:
|
||||||
|
k = tl.load(k_ptrs + start_n * stride_kt)
|
||||||
|
else:
|
||||||
|
k = tl.load(k_ptrs + start_n * stride_kt,
|
||||||
|
mask=offs_d[:, None] < D_HEAD,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M_LOADING, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk += tl.dot(q, k)
|
||||||
|
qk *= sm_scale
|
||||||
|
|
||||||
|
# the following is needed only when LAST_K_BLOCK or BLOCK_M < BLOCK_N
|
||||||
|
if LAST_K_BLOCK | M_LT_N:
|
||||||
|
qk += tl.where(
|
||||||
|
offs_m[:, None] + past_len >= (start_n + offs_n[None, :]),
|
||||||
|
0,
|
||||||
|
float("-inf"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# flash-attn2
|
||||||
|
m_ij = tl.maximum(m_i, tl.max(qk, 1))
|
||||||
|
p = tl.math.exp2(qk - m_ij[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
alpha = tl.math.exp2(m_i - m_ij)
|
||||||
|
acc = acc * alpha[:, None]
|
||||||
|
# update m_i
|
||||||
|
m_i = m_ij
|
||||||
|
l_i = l_i * alpha + l_ij
|
||||||
|
|
||||||
|
p = p.to(Q.dtype.element_ty)
|
||||||
|
# update acc
|
||||||
|
if LAST_K_BLOCK:
|
||||||
|
if EVEN_D:
|
||||||
|
v = tl.load(
|
||||||
|
v_ptrs + start_n * stride_vt,
|
||||||
|
mask=offs_n[:, None] + start_n < k_seqlen,
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
v = tl.load(
|
||||||
|
v_ptrs + start_n * stride_vt,
|
||||||
|
mask=(offs_n[:, None] + start_n < k_seqlen) &
|
||||||
|
(offs_d[None, :] < D_HEAD),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if EVEN_D:
|
||||||
|
v = tl.load(v_ptrs + start_n * stride_vt)
|
||||||
|
else:
|
||||||
|
v = tl.load(v_ptrs + start_n * stride_vt,
|
||||||
|
mask=offs_d[None, :] < D_HEAD,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
acc += tl.dot(p, v)
|
||||||
|
|
||||||
|
return acc, l_i, m_i
|
||||||
|
|
||||||
|
|
||||||
|
@triton.heuristics({
|
||||||
|
"M_LT_N":
|
||||||
|
lambda kwargs: kwargs["BLOCK_M"] < kwargs["BLOCK_N"],
|
||||||
|
})
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_batch_inference(
|
||||||
|
Q,
|
||||||
|
K,
|
||||||
|
V,
|
||||||
|
Out,
|
||||||
|
sm_scale,
|
||||||
|
q_batch_starts,
|
||||||
|
q_batch_ends,
|
||||||
|
k_batch_starts,
|
||||||
|
k_batch_ends,
|
||||||
|
q_batch_ids,
|
||||||
|
q_start_sids,
|
||||||
|
stride_qb,
|
||||||
|
stride_qt,
|
||||||
|
stride_qh,
|
||||||
|
stride_qd,
|
||||||
|
stride_kb,
|
||||||
|
stride_kt,
|
||||||
|
stride_kh,
|
||||||
|
stride_kd,
|
||||||
|
stride_vb,
|
||||||
|
stride_vt,
|
||||||
|
stride_vh,
|
||||||
|
stride_vd,
|
||||||
|
stride_ob,
|
||||||
|
stride_ot,
|
||||||
|
stride_oh,
|
||||||
|
stride_od,
|
||||||
|
layout_crow_ptr,
|
||||||
|
layout_col_ptr,
|
||||||
|
layout_crow_stride_h,
|
||||||
|
layout_crow_stride_m,
|
||||||
|
layout_col_stride_h,
|
||||||
|
layout_col_stride_m,
|
||||||
|
q_k_ratio,
|
||||||
|
HAS_BATCH_DIM: tl.constexpr,
|
||||||
|
D_HEAD: tl.constexpr,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
BLOCK_D: tl.constexpr,
|
||||||
|
BLOCK_M_LOADING: tl.constexpr,
|
||||||
|
EVEN_D: tl.constexpr,
|
||||||
|
M_LT_N: tl.constexpr,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
NOTATION:
|
||||||
|
pid: position id
|
||||||
|
sid: storage id
|
||||||
|
sbid: storage block id
|
||||||
|
pbid: position block id
|
||||||
|
offs_m, offs_n: storage offsets of m-dim(q, row) and n-dim(k, col)
|
||||||
|
|
||||||
|
TODO(linxihui):
|
||||||
|
Optimize grouped-attn
|
||||||
|
"""
|
||||||
|
off_zm = tl.program_id(0)
|
||||||
|
off_h = tl.program_id(1)
|
||||||
|
|
||||||
|
off_h_for_kv = off_h // q_k_ratio
|
||||||
|
|
||||||
|
if HAS_BATCH_DIM:
|
||||||
|
off_z = tl.program_id(2)
|
||||||
|
Q += off_z * stride_qb
|
||||||
|
K += off_z * stride_kb
|
||||||
|
V += off_z * stride_vb
|
||||||
|
Out += off_z * stride_ob
|
||||||
|
start_m = off_zm
|
||||||
|
q_start_sid = start_m * BLOCK_M # always 0 for decoding
|
||||||
|
else:
|
||||||
|
off_z = tl.load(q_batch_ids + off_zm).to(tl.int32) # [0, 0, 0, 1]
|
||||||
|
q_start_sid = tl.load(q_start_sids + off_zm)
|
||||||
|
start_m = q_start_sid // BLOCK_M # q_sbid
|
||||||
|
|
||||||
|
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M_LOADING)
|
||||||
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
offs_d = tl.arange(0, BLOCK_D)
|
||||||
|
|
||||||
|
q_cu_start = tl.load(q_batch_starts + off_z).to(tl.int32)
|
||||||
|
q_seqlen = tl.load(q_batch_ends + off_z).to(tl.int32) - q_cu_start
|
||||||
|
k_cu_start = tl.load(k_batch_starts + off_z).to(tl.int32)
|
||||||
|
k_seqlen = tl.load(k_batch_ends + off_z).to(tl.int32) - k_cu_start
|
||||||
|
past_len = k_seqlen - q_seqlen
|
||||||
|
|
||||||
|
Q += q_cu_start * stride_qt + off_h * stride_qh
|
||||||
|
K += k_cu_start * stride_kt + off_h_for_kv * stride_kh
|
||||||
|
V += k_cu_start * stride_vt + off_h_for_kv * stride_vh
|
||||||
|
Out += q_cu_start * stride_ot + off_h * stride_oh
|
||||||
|
|
||||||
|
q_pbid = (past_len + q_start_sid) // BLOCK_M
|
||||||
|
|
||||||
|
if EVEN_D:
|
||||||
|
q = tl.load(
|
||||||
|
Q + offs_m[:, None] * stride_qt + offs_d[None, :] * stride_qd,
|
||||||
|
mask=offs_m[:, None] < q_seqlen,
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
q = tl.load(
|
||||||
|
Q + offs_m[:, None] * stride_qt + offs_d[None, :] * stride_qd,
|
||||||
|
mask=(offs_m[:, None] < q_seqlen) & (offs_d[None, :] < D_HEAD),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
sparse_crow_ptr = (layout_crow_ptr + off_h * layout_crow_stride_h +
|
||||||
|
q_pbid * layout_crow_stride_m)
|
||||||
|
|
||||||
|
# TODO(linxihui): load at once, with any Triton version
|
||||||
|
# that supports `tl.split`, e.g., Triton 3.0
|
||||||
|
k_block_start = tl.load(sparse_crow_ptr).to(tl.int32)
|
||||||
|
k_block_end = tl.load(sparse_crow_ptr + 1).to(tl.int32)
|
||||||
|
|
||||||
|
m_i = tl.zeros([BLOCK_M_LOADING], dtype=tl.float32) - float("inf")
|
||||||
|
l_i = tl.zeros([BLOCK_M_LOADING], dtype=tl.float32)
|
||||||
|
acc = tl.zeros([BLOCK_M_LOADING, BLOCK_D], dtype=tl.float32)
|
||||||
|
|
||||||
|
k_ptrs = K + offs_n[None, :] * stride_kt + offs_d[:, None] * stride_kd
|
||||||
|
v_ptrs = V + offs_n[:, None] * stride_vt + offs_d[None, :] * stride_vd
|
||||||
|
|
||||||
|
sm_scale *= (
|
||||||
|
1.44269504 # 1/log2 as we use base2 for exponential and logarithm
|
||||||
|
)
|
||||||
|
|
||||||
|
for k_block_col_idx in range(k_block_start, k_block_end - 1):
|
||||||
|
acc, l_i, m_i = _fwd_kernel_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
Q,
|
||||||
|
k_block_col_idx,
|
||||||
|
layout_col_ptr,
|
||||||
|
layout_col_stride_h,
|
||||||
|
layout_col_stride_m,
|
||||||
|
k_ptrs,
|
||||||
|
v_ptrs,
|
||||||
|
off_h,
|
||||||
|
offs_m,
|
||||||
|
offs_n,
|
||||||
|
offs_d,
|
||||||
|
stride_kt,
|
||||||
|
stride_vt,
|
||||||
|
sm_scale,
|
||||||
|
k_seqlen,
|
||||||
|
past_len,
|
||||||
|
False,
|
||||||
|
BLOCK_M_LOADING,
|
||||||
|
BLOCK_N,
|
||||||
|
D_HEAD,
|
||||||
|
EVEN_D,
|
||||||
|
M_LT_N,
|
||||||
|
)
|
||||||
|
|
||||||
|
acc, l_i, m_i = _fwd_kernel_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
Q,
|
||||||
|
k_block_end - 1,
|
||||||
|
layout_col_ptr,
|
||||||
|
layout_col_stride_h,
|
||||||
|
layout_col_stride_m,
|
||||||
|
k_ptrs,
|
||||||
|
v_ptrs,
|
||||||
|
off_h,
|
||||||
|
offs_m,
|
||||||
|
offs_n,
|
||||||
|
offs_d,
|
||||||
|
stride_kt,
|
||||||
|
stride_vt,
|
||||||
|
sm_scale,
|
||||||
|
k_seqlen,
|
||||||
|
past_len,
|
||||||
|
True,
|
||||||
|
BLOCK_M_LOADING,
|
||||||
|
BLOCK_N,
|
||||||
|
D_HEAD,
|
||||||
|
EVEN_D,
|
||||||
|
M_LT_N,
|
||||||
|
)
|
||||||
|
|
||||||
|
# flash-attn 2
|
||||||
|
m_i += tl.math.log2(l_i)
|
||||||
|
acc = acc / l_i[:, None]
|
||||||
|
|
||||||
|
# write output
|
||||||
|
if EVEN_D:
|
||||||
|
tl.store(
|
||||||
|
Out + offs_m[:, None] * stride_ot + offs_d[None, :] * stride_od,
|
||||||
|
acc,
|
||||||
|
mask=offs_m[:, None] < q_seqlen,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
tl.store(
|
||||||
|
Out + offs_m[:, None] * stride_ot + offs_d[None, :] * stride_od,
|
||||||
|
acc,
|
||||||
|
mask=(offs_m[:, None] < q_seqlen) & (offs_d[None, :] < D_HEAD),
|
||||||
|
)
|
||||||
238
vllm/attention/ops/blocksparse_attention/interface.py
Normal file
238
vllm/attention/ops/blocksparse_attention/interface.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
from .utils import (dense_to_crow_col, get_head_sliding_step,
|
||||||
|
get_sparse_attn_mask)
|
||||||
|
|
||||||
|
IS_COMPUTE_8_OR_ABOVE = current_platform.has_device_capability(80)
|
||||||
|
|
||||||
|
if IS_COMPUTE_8_OR_ABOVE:
|
||||||
|
from .blocksparse_attention_kernel import blocksparse_flash_attn_varlen_fwd
|
||||||
|
|
||||||
|
|
||||||
|
class LocalStridedBlockSparseAttn(torch.nn.Module):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
n_heads,
|
||||||
|
max_seqlen,
|
||||||
|
local_blocks,
|
||||||
|
vert_stride,
|
||||||
|
block_size,
|
||||||
|
device=None,
|
||||||
|
dtype=None,
|
||||||
|
homo_head=False,
|
||||||
|
active_head_range=None,
|
||||||
|
q_block_size=None,
|
||||||
|
use_spda=None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
if use_spda is None:
|
||||||
|
use_spda = current_platform.is_rocm() or \
|
||||||
|
current_platform.is_cpu() or not \
|
||||||
|
IS_COMPUTE_8_OR_ABOVE
|
||||||
|
device = device or (torch.cuda.current_device()
|
||||||
|
if current_platform.is_cuda_alike() else "cpu")
|
||||||
|
device = torch.device(device)
|
||||||
|
# NOTE: vllm CPU backend support BF16 instead of FP16.
|
||||||
|
dtype = dtype or (torch.bfloat16 if IS_COMPUTE_8_OR_ABOVE
|
||||||
|
or device.type == "cpu" else torch.half)
|
||||||
|
|
||||||
|
self.n_heads = n_heads
|
||||||
|
self.max_seqlen = max_seqlen
|
||||||
|
self.local_blocks = local_blocks
|
||||||
|
self.vert_stride = vert_stride
|
||||||
|
self.use_spda = use_spda
|
||||||
|
self.dtype = dtype
|
||||||
|
self.device = device
|
||||||
|
self.block_size = block_size
|
||||||
|
self.q_block_size = q_block_size
|
||||||
|
self.homo_head = homo_head
|
||||||
|
self.active_head_range = active_head_range
|
||||||
|
self.head_sliding_step = get_head_sliding_step(n_heads, vert_stride,
|
||||||
|
homo_head)
|
||||||
|
|
||||||
|
sparse_layout, sparse_pattern, self.dense_attn_mask = (
|
||||||
|
self.get_attn_pattern(dtype, device))
|
||||||
|
|
||||||
|
if q_block_size is not None and q_block_size != block_size:
|
||||||
|
if q_block_size > block_size:
|
||||||
|
assert q_block_size % block_size == 0
|
||||||
|
blocks_to_merge = q_block_size // block_size
|
||||||
|
shape = sparse_pattern.shape
|
||||||
|
sparse_pattern = sparse_pattern.view(shape[0], -1,
|
||||||
|
blocks_to_merge,
|
||||||
|
shape[-1])
|
||||||
|
sparse_pattern = sparse_pattern.sum(2)
|
||||||
|
sparse_layout = dense_to_crow_col(sparse_pattern)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Does not support smaller q_block_size. It will be slower."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sparse_layout = sparse_layout
|
||||||
|
|
||||||
|
def get_attn_pattern(self, dtype, device):
|
||||||
|
sparse_layout, sparse_pattern, dense_attn_mask = get_sparse_attn_mask(
|
||||||
|
self.n_heads,
|
||||||
|
self.max_seqlen,
|
||||||
|
self.max_seqlen,
|
||||||
|
dtype,
|
||||||
|
device,
|
||||||
|
block_size=self.block_size,
|
||||||
|
local_blocks=self.local_blocks,
|
||||||
|
vert_stride=self.vert_stride,
|
||||||
|
homo_head=self.homo_head,
|
||||||
|
return_dense=self.use_spda,
|
||||||
|
dense_mask_type="bias",
|
||||||
|
)
|
||||||
|
if (not self.homo_head) and (self.active_head_range is not None):
|
||||||
|
assert isinstance(self.active_head_range, tuple)
|
||||||
|
assert (len(self.active_head_range) == 2)
|
||||||
|
h_start, h_end = self.active_head_range
|
||||||
|
sparse_layout = tuple(x[h_start:h_end] for x in sparse_layout)
|
||||||
|
if self.use_spda:
|
||||||
|
dense_attn_mask = dense_attn_mask[h_start:h_end]
|
||||||
|
return sparse_layout, sparse_pattern, dense_attn_mask
|
||||||
|
|
||||||
|
def varlen_attn(self,
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
cu_seqlens_k,
|
||||||
|
cu_seqlens_q=None,
|
||||||
|
sm_scale=None):
|
||||||
|
"""
|
||||||
|
q, k, v: shape = (num_tokens, num_heads_q/kv, head_size).
|
||||||
|
Support grouped attention, with `q[:, i*r:(i*r + r)]`
|
||||||
|
is correspondent to `k[:, i]`, where `r` is the q/k ratio.
|
||||||
|
cu_seqlens_k: shape=(batch_size + 1,),
|
||||||
|
indicating segment of samples,
|
||||||
|
e.g., `k[cu_seqlen[i]:cu_seqlne[i+1]]` is q of sample i
|
||||||
|
cu_seqlens_q: shape=(batch_size + 1, ).
|
||||||
|
Default None: same as cu_seqlens_k for prefilling or
|
||||||
|
[0, 1, .., batch_size] for decoding.
|
||||||
|
The only case you need to specify is when q is a mix of
|
||||||
|
prefilling and decoding.
|
||||||
|
sm_scale: softmax scale, default to 1/sqrt(head_size).
|
||||||
|
|
||||||
|
return: tensor of shape as q.
|
||||||
|
"""
|
||||||
|
assert (
|
||||||
|
IS_COMPUTE_8_OR_ABOVE
|
||||||
|
), "Requires compute capability of 8 or above (Ampere or newer) to use \
|
||||||
|
Triton kernel."
|
||||||
|
|
||||||
|
sm_scale = sm_scale or 1.0 / math.sqrt(q.size(-1))
|
||||||
|
|
||||||
|
return blocksparse_flash_attn_varlen_fwd(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
cu_seqlens_k,
|
||||||
|
cu_seqlens_q,
|
||||||
|
sm_scale,
|
||||||
|
self.sparse_layout,
|
||||||
|
block_size=self.block_size,
|
||||||
|
q_block_size=self.q_block_size,
|
||||||
|
max_seqlen=self.max_seqlen,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transpose_and_pad(x, cu_seqlens, maxlen, head_repeats=1):
|
||||||
|
"""
|
||||||
|
:param x: (total_tokens, n_heads, head_size)
|
||||||
|
:return: (batch, n_heads, length, head_size)
|
||||||
|
"""
|
||||||
|
x_padded = x.new_empty(
|
||||||
|
len(cu_seqlens) - 1, x.size(1), head_repeats, maxlen, x.size(2))
|
||||||
|
cu_seqlens = cu_seqlens.cpu()
|
||||||
|
for i, (s, e) in enumerate(zip(cu_seqlens[:-1], cu_seqlens[1:])):
|
||||||
|
x_padded[i, :, :, :e - s].copy_(x[s:e].transpose(0,
|
||||||
|
1).unsqueeze(1))
|
||||||
|
return x_padded.flatten(1, 2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transpose_and_unpad(x_padded, cu_seqlens):
|
||||||
|
"""
|
||||||
|
:param x_padded: (batch, n_heads, length, head_size)
|
||||||
|
:return: (total_tokens, n_heads, head_size)
|
||||||
|
"""
|
||||||
|
cu_seqlens = cu_seqlens.cpu()
|
||||||
|
total_n_tokens = cu_seqlens[-1]
|
||||||
|
x = x_padded.new_empty(total_n_tokens, x_padded.size(1),
|
||||||
|
x_padded.size(3))
|
||||||
|
for i, (s, e) in enumerate(zip(cu_seqlens[:-1], cu_seqlens[1:])):
|
||||||
|
x[s:e].copy_(x_padded[i, :, :e - s].transpose(0, 1))
|
||||||
|
return x
|
||||||
|
|
||||||
|
def spda(self, q, k, v, cu_seqlens_k, cu_seqlens_q=None, sm_scale=None):
|
||||||
|
"""For CPU, V100 or other older GPUs.
|
||||||
|
NOTE: torch SPDA supports nested tensor,
|
||||||
|
but seems extremely slow. Choose to pad instead.
|
||||||
|
"""
|
||||||
|
assert (cu_seqlens_q is None or
|
||||||
|
(cu_seqlens_q
|
||||||
|
== cu_seqlens_k).all()), "Can only handle prompt with SPDA."
|
||||||
|
assert q.size(0) == k.size(0), "can only handle prompt with SPDA."
|
||||||
|
|
||||||
|
assert q.size(1) % k.size(1) == 0
|
||||||
|
q_k_ratio = q.size(1) // k.size(1)
|
||||||
|
sm_scale = sm_scale or 1.0 / math.sqrt(q.size(-1))
|
||||||
|
cu_seqlens = cu_seqlens_k.cpu()
|
||||||
|
maxlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max()
|
||||||
|
|
||||||
|
if (self.dense_attn_mask.dtype != q.dtype
|
||||||
|
or self.dense_attn_mask.device != q.device):
|
||||||
|
_, _, self.dense_attn_mask = self.get_attn_pattern(
|
||||||
|
q.dtype, q.device)
|
||||||
|
attn_mask = self.dense_attn_mask[None, :, :maxlen, :maxlen]
|
||||||
|
|
||||||
|
q2 = self.transpose_and_pad(q, cu_seqlens, maxlen, 1)
|
||||||
|
k2, v2 = (self.transpose_and_pad(x, cu_seqlens, maxlen, q_k_ratio)
|
||||||
|
for x in [k, v])
|
||||||
|
spda_output = torch.nn.functional.scaled_dot_product_attention(
|
||||||
|
q2, k2, v2, attn_mask=attn_mask, scale=sm_scale)
|
||||||
|
return self.transpose_and_unpad(spda_output, cu_seqlens)
|
||||||
|
|
||||||
|
def forward(self, q, k, v, cu_seqlens_k, cu_seqlens_q=None, sm_scale=None):
|
||||||
|
"""Dispatch to `varlen_attn` (Ampere or newer) or
|
||||||
|
`self.spda`(cpu, Volta, Turing or older)based on
|
||||||
|
the type of device used and cuda compute capability.
|
||||||
|
|
||||||
|
q, k, v: shape = (num_tokens, num_heads_q/kv, head_size).
|
||||||
|
Support grouped attention, with `q[:, i*r:(i*r + r)]`
|
||||||
|
is correspondent to `k[:, i]`, where `r` is the q/k ratio.
|
||||||
|
cu_seqlens_k: shape=(batch_size + 1,), indicating segment of samples,
|
||||||
|
e.g., `k[cu_seqlen[i]:cu_seqlne[i+1]]` is q of sample i
|
||||||
|
cu_seqlens_q: shape=(batch_size + 1, ).
|
||||||
|
Default None: same as cu_seqlens_k for prefilling or
|
||||||
|
[0, 1, .., batch_size] for decoding.
|
||||||
|
The only case you need to specify
|
||||||
|
is when q is a mix of prefilling
|
||||||
|
and decoding.
|
||||||
|
sm_scale: softmax scale, default to 1/sqrt(head_size).
|
||||||
|
|
||||||
|
return: tensor of shape as q.
|
||||||
|
"""
|
||||||
|
assert k.dim() == 3
|
||||||
|
if self.use_spda:
|
||||||
|
return self.spda(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
cu_seqlens_k,
|
||||||
|
cu_seqlens_q=cu_seqlens_q,
|
||||||
|
sm_scale=sm_scale,
|
||||||
|
)
|
||||||
|
return self.varlen_attn(q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
cu_seqlens_k,
|
||||||
|
cu_seqlens_q=cu_seqlens_q,
|
||||||
|
sm_scale=sm_scale)
|
||||||
244
vllm/attention/ops/blocksparse_attention/utils.py
Normal file
244
vllm/attention/ops/blocksparse_attention/utils.py
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# Helper functions for 3D sparse pattern
|
||||||
|
# These function are not optimized and very inefficient.
|
||||||
|
# Avoid calling them too frequent or use a cache mechanism.
|
||||||
|
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
|
||||||
|
|
||||||
|
class csr_matrix:
|
||||||
|
"""Simple implementation of CSR matrix conversion without scipy.
|
||||||
|
This replaced scipy.sparse.csr_matrix() previously used."""
|
||||||
|
|
||||||
|
def __init__(self, input_array):
|
||||||
|
if not isinstance(input_array, np.ndarray):
|
||||||
|
raise ValueError("Input must be a NumPy array")
|
||||||
|
|
||||||
|
self.shape = input_array.shape
|
||||||
|
rows, cols = self.shape
|
||||||
|
data = []
|
||||||
|
indices = []
|
||||||
|
indptr = [0]
|
||||||
|
|
||||||
|
for i in range(rows):
|
||||||
|
for j in range(cols):
|
||||||
|
if input_array[i, j]:
|
||||||
|
data.append(input_array[i, j])
|
||||||
|
indices.append(j)
|
||||||
|
indptr.append(len(indices))
|
||||||
|
|
||||||
|
self.data = np.array(data)
|
||||||
|
self.indices = np.array(indices)
|
||||||
|
self.indptr = np.array(indptr)
|
||||||
|
|
||||||
|
|
||||||
|
def dense_to_crow_col(x: torch.Tensor):
|
||||||
|
"""Turning a 2D/3D torch tensor (x) to CSR rows/cols indexing.
|
||||||
|
NOTE: col_indices padded -1
|
||||||
|
"""
|
||||||
|
device = x.device
|
||||||
|
pad = -1
|
||||||
|
dim = x.dim()
|
||||||
|
assert x.dim() in (2, 3)
|
||||||
|
if x.dim() == 2:
|
||||||
|
x = x[None]
|
||||||
|
x = [csr_matrix(xi.bool().cpu().numpy()) for xi in x]
|
||||||
|
crows = torch.vstack([torch.from_numpy(xi.indptr) for xi in x])
|
||||||
|
cols = [torch.from_numpy(xi.indices) for xi in x]
|
||||||
|
max_cols = max(len(xi) for xi in cols)
|
||||||
|
cols = [
|
||||||
|
torch.cat([xi, pad + xi.new_zeros(max_cols - xi.shape[0])])
|
||||||
|
for xi in cols
|
||||||
|
]
|
||||||
|
cols = torch.vstack(cols)
|
||||||
|
if dim == 2:
|
||||||
|
crows = crows[0]
|
||||||
|
cols = cols[0]
|
||||||
|
return crows.to(device), cols.to(device)
|
||||||
|
|
||||||
|
|
||||||
|
def crow_col_to_dense(crows: torch.Tensor,
|
||||||
|
cols: torch.Tensor,
|
||||||
|
dtype: torch.dtype = torch.float16):
|
||||||
|
dim = crows.dim()
|
||||||
|
if dim == 1:
|
||||||
|
crows = crows[None]
|
||||||
|
cols = cols[None]
|
||||||
|
device = crows.device
|
||||||
|
crows, cols = crows.cpu(), cols.cpu() # faster in cpu
|
||||||
|
shape = (crows.shape[0], crows.shape[1] - 1, cols.max() + 1)
|
||||||
|
x = torch.zeros(shape, dtype=dtype)
|
||||||
|
for i in range(shape[0]):
|
||||||
|
for j in range(shape[1]):
|
||||||
|
x[i, j, cols[i, crows[i, j]:crows[i, j + 1]]] = 1
|
||||||
|
if dim == 1:
|
||||||
|
x = x[0]
|
||||||
|
return x.to(device)
|
||||||
|
|
||||||
|
|
||||||
|
def dense_to_ccol_row(x: torch.Tensor):
|
||||||
|
"""Similar, but to CSC format"""
|
||||||
|
x = x.transpose(-2, -1)
|
||||||
|
return dense_to_crow_col(x)
|
||||||
|
|
||||||
|
|
||||||
|
def ccol_row_to_dense(ccol: torch.Tensor,
|
||||||
|
rows: torch.Tensor,
|
||||||
|
dtype: torch.dtype = torch.float16):
|
||||||
|
return crow_col_to_dense(ccol, rows, dtype).permute(0, 2, 1).contiguous()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sparse_attn_mask_homo_head(
|
||||||
|
q_len: int,
|
||||||
|
max_seqlen: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
device: torch.device,
|
||||||
|
block_size: int = 128,
|
||||||
|
local_blocks: int = 4,
|
||||||
|
vert_stride: int = 4,
|
||||||
|
return_dense: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:return: a tuple of 3:
|
||||||
|
- tuple of crow_indices, col_indices representation
|
||||||
|
of CSR format.
|
||||||
|
- block dense mask
|
||||||
|
- all token dense mask (be aware that it can be
|
||||||
|
OOM if it is too big) if `return_dense==True`,
|
||||||
|
otherwise, None
|
||||||
|
"""
|
||||||
|
with torch.no_grad():
|
||||||
|
num_blocks = triton.cdiv(max_seqlen, block_size)
|
||||||
|
q_pos = torch.arange(num_blocks)[:, None]
|
||||||
|
k_pos = torch.arange(num_blocks)[None]
|
||||||
|
mask_vert_strided = (torch.arange(num_blocks) + 1) % vert_stride == 0
|
||||||
|
block_mask_dense = (((q_pos >= k_pos)
|
||||||
|
& ((q_pos - k_pos < local_blocks)
|
||||||
|
| mask_vert_strided)).to(device).to(dtype))
|
||||||
|
num_blocks_q = triton.cdiv(q_len, block_size)
|
||||||
|
block_mask_dense_output = (dense_to_crow_col(
|
||||||
|
block_mask_dense[-num_blocks_q:].contiguous()))
|
||||||
|
if return_dense:
|
||||||
|
mask_dense = torch.kron(
|
||||||
|
block_mask_dense,
|
||||||
|
block_mask_dense.new_ones((block_size, block_size)),
|
||||||
|
)
|
||||||
|
causal_mask = torch.tril(torch.ones(
|
||||||
|
max_seqlen, max_seqlen)).type_as(mask_dense)[-q_len:]
|
||||||
|
mask_dense = mask_dense[-q_len:, :max_seqlen] * causal_mask
|
||||||
|
return (
|
||||||
|
block_mask_dense_output,
|
||||||
|
block_mask_dense,
|
||||||
|
mask_dense,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
block_mask_dense_output,
|
||||||
|
block_mask_dense,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def binary_mask_to_bias(mask_dense: torch.Tensor):
|
||||||
|
mask_dense = 1 - mask_dense
|
||||||
|
mask_dense.masked_fill_(mask_dense.bool(), -torch.inf)
|
||||||
|
return mask_dense
|
||||||
|
|
||||||
|
|
||||||
|
def get_head_sliding_step(n_heads: int,
|
||||||
|
vert_stride: int,
|
||||||
|
homo_head: bool = False):
|
||||||
|
if homo_head:
|
||||||
|
return 0
|
||||||
|
return max(1, int(vert_stride / n_heads))
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache
|
||||||
|
def get_sparse_attn_mask(
|
||||||
|
n_heads: int,
|
||||||
|
q_len: int,
|
||||||
|
max_seqlen: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
device: torch.device,
|
||||||
|
block_size: int = 64,
|
||||||
|
local_blocks: int = 4,
|
||||||
|
vert_stride: int = 4,
|
||||||
|
homo_head: bool = True,
|
||||||
|
return_dense: bool = False,
|
||||||
|
dense_mask_type: str = "binary",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param dense_mask_type: "binary" (0 for skip token, 1 for others)
|
||||||
|
or "bias" (-inf for skip token, 0 or others)
|
||||||
|
:return: a tuple of 3:
|
||||||
|
- tuple of crow_indices, col_indices representation
|
||||||
|
of CSR format.
|
||||||
|
- block dense mask
|
||||||
|
- all token dense mask (be aware that it can be OOM if it
|
||||||
|
is too big) if `return_dense==True`, otherwise, None
|
||||||
|
"""
|
||||||
|
assert dense_mask_type in ("binary", "bias")
|
||||||
|
if homo_head:
|
||||||
|
with torch.no_grad():
|
||||||
|
(crow, col), block_mask_dense, mask_dense = (
|
||||||
|
_get_sparse_attn_mask_homo_head(
|
||||||
|
q_len,
|
||||||
|
max_seqlen,
|
||||||
|
dtype,
|
||||||
|
device,
|
||||||
|
block_size,
|
||||||
|
local_blocks,
|
||||||
|
vert_stride,
|
||||||
|
return_dense,
|
||||||
|
))
|
||||||
|
crow = crow[None].expand(n_heads, crow.shape[0])
|
||||||
|
col = col[None].expand(n_heads, col.shape[0])
|
||||||
|
if return_dense:
|
||||||
|
mask_dense = mask_dense[None].expand(n_heads,
|
||||||
|
*mask_dense.shape)
|
||||||
|
if dense_mask_type == "bias":
|
||||||
|
mask_dense = binary_mask_to_bias(mask_dense)
|
||||||
|
return (crow, col), block_mask_dense, mask_dense
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
num_blocks = triton.cdiv(max_seqlen, block_size)
|
||||||
|
q_pos = torch.arange(num_blocks)[None, :, None]
|
||||||
|
k_pos = torch.arange(num_blocks)[None, None]
|
||||||
|
head_sliding_step = get_head_sliding_step(n_heads, vert_stride)
|
||||||
|
mask_vert_strided = [
|
||||||
|
(torch.arange(num_blocks) + h * head_sliding_step + 1) %
|
||||||
|
vert_stride == 0 for h in range(n_heads)
|
||||||
|
]
|
||||||
|
mask_vert_strided = torch.vstack(mask_vert_strided).unsqueeze(1)
|
||||||
|
block_mask_dense = (((q_pos >= k_pos)
|
||||||
|
& ((q_pos - k_pos < local_blocks)
|
||||||
|
| mask_vert_strided)).to(device).to(dtype))
|
||||||
|
num_blocks_q = triton.cdiv(q_len, block_size)
|
||||||
|
block_mask_dense_output = block_mask_dense[:, -num_blocks_q:]
|
||||||
|
if return_dense:
|
||||||
|
mask_dense = torch.kron(
|
||||||
|
block_mask_dense,
|
||||||
|
block_mask_dense.new_ones((block_size, block_size)),
|
||||||
|
)
|
||||||
|
causal_mask = torch.tril(torch.ones(
|
||||||
|
max_seqlen, max_seqlen)).type_as(mask_dense)[-q_len:]
|
||||||
|
mask_dense = mask_dense[..., -q_len:, :max_seqlen] * causal_mask[None]
|
||||||
|
if dense_mask_type == "bias":
|
||||||
|
mask_dense = binary_mask_to_bias(mask_dense)
|
||||||
|
|
||||||
|
return (
|
||||||
|
dense_to_crow_col(block_mask_dense_output),
|
||||||
|
block_mask_dense,
|
||||||
|
mask_dense,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
dense_to_crow_col(block_mask_dense_output),
|
||||||
|
block_mask_dense,
|
||||||
|
None,
|
||||||
|
)
|
||||||
366
vllm/attention/ops/chunked_prefill_paged_decode.py
Normal file
366
vllm/attention/ops/chunked_prefill_paged_decode.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# Authors:
|
||||||
|
# - Burkhard Ringlein <ngl@zurich.ibm.com>
|
||||||
|
# - Jan van Lunteren <jvl@zurich.ibm.com>
|
||||||
|
# - Chih-Chieh Yang <chih.chieh.yang@ibm.com>
|
||||||
|
# - Thomas Parnell <tpa@zurich.ibm.com>
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.platforms.rocm import use_rocm_custom_paged_attention
|
||||||
|
|
||||||
|
from .prefix_prefill import context_attention_fwd
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def cdiv_fn(x, y):
|
||||||
|
return (x + y - 1) // y
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def kernel_paged_attention_2d(
|
||||||
|
output_ptr, # [num_tokens, num_query_heads, head_size]
|
||||||
|
query_ptr, # [num_tokens, num_query_heads, head_size]
|
||||||
|
key_cache_ptr, # [num_blks, num_kv_heads, head_size // x, blk_size, x]
|
||||||
|
value_cache_ptr, # [num_blks, num_kv_heads, head_size, blk_size]
|
||||||
|
block_tables_ptr, # [num_seqs, max_num_blocks_per_seq]
|
||||||
|
seq_lens_ptr, # [num_seqs]
|
||||||
|
alibi_slopes_ptr, # [num_query_heads]
|
||||||
|
scale, # float32
|
||||||
|
k_scale, # float32
|
||||||
|
v_scale, # float32
|
||||||
|
num_query_heads: tl.constexpr, # int
|
||||||
|
num_queries_per_kv: tl.constexpr, # int
|
||||||
|
num_queries_per_kv_padded: tl.constexpr, # int
|
||||||
|
block_table_stride: tl.int64, # int
|
||||||
|
query_stride_0: tl.int64, # int
|
||||||
|
query_stride_1: tl.int64, # int, should be equal to head_size
|
||||||
|
output_stride_0: tl.int64, # int
|
||||||
|
output_stride_1: tl.int64, # int, should be equal to head_size
|
||||||
|
BLOCK_SIZE: tl.constexpr, # int
|
||||||
|
HEAD_SIZE: tl.constexpr, # int
|
||||||
|
HEAD_SIZE_PADDED: tl.constexpr, # int, must be power of 2
|
||||||
|
USE_ALIBI_SLOPES: tl.constexpr, # bool
|
||||||
|
SLIDING_WINDOW: tl.constexpr, # int
|
||||||
|
x: tl.constexpr, # int
|
||||||
|
stride_k_cache_0: tl.int64, # int
|
||||||
|
stride_k_cache_1: tl.int64, # int
|
||||||
|
stride_k_cache_2: tl.int64, # int
|
||||||
|
stride_k_cache_3: tl.int64, # int
|
||||||
|
stride_k_cache_4: tl.int64, # int
|
||||||
|
stride_v_cache_0: tl.int64, # int
|
||||||
|
stride_v_cache_1: tl.int64, # int
|
||||||
|
stride_v_cache_2: tl.int64, # int
|
||||||
|
stride_v_cache_3: tl.int64, # int
|
||||||
|
filter_by_query_len: tl.constexpr, # bool
|
||||||
|
query_start_len_ptr, # [num_seqs+1]
|
||||||
|
):
|
||||||
|
seq_idx = tl.program_id(0)
|
||||||
|
kv_head_idx = tl.program_id(1)
|
||||||
|
|
||||||
|
if filter_by_query_len:
|
||||||
|
cur_batch_in_all_start_index = tl.load(query_start_len_ptr + seq_idx)
|
||||||
|
cur_batch_in_all_stop_index = tl.load(query_start_len_ptr + seq_idx +
|
||||||
|
1)
|
||||||
|
cur_batch_query_len = cur_batch_in_all_stop_index \
|
||||||
|
- cur_batch_in_all_start_index
|
||||||
|
if cur_batch_query_len > 1:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
cur_batch_in_all_start_index = seq_idx
|
||||||
|
|
||||||
|
query_head_idx = kv_head_idx * num_queries_per_kv + tl.arange(
|
||||||
|
0, num_queries_per_kv_padded)
|
||||||
|
|
||||||
|
query_offset = (cur_batch_in_all_start_index * query_stride_0 +
|
||||||
|
query_head_idx[:, None] * query_stride_1)
|
||||||
|
|
||||||
|
head_mask = query_head_idx < (kv_head_idx + 1) * num_queries_per_kv
|
||||||
|
head_mask = head_mask & (query_head_idx < num_query_heads)
|
||||||
|
|
||||||
|
dim_mask = tl.where(tl.arange(0, HEAD_SIZE_PADDED) < HEAD_SIZE, 1,
|
||||||
|
0).to(tl.int1)
|
||||||
|
|
||||||
|
# Q : (num_queries_per_kv, HEAD_SIZE,)
|
||||||
|
Q = tl.load(
|
||||||
|
query_ptr + query_offset + tl.arange(0, HEAD_SIZE_PADDED)[None, :],
|
||||||
|
mask=dim_mask[None, :] & head_mask[:, None],
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
block_table_offset = seq_idx * block_table_stride
|
||||||
|
|
||||||
|
M = tl.full([num_queries_per_kv_padded], float("-inf"), dtype=tl.float32)
|
||||||
|
L = tl.full([num_queries_per_kv_padded], 1.0, dtype=tl.float32)
|
||||||
|
acc = tl.zeros([num_queries_per_kv_padded, HEAD_SIZE_PADDED],
|
||||||
|
dtype=tl.float32)
|
||||||
|
|
||||||
|
# sequence len for this particular sequence
|
||||||
|
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
||||||
|
|
||||||
|
# alibi slope for this head
|
||||||
|
if USE_ALIBI_SLOPES:
|
||||||
|
alibi_slope = tl.load(alibi_slopes_ptr + query_head_idx,
|
||||||
|
mask=head_mask,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
num_blocks = cdiv_fn(seq_len, BLOCK_SIZE)
|
||||||
|
|
||||||
|
# iterate through tiles
|
||||||
|
for j in range(0, num_blocks):
|
||||||
|
|
||||||
|
physical_block_idx = tl.load(block_tables_ptr + block_table_offset + j)
|
||||||
|
|
||||||
|
offs_n = tl.arange(0, BLOCK_SIZE)
|
||||||
|
offs_d = tl.arange(0, HEAD_SIZE_PADDED)
|
||||||
|
|
||||||
|
v_offset = (physical_block_idx * stride_v_cache_0 +
|
||||||
|
kv_head_idx * stride_v_cache_1 +
|
||||||
|
offs_d[None, :] * stride_v_cache_2 +
|
||||||
|
offs_n[:, None] * stride_v_cache_3)
|
||||||
|
|
||||||
|
k_offset = (physical_block_idx * stride_k_cache_0 +
|
||||||
|
kv_head_idx * stride_k_cache_1 +
|
||||||
|
(offs_d[:, None] // x) * stride_k_cache_2 +
|
||||||
|
offs_n[None, :] * stride_k_cache_3 +
|
||||||
|
(offs_d[:, None] % x) * stride_k_cache_4)
|
||||||
|
|
||||||
|
# K : (HEAD_SIZE, BLOCK_SIZE)
|
||||||
|
K_load = tl.load(key_cache_ptr + k_offset,
|
||||||
|
mask=dim_mask[:, None],
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
if K_load.dtype.is_fp8():
|
||||||
|
K = (K_load.to(tl.float32) * tl.load(k_scale)).to(Q.dtype)
|
||||||
|
else:
|
||||||
|
K = K_load
|
||||||
|
|
||||||
|
# V : (BLOCK_SIZE, HEAD_SIZE)
|
||||||
|
V_load = tl.load(value_cache_ptr + v_offset,
|
||||||
|
mask=dim_mask[None, :],
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
if V_load.dtype.is_fp8():
|
||||||
|
V = (V_load.to(tl.float32) * tl.load(v_scale)).to(Q.dtype)
|
||||||
|
else:
|
||||||
|
V = V_load
|
||||||
|
|
||||||
|
seq_offset = j * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
||||||
|
boundary = tl.full([BLOCK_SIZE], seq_len, dtype=tl.int32)
|
||||||
|
seq_mask = seq_offset[None, :] < boundary
|
||||||
|
|
||||||
|
# S : (num_queries_per_kv, BLOCK_SIZE,)
|
||||||
|
S = tl.where(head_mask[:, None] & seq_mask, 0.0,
|
||||||
|
float("-inf")).to(tl.float32)
|
||||||
|
S += scale * tl.dot(Q, K)
|
||||||
|
|
||||||
|
context_len = seq_len - 1
|
||||||
|
|
||||||
|
if SLIDING_WINDOW > 0:
|
||||||
|
S = tl.where((context_len - seq_offset) < SLIDING_WINDOW, S,
|
||||||
|
-10000)
|
||||||
|
|
||||||
|
if USE_ALIBI_SLOPES:
|
||||||
|
S += alibi_slope[:, None] * (seq_offset - context_len)
|
||||||
|
|
||||||
|
# compute running maximum
|
||||||
|
# m_j : (num_queries_per_kv,)
|
||||||
|
m_j = tl.maximum(M, tl.max(S, axis=1))
|
||||||
|
|
||||||
|
# P : (num_queries_per_kv, BLOCK_SIZE,)
|
||||||
|
P = tl.exp(S - m_j[:, None])
|
||||||
|
|
||||||
|
# l_j : (num_queries_per_kv,)
|
||||||
|
l_j = tl.sum(P, axis=1)
|
||||||
|
|
||||||
|
# alpha : (num_queries_per_kv, )
|
||||||
|
alpha = tl.exp(M - m_j)
|
||||||
|
|
||||||
|
# acc : (num_queries_per_kv, BLOCK_SIZE,)
|
||||||
|
acc = acc * alpha[:, None]
|
||||||
|
|
||||||
|
# update constants
|
||||||
|
L = L * alpha + l_j
|
||||||
|
M = m_j
|
||||||
|
|
||||||
|
# acc : (num_queries_per_kv, BLOCK_SIZE,)
|
||||||
|
acc += tl.dot(P.to(V.dtype), V)
|
||||||
|
|
||||||
|
# epilogue
|
||||||
|
acc = acc / L[:, None]
|
||||||
|
|
||||||
|
output_offset = (cur_batch_in_all_start_index * output_stride_0 +
|
||||||
|
query_head_idx * output_stride_1)
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
output_ptr + output_offset[:, None] +
|
||||||
|
tl.arange(0, HEAD_SIZE_PADDED)[None, :],
|
||||||
|
acc,
|
||||||
|
mask=dim_mask[None, :] & head_mask[:, None],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def chunked_prefill_paged_decode(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
output,
|
||||||
|
kv_cache_dtype,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
block_table,
|
||||||
|
query_start_loc,
|
||||||
|
seq_lens,
|
||||||
|
max_seq_len,
|
||||||
|
max_query_len,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
alibi_slopes=None,
|
||||||
|
sliding_window=None,
|
||||||
|
sm_scale=None,
|
||||||
|
):
|
||||||
|
|
||||||
|
if sm_scale is None:
|
||||||
|
sm_scale = 1.0 / (query.shape[1]**0.5)
|
||||||
|
|
||||||
|
use_alibi_slopes = alibi_slopes is not None
|
||||||
|
|
||||||
|
if sliding_window is None or sliding_window <= 0:
|
||||||
|
sliding_window = 0
|
||||||
|
|
||||||
|
if max_query_len > 1:
|
||||||
|
context_attention_fwd(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
o=output,
|
||||||
|
kv_cache_dtype=kv_cache_dtype,
|
||||||
|
k_cache=key_cache,
|
||||||
|
v_cache=value_cache,
|
||||||
|
b_loc=block_table,
|
||||||
|
b_start_loc=query_start_loc,
|
||||||
|
b_seq_len=seq_lens,
|
||||||
|
max_seq_len=max_seq_len,
|
||||||
|
max_input_len=max_query_len,
|
||||||
|
k_scale=k_scale,
|
||||||
|
v_scale=v_scale,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
sliding_window=sliding_window,
|
||||||
|
sm_scale=sm_scale,
|
||||||
|
skip_decode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
num_seqs = len(seq_lens)
|
||||||
|
num_query_heads = query.shape[1]
|
||||||
|
num_kv_heads = key.shape[1]
|
||||||
|
num_queries_per_kv = query.shape[1] // key.shape[1]
|
||||||
|
head_size = query.shape[2]
|
||||||
|
|
||||||
|
# Conversion of FP8 Tensor from uint8 storage to
|
||||||
|
# appropriate torch.dtype for interpretation by Triton
|
||||||
|
if "fp8" in kv_cache_dtype:
|
||||||
|
assert key_cache.dtype == torch.uint8
|
||||||
|
assert value_cache.dtype == torch.uint8
|
||||||
|
|
||||||
|
if kv_cache_dtype in ("fp8", "fp8_e4m3"):
|
||||||
|
target_dtype = torch.float8_e4m3fn
|
||||||
|
elif kv_cache_dtype == "fp8_e5m2":
|
||||||
|
target_dtype = torch.float8_e5m2
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported FP8 dtype:", kv_cache_dtype)
|
||||||
|
|
||||||
|
key_cache = key_cache.view(target_dtype)
|
||||||
|
value_cache = value_cache.view(target_dtype)
|
||||||
|
|
||||||
|
num_queries_per_kv_padded = max(triton.next_power_of_2(num_queries_per_kv),
|
||||||
|
16)
|
||||||
|
|
||||||
|
use_custom = use_rocm_custom_paged_attention(query.dtype, head_size,
|
||||||
|
block_size,
|
||||||
|
num_queries_per_kv,
|
||||||
|
max_seq_len, sliding_window)
|
||||||
|
if use_custom:
|
||||||
|
_PARTITION_SIZE_ROCM = 256
|
||||||
|
max_num_partitions = ((max_seq_len + _PARTITION_SIZE_ROCM - 1) //
|
||||||
|
_PARTITION_SIZE_ROCM)
|
||||||
|
assert _PARTITION_SIZE_ROCM % block_size == 0
|
||||||
|
total_num_seq = query.shape[0]
|
||||||
|
tmp_output = torch.empty(
|
||||||
|
size=(total_num_seq, num_query_heads, max_num_partitions,
|
||||||
|
head_size),
|
||||||
|
dtype=output.dtype,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
exp_sums = torch.empty(
|
||||||
|
size=(total_num_seq, num_query_heads, max_num_partitions),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
max_logits = torch.empty_like(exp_sums)
|
||||||
|
|
||||||
|
ops.paged_attention_rocm(
|
||||||
|
output,
|
||||||
|
exp_sums,
|
||||||
|
max_logits,
|
||||||
|
tmp_output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
num_kv_heads,
|
||||||
|
scale=sm_scale,
|
||||||
|
block_tables=block_table,
|
||||||
|
seq_lens=seq_lens,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
block_size=block_size,
|
||||||
|
max_seq_len=max_seq_len,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
kv_cache_dtype=kv_cache_dtype,
|
||||||
|
k_scale=k_scale,
|
||||||
|
v_scale=v_scale,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
kernel_paged_attention_2d[(
|
||||||
|
num_seqs,
|
||||||
|
num_kv_heads,
|
||||||
|
)](
|
||||||
|
output_ptr=output,
|
||||||
|
query_ptr=query,
|
||||||
|
key_cache_ptr=key_cache,
|
||||||
|
value_cache_ptr=value_cache,
|
||||||
|
block_tables_ptr=block_table,
|
||||||
|
seq_lens_ptr=seq_lens,
|
||||||
|
alibi_slopes_ptr=alibi_slopes,
|
||||||
|
scale=sm_scale,
|
||||||
|
k_scale=k_scale,
|
||||||
|
v_scale=v_scale,
|
||||||
|
num_query_heads=num_query_heads,
|
||||||
|
num_queries_per_kv=num_queries_per_kv,
|
||||||
|
num_queries_per_kv_padded=num_queries_per_kv_padded,
|
||||||
|
block_table_stride=block_table.stride(0),
|
||||||
|
query_stride_0=query.stride(0),
|
||||||
|
query_stride_1=query.stride(1),
|
||||||
|
output_stride_0=output.stride(0),
|
||||||
|
output_stride_1=output.stride(1),
|
||||||
|
BLOCK_SIZE=block_size,
|
||||||
|
HEAD_SIZE=head_size,
|
||||||
|
HEAD_SIZE_PADDED=triton.next_power_of_2(head_size),
|
||||||
|
USE_ALIBI_SLOPES=use_alibi_slopes,
|
||||||
|
SLIDING_WINDOW=sliding_window,
|
||||||
|
x=key_cache.shape[4],
|
||||||
|
stride_k_cache_0=key_cache.stride(0),
|
||||||
|
stride_k_cache_1=key_cache.stride(1),
|
||||||
|
stride_k_cache_2=key_cache.stride(2),
|
||||||
|
stride_k_cache_3=key_cache.stride(3),
|
||||||
|
stride_k_cache_4=key_cache.stride(4),
|
||||||
|
stride_v_cache_0=value_cache.stride(0),
|
||||||
|
stride_v_cache_1=value_cache.stride(1),
|
||||||
|
stride_v_cache_2=value_cache.stride(2),
|
||||||
|
stride_v_cache_3=value_cache.stride(3),
|
||||||
|
filter_by_query_len=True,
|
||||||
|
query_start_len_ptr=query_start_loc,
|
||||||
|
)
|
||||||
115
vllm/attention/ops/flashmla.py
Normal file
115
vllm/attention/ops/flashmla.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# adapted from: https://github.com/deepseek-ai/FlashMLA/blob/main/flash_mla/flash_mla_interface.py
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
if current_platform.is_cuda():
|
||||||
|
try:
|
||||||
|
import vllm._flashmla_C # noqa: F401
|
||||||
|
_flashmla_C_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_flashmla_C_AVAILABLE = False
|
||||||
|
else:
|
||||||
|
_flashmla_C_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def is_flashmla_supported() -> Tuple[bool, Optional[str]]:
|
||||||
|
"""
|
||||||
|
Return: is_supported_flag, unsupported_reason (optional).
|
||||||
|
"""
|
||||||
|
if not current_platform.is_cuda():
|
||||||
|
return False, "FlashMLA is only supported on CUDA devices."
|
||||||
|
if current_platform.get_device_capability()[0] != 9:
|
||||||
|
return False, "FlashMLA is only supported on Hopper devices."
|
||||||
|
if not _flashmla_C_AVAILABLE:
|
||||||
|
return False, "vllm._flashmla_C is not available, likely was not "\
|
||||||
|
"compiled due to insufficient nvcc version or a supported arch "\
|
||||||
|
"(only sm90a currently) was not in the list of target arches to "\
|
||||||
|
"compile for."
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_mla_metadata(
|
||||||
|
cache_seqlens: torch.Tensor,
|
||||||
|
num_heads_per_head_k: int,
|
||||||
|
num_heads_k: int,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
cache_seqlens: (batch_size), dtype torch.int32.
|
||||||
|
num_heads_per_head_k: Equals to seq_len_q * num_heads_q // num_heads_k.
|
||||||
|
num_heads_k: num_heads_k.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
tile_scheduler_metadata: (num_sm_parts, TileSchedulerMetaDataSize),
|
||||||
|
dtype torch.int32.
|
||||||
|
num_splits: (batch_size + 1), dtype torch.int32.
|
||||||
|
"""
|
||||||
|
return torch.ops._flashmla_C.get_mla_metadata(cache_seqlens,
|
||||||
|
num_heads_per_head_k,
|
||||||
|
num_heads_k)
|
||||||
|
|
||||||
|
|
||||||
|
def flash_mla_with_kvcache(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k_cache: torch.Tensor,
|
||||||
|
block_table: torch.Tensor,
|
||||||
|
cache_seqlens: torch.Tensor,
|
||||||
|
head_dim_v: int,
|
||||||
|
tile_scheduler_metadata: torch.Tensor,
|
||||||
|
num_splits: torch.Tensor,
|
||||||
|
softmax_scale: Optional[float] = None,
|
||||||
|
causal: bool = False,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""
|
||||||
|
Arguments:
|
||||||
|
q: (batch_size, seq_len_q, num_heads_q, head_dim).
|
||||||
|
k_cache: (num_blocks, page_block_size, num_heads_k, head_dim).
|
||||||
|
block_table: (batch_size, max_num_blocks_per_seq), torch.int32.
|
||||||
|
cache_seqlens: (batch_size), torch.int32.
|
||||||
|
head_dim_v: Head_dim of v.
|
||||||
|
tile_scheduler_metadata: (num_sm_parts, TileSchedulerMetaDataSize),
|
||||||
|
torch.int32, return by get_mla_metadata.
|
||||||
|
num_splits: (batch_size + 1), torch.int32, return by get_mla_metadata.
|
||||||
|
softmax_scale: float. The scaling of QK^T before applying softmax.
|
||||||
|
Default to 1 / sqrt(head_dim).
|
||||||
|
causal: bool. Whether to apply causal attention mask.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
out: (batch_size, seq_len_q, num_heads_q, head_dim_v).
|
||||||
|
softmax_lse: (batch_size, num_heads_q, seq_len_q), torch.float32.
|
||||||
|
"""
|
||||||
|
if softmax_scale is None:
|
||||||
|
softmax_scale = q.shape[-1]**(-0.5)
|
||||||
|
out, softmax_lse = torch.ops._flashmla_C.fwd_kvcache_mla(
|
||||||
|
q,
|
||||||
|
k_cache,
|
||||||
|
None,
|
||||||
|
head_dim_v,
|
||||||
|
cache_seqlens,
|
||||||
|
block_table,
|
||||||
|
softmax_scale,
|
||||||
|
causal,
|
||||||
|
tile_scheduler_metadata,
|
||||||
|
num_splits,
|
||||||
|
)
|
||||||
|
return out, softmax_lse
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# TODO: Add fake functions
|
||||||
|
#
|
||||||
|
# @register_fake("_flashmla_C::get_mla_metadata")
|
||||||
|
# def _get_mla_metadata_fake(....) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
# return ....
|
||||||
|
#
|
||||||
|
# @register_fake("_flashmla_C::fwd_kvcache_mla")
|
||||||
|
# def _fwd_kvcache_mla_fake(....) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
# return ....
|
||||||
|
#
|
||||||
106
vllm/attention/ops/hpu_paged_attn.py
Normal file
106
vllm/attention/ops/hpu_paged_attn.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Copyright (C) 2024 Habana Labs, Ltd. an Intel Company
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from vllm_hpu_extension import cache_ops, ops
|
||||||
|
|
||||||
|
# Should be the same as PARTITION_SIZE in `paged_attention_v2_launcher`.
|
||||||
|
_PARTITION_SIZE = 512
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HPUPagedAttentionMetadata:
|
||||||
|
"""Metadata for PagedAttention."""
|
||||||
|
block_list: Optional[torch.Tensor]
|
||||||
|
block_mapping: Optional[torch.Tensor]
|
||||||
|
block_usage: Optional[torch.Tensor]
|
||||||
|
block_indices: Optional[torch.Tensor]
|
||||||
|
block_offsets: Optional[torch.Tensor]
|
||||||
|
block_scales: Optional[torch.Tensor]
|
||||||
|
block_groups: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
|
||||||
|
class HPUPagedAttention:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [64, 80, 96, 112, 128, 256]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (num_blocks, block_size, num_kv_heads, head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_kv_cache(
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
return key_cache, value_cache
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_to_paged_cache(key: torch.Tensor, value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor, kv_cache_dtype: str,
|
||||||
|
is_prompt: bool) -> None:
|
||||||
|
cache_ops.reshape_and_cache(key, value, key_cache, value_cache,
|
||||||
|
slot_mapping, kv_cache_dtype, is_prompt)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_decode(**kwargs) -> torch.Tensor:
|
||||||
|
return ops.flat_pa(**kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_prefix(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
subquery_start_loc: torch.Tensor,
|
||||||
|
seq_lens_tensor: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
max_query_len: int,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
) -> torch.Tensor:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"forward_prefix is not implemented for HPUPagedAttention")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: Dict[int, int],
|
||||||
|
) -> None:
|
||||||
|
src_key_cache = src_kv_cache[0]
|
||||||
|
dst_key_cache = dst_kv_cache[0]
|
||||||
|
cache_ops.swap_blocks(src_key_cache, dst_key_cache, src_to_dst)
|
||||||
|
|
||||||
|
src_value_cache = src_kv_cache[1]
|
||||||
|
dst_value_cache = dst_kv_cache[1]
|
||||||
|
cache_ops.swap_blocks(src_value_cache, dst_value_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: Dict[int, List[int]],
|
||||||
|
) -> None:
|
||||||
|
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||||||
|
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||||||
|
cache_ops.copy_blocks(key_caches, value_caches, src_to_dists)
|
||||||
193
vllm/attention/ops/ipex_attn.py
Normal file
193
vllm/attention/ops/ipex_attn.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
import intel_extension_for_pytorch.llm.modules as ipex_modules
|
||||||
|
_use_ipex = True
|
||||||
|
except ImportError:
|
||||||
|
_use_ipex = False
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
|
||||||
|
|
||||||
|
class _PagedAttention:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [32, 64, 80, 96, 112, 128, 192, 256]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
*args,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (2, num_blocks, block_size * num_kv_heads * head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_kv_cache(
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
*args,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
x = 16 // kv_cache.element_size()
|
||||||
|
num_blocks = kv_cache.shape[1]
|
||||||
|
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
key_cache = key_cache.view(num_blocks, num_kv_heads, head_size // x,
|
||||||
|
-1, x)
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
value_cache = value_cache.view(num_blocks, num_kv_heads, head_size, -1)
|
||||||
|
return key_cache, value_cache
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_to_paged_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
|
ops.reshape_and_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
slot_mapping.flatten(),
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_decode(
|
||||||
|
output: torch.Tensor,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
max_context_len: int,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
|
tp_rank: int = 0
|
||||||
|
blocksparse_local_blocks: int = 0
|
||||||
|
blocksparse_vert_stride: int = 0
|
||||||
|
blocksparse_block_size: int = 64
|
||||||
|
blocksparse_head_sliding_step: int = 0
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
|
||||||
|
ops.paged_attention_v1(
|
||||||
|
output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
num_kv_heads,
|
||||||
|
scale,
|
||||||
|
block_tables,
|
||||||
|
context_lens,
|
||||||
|
block_size,
|
||||||
|
max_context_len,
|
||||||
|
alibi_slopes,
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
tp_rank,
|
||||||
|
blocksparse_local_blocks,
|
||||||
|
blocksparse_vert_stride,
|
||||||
|
blocksparse_block_size,
|
||||||
|
blocksparse_head_sliding_step,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: Dict[int, List[int]],
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
|
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||||||
|
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||||||
|
ops.copy_blocks(key_caches, value_caches, src_to_dists)
|
||||||
|
|
||||||
|
|
||||||
|
class _IPEXPagedAttention(_PagedAttention):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_kv_cache(
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
*args,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
num_blocks = kv_cache.shape[1]
|
||||||
|
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
key_cache = key_cache.view(num_blocks, num_kv_heads, -1, head_size)
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
value_cache = value_cache.view(num_blocks, num_kv_heads, -1, head_size)
|
||||||
|
return key_cache, value_cache
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_to_paged_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
|
ipex_modules.PagedAttention.reshape_and_cache(
|
||||||
|
key, value, key_cache, value_cache,
|
||||||
|
slot_mapping.flatten().int())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_decode(
|
||||||
|
output: torch.Tensor,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
context_lens: torch.Tensor,
|
||||||
|
max_context_len: int,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
|
block_size = value_cache.shape[2]
|
||||||
|
head_mapping = torch.arange(
|
||||||
|
0,
|
||||||
|
num_kv_heads,
|
||||||
|
device="cpu",
|
||||||
|
dtype=torch.int32,
|
||||||
|
).view(num_kv_heads,
|
||||||
|
1).repeat_interleave(query.size(1) // num_kv_heads).flatten()
|
||||||
|
ipex_modules.PagedAttention.single_query_cached_kv_attention(
|
||||||
|
output, query.contiguous(), key_cache, value_cache, head_mapping,
|
||||||
|
scale, block_tables, context_lens, block_size, max_context_len,
|
||||||
|
alibi_slopes)
|
||||||
|
|
||||||
|
|
||||||
|
PagedAttention = _IPEXPagedAttention if _use_ipex else _PagedAttention
|
||||||
905
vllm/attention/ops/nki_flash_attn.py
Normal file
905
vllm/attention/ops/nki_flash_attn.py
Normal file
@@ -0,0 +1,905 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import neuronxcc.nki.isa as nisa
|
||||||
|
import neuronxcc.nki.language as nl
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from neuronxcc import nki
|
||||||
|
from neuronxcc.nki.language import par_dim
|
||||||
|
|
||||||
|
|
||||||
|
def ceil_div(a, b):
|
||||||
|
return (a + b - 1) // b
|
||||||
|
|
||||||
|
|
||||||
|
def is_power_of_2(x):
|
||||||
|
return x > 0 and (x & (x - 1)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def load_block_tables(block_tables_hbm, num_tiles, num_blocks_per_tile):
|
||||||
|
"""
|
||||||
|
Load block tables from HBM into SRAM
|
||||||
|
|
||||||
|
`block_tables_hbm` has shape `(num_tiles * num_blocks_per_tile, )`.
|
||||||
|
In case `num_tiles > B_P_SIZE`, we need further tile `num_tile` dimension.
|
||||||
|
"""
|
||||||
|
B_P_SIZE = 128
|
||||||
|
|
||||||
|
# reshape as `(num_tiles, num_blocks_per_tile)`
|
||||||
|
assert len(block_tables_hbm.shape) == 1
|
||||||
|
(num_total_blocks, ) = block_tables_hbm.shape
|
||||||
|
assert num_blocks_per_tile * num_tiles == num_total_blocks
|
||||||
|
block_tables_hbm = block_tables_hbm.reshape(
|
||||||
|
(num_tiles, num_blocks_per_tile))
|
||||||
|
|
||||||
|
block_tables_sbuf = nl.zeros(
|
||||||
|
(ceil_div(num_tiles,
|
||||||
|
B_P_SIZE), par_dim(B_P_SIZE), num_blocks_per_tile),
|
||||||
|
dtype=nl.int32,
|
||||||
|
)
|
||||||
|
for i in nl.affine_range(ceil_div(num_tiles, B_P_SIZE)):
|
||||||
|
i_p = nl.arange(B_P_SIZE)[:, None]
|
||||||
|
i_f = nl.arange(num_blocks_per_tile)[None, :]
|
||||||
|
block_tables_sbuf[i, i_p, i_f] = nl.load(
|
||||||
|
block_tables_hbm[i_p + i * B_P_SIZE, i_f],
|
||||||
|
dtype=nl.int32,
|
||||||
|
mask=(i_p + i * B_P_SIZE < num_tiles),
|
||||||
|
)
|
||||||
|
return block_tables_sbuf
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def transform_block_tables_for_indirect_load(
|
||||||
|
block_tables,
|
||||||
|
block_size_tiling_factor,
|
||||||
|
num_head,
|
||||||
|
head_id,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
This function does two things:
|
||||||
|
1. calculate new `block_tables` for a `head_id` after flattening
|
||||||
|
`num_block`, `num_head`, and `block_size_tiling_factor` dimensions
|
||||||
|
2. transpose the result so that `block_table` for each tile is mapped to
|
||||||
|
SBUF Partition dimension for vectorized DMA
|
||||||
|
|
||||||
|
Tiling trick to further improve DMA performance:
|
||||||
|
Given KV cache shape `(num_block, num_head, block_size, D)`, when loading M
|
||||||
|
blocks of a given `head_id` from HBM, the load `cache[block_tables,
|
||||||
|
head_id]` has shape `(M, block_size, D)`. If M < B_P_SIZE = 128, DMA may not
|
||||||
|
fully utilize hardware parallelization. The solution is to tile `block_size`
|
||||||
|
into `(block_size_tiling_factor, tiled_block_size)` s.t. `M *
|
||||||
|
block_size_tiling_factor = B_P_SIZE`. After tiling, KV cache has shape
|
||||||
|
`(num_block, num_head, block_size_tiling_factor, tiled_block_size, D)`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
We don't further tile D dimension as small DMA size also hurts performance.
|
||||||
|
"""
|
||||||
|
B_P_SIZE = 128
|
||||||
|
num_partitions, num_tiles_per_partition, num_blocks_per_tile = (
|
||||||
|
block_tables.shape)
|
||||||
|
assert num_tiles_per_partition == B_P_SIZE
|
||||||
|
assert is_power_of_2(
|
||||||
|
num_blocks_per_tile), f"{num_blocks_per_tile=} is not power of 2"
|
||||||
|
|
||||||
|
num_loads = ceil_div(num_blocks_per_tile, B_P_SIZE)
|
||||||
|
block_tables_transposed = nl.ndarray(
|
||||||
|
(
|
||||||
|
num_loads,
|
||||||
|
par_dim(B_P_SIZE),
|
||||||
|
num_partitions * num_tiles_per_partition,
|
||||||
|
),
|
||||||
|
dtype=nl.int32,
|
||||||
|
)
|
||||||
|
|
||||||
|
# prepare iota ahead of time to avoid repeatedly using Gpsimd
|
||||||
|
if num_head > 1:
|
||||||
|
head_id = nisa.iota(head_id, dtype=nl.int32).reshape((1, 1))
|
||||||
|
head_id = nl.transpose(
|
||||||
|
head_id.broadcast_to((1, num_tiles_per_partition)))
|
||||||
|
if num_blocks_per_tile > 1:
|
||||||
|
head_id = head_id.broadcast_to(
|
||||||
|
(num_tiles_per_partition, num_blocks_per_tile))
|
||||||
|
|
||||||
|
if block_size_tiling_factor > 1:
|
||||||
|
broadcast_shape = (
|
||||||
|
num_tiles_per_partition,
|
||||||
|
num_blocks_per_tile,
|
||||||
|
block_size_tiling_factor,
|
||||||
|
)
|
||||||
|
offset = nisa.iota(nl.arange(block_size_tiling_factor)[None, None, :],
|
||||||
|
dtype=nl.int32).broadcast_to(broadcast_shape)
|
||||||
|
|
||||||
|
for partition_id in nl.affine_range(num_partitions):
|
||||||
|
block_tables_partition = block_tables[partition_id]
|
||||||
|
if num_head > 1:
|
||||||
|
# fuse num_block and num_head dimension
|
||||||
|
block_tables_partition = block_tables_partition * num_head + head_id
|
||||||
|
|
||||||
|
if block_size_tiling_factor > 1:
|
||||||
|
# need to apply block size tiling trick
|
||||||
|
assert num_blocks_per_tile * block_size_tiling_factor == B_P_SIZE
|
||||||
|
block_tables_partition = ((block_tables_partition *
|
||||||
|
block_size_tiling_factor).reshape(
|
||||||
|
(num_tiles_per_partition,
|
||||||
|
num_blocks_per_tile,
|
||||||
|
1)).broadcast_to(broadcast_shape))
|
||||||
|
new_block_tables = block_tables_partition + offset
|
||||||
|
new_block_tables = new_block_tables.reshape(
|
||||||
|
(num_tiles_per_partition, B_P_SIZE))
|
||||||
|
else:
|
||||||
|
new_block_tables = block_tables_partition
|
||||||
|
|
||||||
|
# transpose the block table so that it can be used by vector DGE
|
||||||
|
for i in nl.affine_range(num_loads):
|
||||||
|
i_p = nl.arange(B_P_SIZE)[:, None]
|
||||||
|
i_f = (partition_id * num_tiles_per_partition +
|
||||||
|
nl.arange(num_tiles_per_partition)[None, :])
|
||||||
|
block_tables_transposed[i, i_p, i_f] = nl.transpose(
|
||||||
|
new_block_tables[:, nl.ds(i * B_P_SIZE, B_P_SIZE)])
|
||||||
|
return block_tables_transposed
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def load_kv_tile_from_cache(
|
||||||
|
cur_k_tile,
|
||||||
|
cur_v_tile,
|
||||||
|
kv_cache,
|
||||||
|
block_tables,
|
||||||
|
large_k_tile_idx,
|
||||||
|
num_blocks_per_large_tile,
|
||||||
|
tiled_block_size,
|
||||||
|
B_P_SIZE,
|
||||||
|
B_D_SIZE,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Load KV cache and transform Key and Value into layout required by Matmul
|
||||||
|
|
||||||
|
Vectorized DMA Load layout:
|
||||||
|
Key and Value: (par_dim(B_P_SIZE), seqlen_kv // B_P_SIZE * B_D_SIZE)
|
||||||
|
|
||||||
|
Layout used by attention matmuls:
|
||||||
|
Key: (par_dim(B_D_SIZE), seqlen_kv)
|
||||||
|
Value: (seqlen_kv // B_P_SIZE, par_dim(B_P_SIZE), B_D_SIZE)
|
||||||
|
equivalent to (par_dim(B_P_SIZE), seqlen_kv // B_P_SIZE * B_D_SIZE)
|
||||||
|
"""
|
||||||
|
# load key cache
|
||||||
|
num_loads = ceil_div(num_blocks_per_large_tile, B_P_SIZE)
|
||||||
|
for load_idx in nl.affine_range(num_loads):
|
||||||
|
i_p = nl.arange(B_P_SIZE)[:, None]
|
||||||
|
i_f = nl.arange(tiled_block_size * B_D_SIZE)[None, :]
|
||||||
|
loaded = nl.load(kv_cache[0, block_tables[load_idx, i_p,
|
||||||
|
large_k_tile_idx], i_f])
|
||||||
|
if cur_k_tile.dtype != loaded.dtype:
|
||||||
|
loaded = nl.copy(loaded, dtype=cur_k_tile.dtype)
|
||||||
|
# Transpose SBUF tensor using PE
|
||||||
|
for tb_i in nl.affine_range(tiled_block_size):
|
||||||
|
cur_k_tile[
|
||||||
|
:,
|
||||||
|
nl.ds(
|
||||||
|
load_idx * B_P_SIZE * tiled_block_size + tb_i * B_P_SIZE,
|
||||||
|
B_P_SIZE,
|
||||||
|
),
|
||||||
|
] = nl.transpose(loaded[:, nl.ds(tb_i * B_D_SIZE, B_D_SIZE)])
|
||||||
|
|
||||||
|
# load value cache
|
||||||
|
for load_idx in nl.affine_range(num_loads):
|
||||||
|
loaded = nl.load(kv_cache[1, block_tables[load_idx, i_p,
|
||||||
|
large_k_tile_idx], i_f])
|
||||||
|
if cur_v_tile.dtype != loaded.dtype:
|
||||||
|
loaded = nl.copy(loaded, dtype=cur_v_tile.dtype)
|
||||||
|
i_p = nl.arange(B_P_SIZE)[:, None]
|
||||||
|
i_f = nl.arange(tiled_block_size * B_D_SIZE)[None, :]
|
||||||
|
cur_v_tile[
|
||||||
|
:,
|
||||||
|
nl.ds(
|
||||||
|
load_idx * tiled_block_size * B_D_SIZE,
|
||||||
|
tiled_block_size * B_D_SIZE,
|
||||||
|
),
|
||||||
|
] = loaded
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def transpose_p_local(p_local_transposed,
|
||||||
|
p_local,
|
||||||
|
LARGE_TILE_SZ,
|
||||||
|
B_F_SIZE=512):
|
||||||
|
for i in nl.affine_range(LARGE_TILE_SZ // B_F_SIZE):
|
||||||
|
if nisa.get_nc_version() == nisa.nc_version.gen3:
|
||||||
|
p_local_t_tmp = nl.ndarray((par_dim(128), B_F_SIZE),
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
dtype=p_local.dtype)
|
||||||
|
else:
|
||||||
|
p_local_t_tmp = nl.ndarray((par_dim(128), B_F_SIZE),
|
||||||
|
buffer=nl.psum,
|
||||||
|
dtype=np.float32)
|
||||||
|
|
||||||
|
for j in nl.affine_range(B_F_SIZE // 128):
|
||||||
|
j_128_slice = nl.ds(j * 128, 128)
|
||||||
|
i_j_128_slice = nl.ds(i * B_F_SIZE + j * 128, 128)
|
||||||
|
|
||||||
|
if nisa.get_nc_version() == nisa.nc_version.gen3:
|
||||||
|
p_local_t_tmp[:, j_128_slice] = nisa.dma_transpose(
|
||||||
|
p_local[:, i_j_128_slice])
|
||||||
|
else:
|
||||||
|
p_local_t_tmp[:, j_128_slice] = nisa.nc_transpose(
|
||||||
|
p_local[:, i_j_128_slice])
|
||||||
|
|
||||||
|
p_local_transposed[:, nl.ds(i * B_F_SIZE, B_F_SIZE)] = nl.copy(
|
||||||
|
p_local_t_tmp, dtype=p_local_transposed.dtype)
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def _flash_attention_core(
|
||||||
|
q_local_tile,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
o_buffer,
|
||||||
|
l_buffer,
|
||||||
|
m_buffer,
|
||||||
|
kernel_dtype,
|
||||||
|
acc_type,
|
||||||
|
tile_mask,
|
||||||
|
use_causal_mask,
|
||||||
|
q_tile_idx=None,
|
||||||
|
initialize=False,
|
||||||
|
LARGE_TILE_SZ=2048,
|
||||||
|
B_P_SIZE=128,
|
||||||
|
B_F_SIZE=512,
|
||||||
|
B_D_SIZE=128,
|
||||||
|
qk_res_buffer=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
The flash attention core function to calculate self attention between a tile
|
||||||
|
of q and a block of K and V.
|
||||||
|
The q_local_tile has (B_P_SIZE, B_D_SIZE)
|
||||||
|
The K and V have shape (B_D_SIZE, LARGE_TILE_SZ), whose free dimension will
|
||||||
|
be split into size B_F_SIZE tiles
|
||||||
|
|
||||||
|
The results are stored in the following three buffers
|
||||||
|
o_buffer: (B_P_SIZE, d)
|
||||||
|
l_buffer: (B_P_SIZE, 1)
|
||||||
|
m_buffer: (B_P_SIZE, 1)
|
||||||
|
|
||||||
|
All IO buffers are in SBUF.
|
||||||
|
"""
|
||||||
|
num_k_tile_per_large_tile = LARGE_TILE_SZ // B_F_SIZE
|
||||||
|
|
||||||
|
qk_res_buf = nl.ndarray((par_dim(B_P_SIZE), LARGE_TILE_SZ),
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
dtype=acc_type)
|
||||||
|
max_local = nl.ndarray((par_dim(B_P_SIZE), num_k_tile_per_large_tile),
|
||||||
|
dtype=acc_type)
|
||||||
|
for k_i in nl.affine_range(num_k_tile_per_large_tile):
|
||||||
|
k_i_b_f_slice = nl.ds(k_i * B_F_SIZE, B_F_SIZE)
|
||||||
|
|
||||||
|
if use_causal_mask:
|
||||||
|
# mask are used to only apply computation to the lower half of the
|
||||||
|
# matrix, which reduce the arithmetic intensity by up to 50%
|
||||||
|
multiplication_required_selection = (q_tile_idx * B_P_SIZE
|
||||||
|
>= k_i * B_F_SIZE)
|
||||||
|
else:
|
||||||
|
multiplication_required_selection = True
|
||||||
|
|
||||||
|
if multiplication_required_selection:
|
||||||
|
qk_psum = nl.ndarray((par_dim(B_P_SIZE), B_F_SIZE),
|
||||||
|
dtype=np.float32,
|
||||||
|
buffer=nl.psum) # (128, 512)
|
||||||
|
qk_psum[:, :] = nl.matmul(q_local_tile,
|
||||||
|
k[:, k_i_b_f_slice],
|
||||||
|
transpose_x=True) # (p(128), 512)
|
||||||
|
qk_res_buf[:, k_i_b_f_slice] = nl.where(
|
||||||
|
tile_mask[:, k_i_b_f_slice],
|
||||||
|
qk_psum[:, nl.ds(0, B_F_SIZE)],
|
||||||
|
-9984.0,
|
||||||
|
dtype=acc_type,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
qk_res_buf[:, k_i_b_f_slice] = -9984.0
|
||||||
|
|
||||||
|
# Calculate max of the current tile
|
||||||
|
max_local[:, k_i] = nisa.tensor_reduce(
|
||||||
|
np.max,
|
||||||
|
qk_res_buf[:, k_i_b_f_slice],
|
||||||
|
axis=(1, ),
|
||||||
|
dtype=acc_type,
|
||||||
|
negate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if qk_res_buffer is not None:
|
||||||
|
qk_res_buffer[:, :] = nl.copy(qk_res_buf[:, :])
|
||||||
|
|
||||||
|
max_ = nisa.tensor_reduce(
|
||||||
|
np.max,
|
||||||
|
max_local[:, :],
|
||||||
|
axis=(1, ),
|
||||||
|
dtype=acc_type,
|
||||||
|
negate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
o_previous_scaled = nl.ndarray((par_dim(B_P_SIZE), B_D_SIZE),
|
||||||
|
dtype=o_buffer.dtype)
|
||||||
|
|
||||||
|
if initialize:
|
||||||
|
m_buffer[:, 0] = nl.copy(max_)
|
||||||
|
m_current = max_
|
||||||
|
else:
|
||||||
|
m_previous = nl.copy(m_buffer[:, 0])
|
||||||
|
m_buffer[:, 0] = nl.maximum(m_previous, max_) # (128,1)
|
||||||
|
|
||||||
|
m_current = m_buffer[:, 0]
|
||||||
|
# Compute scaling factor
|
||||||
|
alpha = nisa.activation(
|
||||||
|
np.exp,
|
||||||
|
m_previous,
|
||||||
|
bias=-1 * m_current,
|
||||||
|
scale=1.0,
|
||||||
|
)
|
||||||
|
o_previous_scaled[...] = nl.multiply(o_buffer[:, :], alpha)
|
||||||
|
|
||||||
|
p_local = nl.ndarray((par_dim(B_P_SIZE), LARGE_TILE_SZ),
|
||||||
|
dtype=kernel_dtype)
|
||||||
|
REDUCTION_TILE = min(2048, LARGE_TILE_SZ // 2)
|
||||||
|
|
||||||
|
p_partial_sum = nl.ndarray(
|
||||||
|
(par_dim(B_P_SIZE), LARGE_TILE_SZ // REDUCTION_TILE),
|
||||||
|
dtype=acc_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
for k_r_i in nl.affine_range(LARGE_TILE_SZ // REDUCTION_TILE):
|
||||||
|
k_r_i_reduce_slice = nl.ds(k_r_i * REDUCTION_TILE, REDUCTION_TILE)
|
||||||
|
|
||||||
|
# compute exp(qk - max)
|
||||||
|
# Compute partial row - tile sum of exp(qk - max))
|
||||||
|
# FIXME : Use activation accumulate to accumulate over k_r_i loop ?
|
||||||
|
p_local[:, k_r_i_reduce_slice] = nisa.activation_reduce(
|
||||||
|
np.exp,
|
||||||
|
qk_res_buf[:, k_r_i_reduce_slice],
|
||||||
|
bias=-1 * m_current,
|
||||||
|
scale=1.0,
|
||||||
|
reduce_op=nl.add,
|
||||||
|
reduce_res=p_partial_sum[:, k_r_i],
|
||||||
|
dtype=kernel_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
ps = nl.sum(p_partial_sum, axis=1, dtype=acc_type)
|
||||||
|
|
||||||
|
p_local_transposed = nl.ndarray((par_dim(B_P_SIZE), LARGE_TILE_SZ),
|
||||||
|
dtype=kernel_dtype)
|
||||||
|
transpose_p_local(
|
||||||
|
p_local_transposed=p_local_transposed,
|
||||||
|
p_local=p_local,
|
||||||
|
LARGE_TILE_SZ=LARGE_TILE_SZ,
|
||||||
|
B_F_SIZE=B_F_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
pv_psum = nl.zeros(
|
||||||
|
(par_dim(B_P_SIZE), B_D_SIZE),
|
||||||
|
dtype=np.float32,
|
||||||
|
buffer=nl.psum,
|
||||||
|
)
|
||||||
|
for k_i in nl.affine_range(LARGE_TILE_SZ // B_P_SIZE):
|
||||||
|
pv_psum[:, :] += nl.matmul(
|
||||||
|
p_local_transposed[:, nl.ds(k_i * B_P_SIZE, B_P_SIZE)],
|
||||||
|
v[:, nl.ds(k_i * B_D_SIZE, B_D_SIZE)],
|
||||||
|
transpose_x=True,
|
||||||
|
) # (128, 128) (p(Br), d)
|
||||||
|
|
||||||
|
if initialize:
|
||||||
|
o_buffer[:, :] = nl.copy(pv_psum[:, :])
|
||||||
|
l_buffer[:, 0] = nl.add(nl.log(ps), max_)
|
||||||
|
else:
|
||||||
|
o_buffer[:, :] = nl.add(o_previous_scaled, pv_psum)
|
||||||
|
|
||||||
|
l_prev = l_buffer[:, 0]
|
||||||
|
l_exp = nl.add(
|
||||||
|
nl.exp(nl.subtract(l_prev, m_current)),
|
||||||
|
ps,
|
||||||
|
)
|
||||||
|
l_buffer[:, 0] = nl.add(m_current, nl.log(l_exp))
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def load_v_tile(v_hbm_tile, cur_v_tile, large_tile_idx, v_i, LARGE_TILE_SZ):
|
||||||
|
B_P_SIZE = 128
|
||||||
|
B_D_SIZE = v_hbm_tile.shape[-1]
|
||||||
|
loaded = nl.load(v_hbm_tile[
|
||||||
|
nl.ds(large_tile_idx * LARGE_TILE_SZ + B_P_SIZE * v_i, B_P_SIZE),
|
||||||
|
:,
|
||||||
|
])
|
||||||
|
if cur_v_tile.dtype != loaded.dtype:
|
||||||
|
loaded = nl.copy(loaded, dtype=cur_v_tile.dtype)
|
||||||
|
cur_v_tile[:, nl.ds(v_i * B_D_SIZE, B_D_SIZE)] = loaded
|
||||||
|
|
||||||
|
|
||||||
|
@nki.jit
|
||||||
|
def flash_paged_attention(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
kv_cache,
|
||||||
|
block_tables,
|
||||||
|
mask,
|
||||||
|
softmax_scale=None,
|
||||||
|
mixed_precision=True,
|
||||||
|
LARGE_TILE_SZ=2048,
|
||||||
|
return_debug_tensors=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Flash PagedAttention Forward Kernel.
|
||||||
|
|
||||||
|
IO tensor layouts:
|
||||||
|
- query: shape (1, n_heads, d, seq_q)
|
||||||
|
- key: shape (1, n_kv_heads, d, seq_k)
|
||||||
|
- value: shape (1, n_kv_heads, seq_v, d)
|
||||||
|
- kv_cache: (2, num_blocks, n_kv_heads, block_size, d)
|
||||||
|
- block_tables: (num_active_blocks, )
|
||||||
|
- mask: (seq_q, num_active_blocks * block_size + seq_q)
|
||||||
|
- o: shape (1, n_heads, seq_q, d)
|
||||||
|
|
||||||
|
- This kernel requires seq_k == seq_v
|
||||||
|
- We use continuous batching by default, so the batch dimension is
|
||||||
|
always 1, and different requests are concatenated along sequence
|
||||||
|
dimension.
|
||||||
|
- We use paged cache blocks (kv_cache) to store KV cache.
|
||||||
|
|
||||||
|
IO tensor dtypes:
|
||||||
|
- This kernel assumes all IO tensors have the same dtype except for
|
||||||
|
block_tables (int32) and mask (int32)
|
||||||
|
- If mixed_percision is True, then all Tensor Engine operation will be
|
||||||
|
performed in bfloat16 and accumulation will be performed in float32.
|
||||||
|
Otherwise the intermediates will be in the same type as the inputs.
|
||||||
|
|
||||||
|
Compile-time Constants:
|
||||||
|
- softmax_scale: scaling for softmax, is None, default is `1.0/(d**0.5)`
|
||||||
|
- mixed_precision: flag to set non-matmul ops in fp32 precision, default
|
||||||
|
is set to `true`, if false, we use same precision as input types
|
||||||
|
- LARGE_TILE_SZ: `default=2048`, size of the kv tile size for attention
|
||||||
|
computation reduction
|
||||||
|
|
||||||
|
GQA support Notes:
|
||||||
|
the spmd kernel for launching kernel should be on kv_heads instead of
|
||||||
|
nheads
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
MHA: q: [b, h, d, s], k: [b, h, d, s], v: [b, h, s, d]
|
||||||
|
usage: `flash_fwd[b, h](q, k, v, ...)`
|
||||||
|
GQA: q: [b, h, d, s], k: [b, kv_h, d, s], v: [b, kv_h, s, d]
|
||||||
|
usage: `flash_fwd[b, kv_h](q, k, v, ...)`
|
||||||
|
"""
|
||||||
|
B_F_SIZE = 512
|
||||||
|
B_P_SIZE = 128
|
||||||
|
b, h, d, seqlen_q = query.shape
|
||||||
|
B_D_SIZE = d
|
||||||
|
n_tile_q = seqlen_q // B_P_SIZE # since q will be loaded on tensor engine
|
||||||
|
_, num_blocks, k_h, block_size, _ = kv_cache.shape
|
||||||
|
q_h_per_k_h = h // k_h
|
||||||
|
assert b == 1, f"invalid batch size {b=}"
|
||||||
|
assert d <= 128, f" we do not support head_dim > 128, got head dim {d=}"
|
||||||
|
cache_shape = (2, num_blocks, k_h, block_size, d)
|
||||||
|
assert (tuple(kv_cache.shape) == cache_shape
|
||||||
|
), f"{kv_cache.shape=} mismatch, expect {cache_shape}"
|
||||||
|
assert key is None or tuple(key.shape) == (
|
||||||
|
1,
|
||||||
|
k_h,
|
||||||
|
d,
|
||||||
|
seqlen_q,
|
||||||
|
), f"key shape {key.shape} mismatch!"
|
||||||
|
assert value is None or tuple(value.shape) == (
|
||||||
|
1,
|
||||||
|
k_h,
|
||||||
|
seqlen_q,
|
||||||
|
d,
|
||||||
|
), f"value shape {value.shape} mismatch!"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
nl.program_ndim() == 2
|
||||||
|
), f"Expect spmd grid with 2 dimensions, got {nl.program_ndim()} instead!"
|
||||||
|
batch_id = nl.program_id(axis=0)
|
||||||
|
head_id = nl.program_id(axis=1)
|
||||||
|
|
||||||
|
(num_active_blocks, ) = block_tables.shape
|
||||||
|
context_kv_len = num_active_blocks * block_size
|
||||||
|
assert (
|
||||||
|
LARGE_TILE_SZ % B_F_SIZE == 0
|
||||||
|
), f"Need {LARGE_TILE_SZ=} to be divisible by {B_F_SIZE=} in transpose_p"
|
||||||
|
assert (context_kv_len % LARGE_TILE_SZ == 0
|
||||||
|
), f"Need {context_kv_len=} to be divisible by {LARGE_TILE_SZ=}"
|
||||||
|
|
||||||
|
num_blocks_per_large_tile = LARGE_TILE_SZ // block_size
|
||||||
|
assert is_power_of_2(
|
||||||
|
num_blocks_per_large_tile
|
||||||
|
), f"{num_blocks_per_large_tile=} is expected of be power of 2"
|
||||||
|
if seqlen_q > B_F_SIZE:
|
||||||
|
MAX_REDUCTION_TILE = 2048
|
||||||
|
if seqlen_q // 2 > MAX_REDUCTION_TILE:
|
||||||
|
assert (
|
||||||
|
seqlen_q % MAX_REDUCTION_TILE == 0
|
||||||
|
), f"{seqlen_q=} should be divisible by {MAX_REDUCTION_TILE=}"
|
||||||
|
else:
|
||||||
|
assert (seqlen_q % B_F_SIZE == 0
|
||||||
|
), f"{seqlen_q=} should be divisible by {B_F_SIZE=})"
|
||||||
|
|
||||||
|
kernel_dtype = nl.bfloat16 if mixed_precision else query.dtype
|
||||||
|
acc_type = np.dtype(np.float32) if mixed_precision else kernel_dtype
|
||||||
|
softmax_scale = softmax_scale or (1.0 / (d**0.5))
|
||||||
|
num_large_k_tile = context_kv_len // LARGE_TILE_SZ
|
||||||
|
|
||||||
|
o = nl.ndarray((b, h, seqlen_q, d),
|
||||||
|
dtype=query.dtype,
|
||||||
|
buffer=nl.shared_hbm)
|
||||||
|
hbm_l_buffer, hbm_m_buffer, hbm_qk_res, qk_res_buffer = (
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if return_debug_tensors:
|
||||||
|
hbm_l_buffer = nl.ndarray((b, h, seqlen_q),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.shared_hbm)
|
||||||
|
hbm_m_buffer = nl.ndarray((b, h, seqlen_q),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.shared_hbm)
|
||||||
|
hbm_qk_res = nl.ndarray((b, h, B_P_SIZE, seqlen_q),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.shared_hbm)
|
||||||
|
qk_res_buffer = nl.zeros(
|
||||||
|
(n_tile_q, q_h_per_k_h, par_dim(B_P_SIZE), seqlen_q),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
lazy_initialization=True,
|
||||||
|
)
|
||||||
|
block_tables_sbuf = load_block_tables(
|
||||||
|
block_tables_hbm=block_tables,
|
||||||
|
num_tiles=num_large_k_tile,
|
||||||
|
num_blocks_per_tile=num_blocks_per_large_tile,
|
||||||
|
)
|
||||||
|
|
||||||
|
# On Neuron, we need B_P_SIZE = 128 blocks to make DMA efficient
|
||||||
|
if num_blocks_per_large_tile < B_P_SIZE:
|
||||||
|
# we checked num_blocks_per_tile is a power of 2
|
||||||
|
assert B_P_SIZE % num_blocks_per_large_tile == 0
|
||||||
|
block_size_tiling_factor = B_P_SIZE // num_blocks_per_large_tile
|
||||||
|
# We assume block_size >= block_size_tiling_factor
|
||||||
|
assert block_size % block_size_tiling_factor == 0
|
||||||
|
else:
|
||||||
|
block_size_tiling_factor = 1
|
||||||
|
tiled_block_size = block_size // block_size_tiling_factor
|
||||||
|
|
||||||
|
# Indirect DMA load must be placed along Partition Dimension
|
||||||
|
block_tables_sbuf = transform_block_tables_for_indirect_load(
|
||||||
|
block_tables_sbuf,
|
||||||
|
block_size_tiling_factor=block_size_tiling_factor,
|
||||||
|
num_head=k_h,
|
||||||
|
head_id=head_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Flatten KV cache to be 3D for loading into SBUF
|
||||||
|
new_cache_shape = (
|
||||||
|
2,
|
||||||
|
num_blocks * k_h * block_size_tiling_factor,
|
||||||
|
tiled_block_size * d,
|
||||||
|
)
|
||||||
|
kv_cache = kv_cache.reshape(new_cache_shape)
|
||||||
|
|
||||||
|
# Global Flash Attention accumulators
|
||||||
|
o_buffer = nl.zeros(
|
||||||
|
(n_tile_q, q_h_per_k_h, par_dim(B_P_SIZE), d),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
lazy_initialization=True,
|
||||||
|
)
|
||||||
|
l_buffer = nl.zeros(
|
||||||
|
(n_tile_q, q_h_per_k_h, par_dim(B_P_SIZE), 1),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
lazy_initialization=True,
|
||||||
|
)
|
||||||
|
m_buffer = nl.zeros(
|
||||||
|
(n_tile_q, q_h_per_k_h, par_dim(B_P_SIZE), 1),
|
||||||
|
dtype=acc_type,
|
||||||
|
buffer=nl.sbuf,
|
||||||
|
lazy_initialization=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
for large_k_tile_idx in nl.sequential_range(0, num_large_k_tile):
|
||||||
|
num_loads = ceil_div(num_blocks_per_large_tile, B_P_SIZE)
|
||||||
|
cur_k_tile = nl.ndarray(
|
||||||
|
(par_dim(B_D_SIZE), LARGE_TILE_SZ),
|
||||||
|
dtype=kernel_dtype,
|
||||||
|
)
|
||||||
|
cur_v_tile = nl.ndarray(
|
||||||
|
(par_dim(B_P_SIZE), num_loads * tiled_block_size * B_D_SIZE),
|
||||||
|
dtype=kernel_dtype,
|
||||||
|
)
|
||||||
|
load_kv_tile_from_cache(
|
||||||
|
cur_k_tile=cur_k_tile,
|
||||||
|
cur_v_tile=cur_v_tile,
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
block_tables=block_tables_sbuf,
|
||||||
|
large_k_tile_idx=large_k_tile_idx,
|
||||||
|
num_blocks_per_large_tile=num_blocks_per_large_tile,
|
||||||
|
tiled_block_size=tiled_block_size,
|
||||||
|
B_P_SIZE=B_P_SIZE,
|
||||||
|
B_D_SIZE=B_D_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in nl.affine_range(n_tile_q):
|
||||||
|
cur_mask = nl.load(mask[
|
||||||
|
nl.ds(i * B_P_SIZE, B_P_SIZE),
|
||||||
|
nl.ds(large_k_tile_idx * LARGE_TILE_SZ, LARGE_TILE_SZ),
|
||||||
|
])
|
||||||
|
for i_q_h in nl.affine_range(q_h_per_k_h):
|
||||||
|
q_tile = nl.ndarray((B_D_SIZE, B_P_SIZE), dtype=kernel_dtype)
|
||||||
|
q_hbm_tile = query[batch_id, head_id * q_h_per_k_h + i_q_h]
|
||||||
|
q_sbuf_tile = nl.load(q_hbm_tile[:,
|
||||||
|
nl.ds(i *
|
||||||
|
B_P_SIZE, B_P_SIZE)])
|
||||||
|
if q_sbuf_tile.dtype != kernel_dtype:
|
||||||
|
q_sbuf_tile = nl.copy(q_sbuf_tile, dtype=kernel_dtype)
|
||||||
|
q_tile[:, :] = q_sbuf_tile * softmax_scale
|
||||||
|
|
||||||
|
_flash_attention_core(
|
||||||
|
q_local_tile=q_tile,
|
||||||
|
k=cur_k_tile,
|
||||||
|
v=cur_v_tile,
|
||||||
|
o_buffer=o_buffer[i, i_q_h],
|
||||||
|
l_buffer=l_buffer[i, i_q_h],
|
||||||
|
m_buffer=m_buffer[i, i_q_h],
|
||||||
|
kernel_dtype=kernel_dtype,
|
||||||
|
acc_type=acc_type,
|
||||||
|
tile_mask=cur_mask,
|
||||||
|
use_causal_mask=False,
|
||||||
|
q_tile_idx=i,
|
||||||
|
initialize=large_k_tile_idx == 0,
|
||||||
|
LARGE_TILE_SZ=LARGE_TILE_SZ,
|
||||||
|
B_P_SIZE=B_P_SIZE,
|
||||||
|
B_F_SIZE=B_F_SIZE,
|
||||||
|
B_D_SIZE=B_D_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# compute attention between input query, key and value
|
||||||
|
if key is not None and value is not None:
|
||||||
|
B_F_SIZE = min(seqlen_q, B_F_SIZE)
|
||||||
|
LARGE_TILE_SZ = seqlen_q
|
||||||
|
|
||||||
|
cur_k_tile = nl.ndarray((par_dim(B_D_SIZE), LARGE_TILE_SZ),
|
||||||
|
dtype=kernel_dtype)
|
||||||
|
cur_v_tile = nl.ndarray(
|
||||||
|
(par_dim(B_P_SIZE), LARGE_TILE_SZ // B_P_SIZE * B_D_SIZE),
|
||||||
|
dtype=kernel_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
loaded = nl.load(key[batch_id, head_id, :, :])
|
||||||
|
if loaded.dtype != kernel_dtype:
|
||||||
|
loaded = nl.copy(loaded, dtype=kernel_dtype)
|
||||||
|
cur_k_tile[:, :] = loaded
|
||||||
|
|
||||||
|
v_hbm_tile = value[batch_id, head_id]
|
||||||
|
for v_i in nl.affine_range(LARGE_TILE_SZ // B_P_SIZE):
|
||||||
|
load_v_tile(
|
||||||
|
v_hbm_tile=v_hbm_tile,
|
||||||
|
cur_v_tile=cur_v_tile,
|
||||||
|
large_tile_idx=0,
|
||||||
|
v_i=v_i,
|
||||||
|
LARGE_TILE_SZ=LARGE_TILE_SZ,
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in nl.affine_range(n_tile_q):
|
||||||
|
cur_mask = nl.load(mask[
|
||||||
|
nl.ds(i * B_P_SIZE, B_P_SIZE),
|
||||||
|
nl.ds(context_kv_len, LARGE_TILE_SZ),
|
||||||
|
])
|
||||||
|
for i_q_h in nl.affine_range(q_h_per_k_h):
|
||||||
|
|
||||||
|
q_tile = nl.ndarray((B_D_SIZE, B_P_SIZE), dtype=kernel_dtype)
|
||||||
|
q_hbm_tile = query[batch_id, head_id * q_h_per_k_h + i_q_h]
|
||||||
|
q_sbuf_tile = nl.load(q_hbm_tile[:,
|
||||||
|
nl.ds(i *
|
||||||
|
B_P_SIZE, B_P_SIZE)])
|
||||||
|
if q_sbuf_tile.dtype != kernel_dtype:
|
||||||
|
q_sbuf_tile = nl.copy(q_sbuf_tile, dtype=kernel_dtype)
|
||||||
|
q_tile[:, :] = q_sbuf_tile * softmax_scale
|
||||||
|
_flash_attention_core(
|
||||||
|
q_local_tile=q_tile,
|
||||||
|
k=cur_k_tile,
|
||||||
|
v=cur_v_tile,
|
||||||
|
o_buffer=o_buffer[i, i_q_h],
|
||||||
|
l_buffer=l_buffer[i, i_q_h],
|
||||||
|
m_buffer=m_buffer[i, i_q_h],
|
||||||
|
kernel_dtype=kernel_dtype,
|
||||||
|
acc_type=acc_type,
|
||||||
|
tile_mask=cur_mask,
|
||||||
|
use_causal_mask=True,
|
||||||
|
q_tile_idx=i,
|
||||||
|
initialize=False,
|
||||||
|
LARGE_TILE_SZ=LARGE_TILE_SZ,
|
||||||
|
B_P_SIZE=B_P_SIZE,
|
||||||
|
B_F_SIZE=B_F_SIZE,
|
||||||
|
B_D_SIZE=B_D_SIZE,
|
||||||
|
qk_res_buffer=(qk_res_buffer[i, i_q_h]
|
||||||
|
if qk_res_buffer is not None else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- -- -- -- write output to buffer on HBM -- -- -- -- -- -- #
|
||||||
|
for i_q_h in nl.affine_range(q_h_per_k_h):
|
||||||
|
for i in nl.affine_range(n_tile_q):
|
||||||
|
out = nl.multiply(
|
||||||
|
o_buffer[i, i_q_h],
|
||||||
|
nl.exp(m_buffer[i, i_q_h] - l_buffer[i, i_q_h]),
|
||||||
|
dtype=kernel_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
nl.store(
|
||||||
|
o[
|
||||||
|
batch_id,
|
||||||
|
head_id * q_h_per_k_h + i_q_h,
|
||||||
|
nl.ds(i * B_P_SIZE, B_P_SIZE),
|
||||||
|
:,
|
||||||
|
],
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
# maximum and summation statistics
|
||||||
|
if return_debug_tensors:
|
||||||
|
nl.store(
|
||||||
|
hbm_m_buffer[
|
||||||
|
batch_id,
|
||||||
|
head_id * q_h_per_k_h + i_q_h,
|
||||||
|
nl.ds(i * B_P_SIZE, B_P_SIZE),
|
||||||
|
],
|
||||||
|
m_buffer[i, i_q_h, :, :],
|
||||||
|
)
|
||||||
|
nl.store(
|
||||||
|
hbm_l_buffer[
|
||||||
|
batch_id,
|
||||||
|
head_id * q_h_per_k_h + i_q_h,
|
||||||
|
nl.ds(i * B_P_SIZE, B_P_SIZE),
|
||||||
|
],
|
||||||
|
l_buffer[i, i_q_h],
|
||||||
|
)
|
||||||
|
nl.store(
|
||||||
|
hbm_qk_res[batch_id, head_id * q_h_per_k_h + i_q_h, :, :],
|
||||||
|
qk_res_buffer[batch_id, i_q_h, :, :],
|
||||||
|
)
|
||||||
|
|
||||||
|
if return_debug_tensors:
|
||||||
|
return o, hbm_m_buffer, hbm_l_buffer, hbm_qk_res
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
def reorder_context_mask(mask, LARGE_TILE_SZ, block_size):
|
||||||
|
"""
|
||||||
|
Reorder the mask to make it compatible with the flash attention kernel.
|
||||||
|
|
||||||
|
We vectorize KV cache read to improve DMA utilization. However, the layout
|
||||||
|
that maximizes DMA bandwidth changes the order tokens are consumed.
|
||||||
|
|
||||||
|
The token layout (inner 2 dimensions) after vectorized load is (B_P_SIZE,
|
||||||
|
tiled_block_size) in a tile of `B_P_SIZE * tiled_block_size` tokens. And
|
||||||
|
each step the engine consumes a column (rather than a row) of B_P_SIZE
|
||||||
|
tokens. Therefore, the tokens are visited in a strided way.
|
||||||
|
|
||||||
|
To make sure mask matches the order tokens are consumed, we need to properly
|
||||||
|
transpose mask.
|
||||||
|
"""
|
||||||
|
total_query_len, total_seq_len = mask.shape
|
||||||
|
context_kv_len = total_seq_len - total_query_len
|
||||||
|
|
||||||
|
B_P_SIZE = 128
|
||||||
|
assert (LARGE_TILE_SZ
|
||||||
|
>= B_P_SIZE), f"{LARGE_TILE_SZ=} must be larger than {B_P_SIZE=}"
|
||||||
|
num_tiled_blocks = max(B_P_SIZE, LARGE_TILE_SZ // block_size)
|
||||||
|
tiled_block_size = LARGE_TILE_SZ // num_tiled_blocks
|
||||||
|
if tiled_block_size > 1:
|
||||||
|
# Mask reordering is needed when tiled_block_size > 1
|
||||||
|
device = mask.device
|
||||||
|
mask = mask.cpu()
|
||||||
|
context_mask = mask[:, :context_kv_len]
|
||||||
|
context_mask = context_mask.view(
|
||||||
|
total_query_len,
|
||||||
|
context_kv_len // LARGE_TILE_SZ,
|
||||||
|
num_tiled_blocks // B_P_SIZE,
|
||||||
|
B_P_SIZE,
|
||||||
|
tiled_block_size,
|
||||||
|
)
|
||||||
|
context_mask = context_mask.transpose(3, 4).reshape(
|
||||||
|
total_query_len, context_kv_len)
|
||||||
|
new_mask = mask[:, context_kv_len:]
|
||||||
|
return torch.concat([context_mask, new_mask], dim=1).to(device)
|
||||||
|
else:
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
def flash_attn_varlen_nkifunc(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
kv_cache,
|
||||||
|
block_table,
|
||||||
|
attn_mask,
|
||||||
|
n_kv_head=None,
|
||||||
|
head_size=None,
|
||||||
|
LARGE_TILE_SZ=2048,
|
||||||
|
mixed_precision=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Compute flash paged attention for variable length sequences.
|
||||||
|
|
||||||
|
This function is a wrapper around the flash attention NKI kernel. It takes
|
||||||
|
in the following arguments:
|
||||||
|
- query: (1, n_heads, d, seq_q)
|
||||||
|
- key: (1, n_kv_heads, d, seq_k)
|
||||||
|
- value: (1, n_kv_heads, seq_v, d)
|
||||||
|
- kv_cache: (2, n_blocks, n_kv_heads, block_size, d)
|
||||||
|
- block_tables: (n_active_blocks, )
|
||||||
|
- attn_mask: (seq_q, n_active_blocks * block_size + seq_q)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- attn_mask must be reordered outside using `reorder_context_mask`
|
||||||
|
- Key/value cache layout must be (n_blocks, n_kv_heads, block_size, d)
|
||||||
|
for better DMA throughput
|
||||||
|
"""
|
||||||
|
if n_kv_head is None:
|
||||||
|
n_kv_head = kv_cache.shape[2]
|
||||||
|
assert kv_cache.shape[0] == 2
|
||||||
|
assert kv_cache.shape[2] == n_kv_head
|
||||||
|
if head_size is None:
|
||||||
|
head_size = kv_cache.shape[-1]
|
||||||
|
|
||||||
|
kwargs = dict(
|
||||||
|
query=query,
|
||||||
|
key=key,
|
||||||
|
value=value,
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
block_tables=block_table,
|
||||||
|
mask=attn_mask,
|
||||||
|
softmax_scale=1.0 / (head_size**0.5),
|
||||||
|
mixed_precision=mixed_precision,
|
||||||
|
LARGE_TILE_SZ=LARGE_TILE_SZ,
|
||||||
|
)
|
||||||
|
|
||||||
|
o = flash_paged_attention[1, n_kv_head](**kwargs)
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
def reshape_and_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Writes key-value pairs to the KV cache at specified positions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (torch.Tensor): Key tensor with shape
|
||||||
|
(num_tokens, n_kv_head, d_head)
|
||||||
|
value (torch.Tensor): Value tensor with shape
|
||||||
|
(num_tokens, n_kv_head, d_head)
|
||||||
|
kv_cache (torch.Tensor): Key/value cache tensor with shape
|
||||||
|
(2, num_blocks, n_kv_head, block_size, d_head)
|
||||||
|
slot_mapping (torch.Tensor): Mapping tensor indicating cache positions
|
||||||
|
with shape (num_tokens)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: Updates the kv_cache tensor in-place
|
||||||
|
"""
|
||||||
|
block_size = kv_cache.size(3)
|
||||||
|
n_kv_head = key.size(1)
|
||||||
|
|
||||||
|
# Calculate indices with explicit floor division
|
||||||
|
block_indices = torch.div(slot_mapping, block_size, rounding_mode="floor")
|
||||||
|
block_offsets = slot_mapping % block_size
|
||||||
|
|
||||||
|
# Create the head indices tensor
|
||||||
|
head_indices = torch.arange(n_kv_head, device=key.device)
|
||||||
|
|
||||||
|
# Update caches using index_put_
|
||||||
|
kv_cache.index_put_(
|
||||||
|
(torch.tensor([0], device=key.device), block_indices[:, None],
|
||||||
|
head_indices[None, :], block_offsets[:, None]), key)
|
||||||
|
|
||||||
|
kv_cache.index_put_(
|
||||||
|
(torch.tensor([1], device=key.device), block_indices[:, None],
|
||||||
|
head_indices[None, :], block_offsets[:, None]), value)
|
||||||
255
vllm/attention/ops/paged_attn.py
Normal file
255
vllm/attention/ops/paged_attn.py
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.triton_utils import HAS_TRITON
|
||||||
|
|
||||||
|
if HAS_TRITON:
|
||||||
|
from vllm.attention.ops.prefix_prefill import context_attention_fwd
|
||||||
|
|
||||||
|
# Should be the same as PARTITION_SIZE in `paged_attention_v2_launcher`.
|
||||||
|
_PARTITION_SIZE = 512
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PagedAttentionMetadata:
|
||||||
|
"""Metadata for PagedAttention."""
|
||||||
|
# (batch_size,). The length of sequences (entire tokens seen so far) per
|
||||||
|
# sequence.
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor]
|
||||||
|
# Maximum sequence length in the batch. 0 if it is prefill-only batch.
|
||||||
|
max_decode_seq_len: int
|
||||||
|
# (batch_size, max_blocks_per_seq).
|
||||||
|
# Block addresses per sequence. (Seq id -> list of physical block)
|
||||||
|
# E.g., [0, 1, 2] means tokens are stored in 0th, 1st, and 2nd blocks
|
||||||
|
# in the kv cache. Each block can contain up to block_size tokens.
|
||||||
|
# 2nd dimensions are padded up to max_blocks_per_seq if it is cuda-graph
|
||||||
|
# captured.
|
||||||
|
block_tables: Optional[torch.Tensor]
|
||||||
|
|
||||||
|
|
||||||
|
class PagedAttention:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [32, 64, 80, 96, 112, 120, 128, 192, 256]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (2, num_blocks, block_size * num_kv_heads * head_size)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_kv_cache(
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
x = 16 // kv_cache.element_size()
|
||||||
|
num_blocks = kv_cache.shape[1]
|
||||||
|
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
key_cache = key_cache.view(num_blocks, num_kv_heads, head_size // x,
|
||||||
|
-1, x)
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
value_cache = value_cache.view(num_blocks, num_kv_heads, head_size, -1)
|
||||||
|
return key_cache, value_cache
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_to_paged_cache(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
ops.reshape_and_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
slot_mapping.flatten(),
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_decode(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
seq_lens: torch.Tensor,
|
||||||
|
max_seq_len: int,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
tp_rank: int = 0,
|
||||||
|
blocksparse_local_blocks: int = 0,
|
||||||
|
blocksparse_vert_stride: int = 0,
|
||||||
|
blocksparse_block_size: int = 64,
|
||||||
|
blocksparse_head_sliding_step: int = 0,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if blocksparse_vert_stride is not None and blocksparse_vert_stride > 1:
|
||||||
|
# use blocksparse paged attention
|
||||||
|
block_size = value_cache.size(-1)
|
||||||
|
assert (blocksparse_block_size > 0 and
|
||||||
|
blocksparse_block_size % block_size == 0), \
|
||||||
|
(f"{blocksparse_block_size=} needs to be a multiple of"
|
||||||
|
f"{block_size=} used in block_tables.")
|
||||||
|
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
num_seqs, num_heads, head_size = query.shape
|
||||||
|
max_num_partitions = ((max_seq_len + _PARTITION_SIZE - 1) //
|
||||||
|
_PARTITION_SIZE)
|
||||||
|
# NOTE(woosuk): We use a simple heuristic to decide whether to use
|
||||||
|
# PagedAttention V1 or V2. If the number of partitions is 1, we use
|
||||||
|
# V1 to avoid the overhead of reduction. Also, if the number of
|
||||||
|
# sequences or heads is large, we use V1 since there is enough work
|
||||||
|
# to parallelize.
|
||||||
|
# TODO(woosuk): Tune this heuristic.
|
||||||
|
# For context len > 8192, use V2 kernel to avoid shared memory shortage.
|
||||||
|
use_v1 = (max_seq_len <= 8192
|
||||||
|
and (max_num_partitions == 1 or num_seqs * num_heads > 512))
|
||||||
|
|
||||||
|
if use_v1:
|
||||||
|
# Run PagedAttention V1.
|
||||||
|
ops.paged_attention_v1(
|
||||||
|
output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
num_kv_heads,
|
||||||
|
scale,
|
||||||
|
block_tables,
|
||||||
|
seq_lens,
|
||||||
|
block_size,
|
||||||
|
max_seq_len,
|
||||||
|
alibi_slopes,
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
tp_rank,
|
||||||
|
blocksparse_local_blocks,
|
||||||
|
blocksparse_vert_stride,
|
||||||
|
blocksparse_block_size,
|
||||||
|
blocksparse_head_sliding_step,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Run PagedAttention V2.
|
||||||
|
assert _PARTITION_SIZE % block_size == 0
|
||||||
|
tmp_output = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions, head_size),
|
||||||
|
dtype=output.dtype,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
exp_sums = torch.empty(
|
||||||
|
size=(num_seqs, num_heads, max_num_partitions),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=output.device,
|
||||||
|
)
|
||||||
|
max_logits = torch.empty_like(exp_sums)
|
||||||
|
ops.paged_attention_v2(
|
||||||
|
output,
|
||||||
|
exp_sums,
|
||||||
|
max_logits,
|
||||||
|
tmp_output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
num_kv_heads,
|
||||||
|
scale,
|
||||||
|
block_tables,
|
||||||
|
seq_lens,
|
||||||
|
block_size,
|
||||||
|
max_seq_len,
|
||||||
|
alibi_slopes,
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
tp_rank,
|
||||||
|
blocksparse_local_blocks,
|
||||||
|
blocksparse_vert_stride,
|
||||||
|
blocksparse_block_size,
|
||||||
|
blocksparse_head_sliding_step,
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward_prefix(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
block_tables: torch.Tensor,
|
||||||
|
query_start_loc: torch.Tensor,
|
||||||
|
seq_lens_tensor: torch.Tensor,
|
||||||
|
max_query_len: int,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
sliding_window: Optional[int],
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
max_seq_len = None
|
||||||
|
context_attention_fwd(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
output,
|
||||||
|
kv_cache_dtype,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
block_tables,
|
||||||
|
# query_start_loc is (batch_size + 1,)
|
||||||
|
query_start_loc,
|
||||||
|
seq_lens_tensor,
|
||||||
|
max_seq_len,
|
||||||
|
max_query_len,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
alibi_slopes,
|
||||||
|
sliding_window,
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def swap_blocks(
|
||||||
|
src_kv_cache: torch.Tensor,
|
||||||
|
dst_kv_cache: torch.Tensor,
|
||||||
|
src_to_dst: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
src_key_cache = src_kv_cache[0]
|
||||||
|
dst_key_cache = dst_kv_cache[0]
|
||||||
|
ops.swap_blocks(src_key_cache, dst_key_cache, src_to_dst)
|
||||||
|
|
||||||
|
src_value_cache = src_kv_cache[1]
|
||||||
|
dst_value_cache = dst_kv_cache[1]
|
||||||
|
ops.swap_blocks(src_value_cache, dst_value_cache, src_to_dst)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def copy_blocks(
|
||||||
|
kv_caches: List[torch.Tensor],
|
||||||
|
src_to_dists: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
key_caches = [kv_cache[0] for kv_cache in kv_caches]
|
||||||
|
value_caches = [kv_cache[1] for kv_cache in kv_caches]
|
||||||
|
ops.copy_blocks(key_caches, value_caches, src_to_dists)
|
||||||
894
vllm/attention/ops/prefix_prefill.py
Normal file
894
vllm/attention/ops/prefix_prefill.py
Normal file
@@ -0,0 +1,894 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# The kernels in this file are adapted from LightLLM's context_attention_fwd:
|
||||||
|
# https://github.com/ModelTC/lightllm/blob/main/lightllm/models/llama/triton_kernel/context_flashattention_nopad.py
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
# Static kernels parameters
|
||||||
|
BASE_BLOCK = 128 if current_platform.has_device_capability(80) else 64
|
||||||
|
NUM_WARPS = 4 if current_platform.is_rocm() else 8
|
||||||
|
|
||||||
|
# To check compatibility
|
||||||
|
IS_TURING = current_platform.get_device_capability() == (7, 5)
|
||||||
|
|
||||||
|
if triton.__version__ >= "2.1.0":
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel(
|
||||||
|
Q,
|
||||||
|
K,
|
||||||
|
V,
|
||||||
|
K_cache,
|
||||||
|
V_cache,
|
||||||
|
B_Loc,
|
||||||
|
sm_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
B_Start_Loc,
|
||||||
|
B_Seqlen,
|
||||||
|
block_size,
|
||||||
|
x,
|
||||||
|
Out,
|
||||||
|
stride_b_loc_b,
|
||||||
|
stride_b_loc_s,
|
||||||
|
stride_qbs,
|
||||||
|
stride_qh,
|
||||||
|
stride_qd,
|
||||||
|
stride_kbs,
|
||||||
|
stride_kh,
|
||||||
|
stride_kd,
|
||||||
|
stride_vbs,
|
||||||
|
stride_vh,
|
||||||
|
stride_vd,
|
||||||
|
stride_obs,
|
||||||
|
stride_oh,
|
||||||
|
stride_od,
|
||||||
|
stride_k_cache_bs,
|
||||||
|
stride_k_cache_h,
|
||||||
|
stride_k_cache_d,
|
||||||
|
stride_k_cache_bl,
|
||||||
|
stride_k_cache_x,
|
||||||
|
stride_v_cache_bs,
|
||||||
|
stride_v_cache_h,
|
||||||
|
stride_v_cache_d,
|
||||||
|
stride_v_cache_bl,
|
||||||
|
num_queries_per_kv: int,
|
||||||
|
IN_PRECISION: tl.constexpr,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr, # head size
|
||||||
|
BLOCK_DMODEL_PADDED: tl.constexpr, # head size padded to a power of 2
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
SLIDING_WINDOW: tl.constexpr,
|
||||||
|
SKIP_DECODE: tl.constexpr,
|
||||||
|
):
|
||||||
|
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head = tl.program_id(1)
|
||||||
|
start_m = tl.program_id(2)
|
||||||
|
|
||||||
|
cur_kv_head = cur_head // num_queries_per_kv
|
||||||
|
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
cur_batch_in_all_start_index = tl.load(B_Start_Loc + cur_batch)
|
||||||
|
cur_batch_in_all_stop_index = tl.load(B_Start_Loc + cur_batch + 1)
|
||||||
|
cur_batch_query_len = (cur_batch_in_all_stop_index -
|
||||||
|
cur_batch_in_all_start_index)
|
||||||
|
cur_batch_ctx_len = cur_batch_seq_len - cur_batch_query_len
|
||||||
|
|
||||||
|
if SKIP_DECODE and cur_batch_query_len == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# start position inside of the query
|
||||||
|
# generally, N goes over kv, while M goes over query_len
|
||||||
|
block_start_loc = BLOCK_M * start_m
|
||||||
|
|
||||||
|
# initialize offsets
|
||||||
|
# [N]; starts at 0
|
||||||
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
# [D]; starts at 0
|
||||||
|
offs_d = tl.arange(0, BLOCK_DMODEL_PADDED)
|
||||||
|
# [M]; starts at current position in query
|
||||||
|
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||||
|
# [M,D]
|
||||||
|
off_q = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_qbs +
|
||||||
|
cur_head * stride_qh + offs_d[None, :] * stride_qd)
|
||||||
|
|
||||||
|
dim_mask = tl.where(
|
||||||
|
tl.arange(0, BLOCK_DMODEL_PADDED) < BLOCK_DMODEL, 1,
|
||||||
|
0).to(tl.int1) # [D]
|
||||||
|
|
||||||
|
q = tl.load(Q + off_q,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
(offs_m[:, None] < cur_batch_query_len),
|
||||||
|
other=0.0) # [M,D]
|
||||||
|
|
||||||
|
# initialize pointer to m and l
|
||||||
|
m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf") # [M]
|
||||||
|
l_i = tl.zeros([BLOCK_M], dtype=tl.float32) # [M]
|
||||||
|
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL_PADDED],
|
||||||
|
dtype=tl.float32) # [M,D]
|
||||||
|
|
||||||
|
# compute query against context (no causal mask here)
|
||||||
|
for start_n in range(0, cur_batch_ctx_len, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
bn = tl.load(B_Loc + cur_batch * stride_b_loc_b +
|
||||||
|
((start_n + offs_n) // block_size) * stride_b_loc_s,
|
||||||
|
mask=(start_n + offs_n) < cur_batch_ctx_len,
|
||||||
|
other=0) # [N]
|
||||||
|
# [D,N]
|
||||||
|
off_k = (bn[None, :] * stride_k_cache_bs +
|
||||||
|
cur_kv_head * stride_k_cache_h +
|
||||||
|
(offs_d[:, None] // x) * stride_k_cache_d +
|
||||||
|
((start_n + offs_n[None, :]) % block_size) *
|
||||||
|
stride_k_cache_bl +
|
||||||
|
(offs_d[:, None] % x) * stride_k_cache_x)
|
||||||
|
# [N,D]
|
||||||
|
off_v = (
|
||||||
|
bn[:, None] * stride_v_cache_bs +
|
||||||
|
cur_kv_head * stride_v_cache_h +
|
||||||
|
offs_d[None, :] * stride_v_cache_d +
|
||||||
|
(start_n + offs_n[:, None]) % block_size * stride_v_cache_bl)
|
||||||
|
k_load = tl.load(K_cache + off_k,
|
||||||
|
mask=dim_mask[:, None] &
|
||||||
|
((start_n + offs_n[None, :]) < cur_batch_ctx_len),
|
||||||
|
other=0.0) # [D,N]
|
||||||
|
|
||||||
|
if k_load.dtype.is_fp8():
|
||||||
|
k = (k_load.to(tl.float32) * tl.load(k_scale)).to(q.dtype)
|
||||||
|
else:
|
||||||
|
k = k_load
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32) # [M,N]
|
||||||
|
qk = tl.dot(q, k, acc=qk, input_precision=IN_PRECISION)
|
||||||
|
qk = tl.where((start_n + offs_n[None, :]) < cur_batch_ctx_len, qk,
|
||||||
|
float("-inf"))
|
||||||
|
qk *= sm_scale
|
||||||
|
if SLIDING_WINDOW > 0:
|
||||||
|
# (cur_batch_ctx_len + offs_m[:, None]) are the positions of
|
||||||
|
# Q entries in sequence
|
||||||
|
# (start_n + offs_n[None, :]) are the positions of
|
||||||
|
# KV entries in sequence
|
||||||
|
# So the condition makes sure each entry in Q only attends
|
||||||
|
# to KV entries not more than SLIDING_WINDOW away.
|
||||||
|
#
|
||||||
|
# We can't use -inf here, because the
|
||||||
|
# sliding window may lead to the entire row being masked.
|
||||||
|
# This then makes m_ij contain -inf, which causes NaNs in
|
||||||
|
# exp().
|
||||||
|
qk = tl.where((cur_batch_ctx_len + offs_m[:, None]) -
|
||||||
|
(start_n + offs_n[None, :]) < SLIDING_WINDOW, qk,
|
||||||
|
-10000)
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1) # [M]
|
||||||
|
p = tl.exp(qk - m_ij[:, None]) # [M,N]
|
||||||
|
l_ij = tl.sum(p, 1) # [M]
|
||||||
|
# -- update m_i and l_i
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij) # [M]
|
||||||
|
alpha = tl.exp(m_i - m_i_new) # [M]
|
||||||
|
beta = tl.exp(m_ij - m_i_new) # [M]
|
||||||
|
l_i_new = alpha * l_i + beta * l_ij # [M]
|
||||||
|
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
p_scale = beta / l_i_new
|
||||||
|
p = p * p_scale[:, None]
|
||||||
|
# scale acc
|
||||||
|
acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v_load = tl.load(V_cache + off_v,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
((start_n + offs_n[:, None]) < cur_batch_ctx_len),
|
||||||
|
other=0.0) # [N,D]
|
||||||
|
if v_load.dtype.is_fp8():
|
||||||
|
v = (v_load.to(tl.float32) * tl.load(v_scale)).to(q.dtype)
|
||||||
|
else:
|
||||||
|
v = v_load
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
|
||||||
|
acc = tl.dot(p, v, acc=acc, input_precision=IN_PRECISION)
|
||||||
|
# # update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
|
||||||
|
off_k = (offs_n[None, :] * stride_kbs + cur_kv_head * stride_kh +
|
||||||
|
offs_d[:, None] * stride_kd)
|
||||||
|
off_v = (offs_n[:, None] * stride_vbs + cur_kv_head * stride_vh +
|
||||||
|
offs_d[None, :] * stride_vd)
|
||||||
|
k_ptrs = K + off_k
|
||||||
|
v_ptrs = V + off_v
|
||||||
|
|
||||||
|
# block_mask is 0 when we're already past the current query length
|
||||||
|
block_mask = tl.where(block_start_loc < cur_batch_query_len, 1, 0)
|
||||||
|
|
||||||
|
# compute query against itself (with causal mask)
|
||||||
|
for start_n in range(0, block_mask * (start_m + 1) * BLOCK_M, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
k = tl.load(k_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_kbs,
|
||||||
|
mask=dim_mask[:, None] &
|
||||||
|
((start_n + offs_n[None, :]) < cur_batch_query_len),
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk = tl.dot(q, k, acc=qk, input_precision=IN_PRECISION)
|
||||||
|
qk *= sm_scale
|
||||||
|
# apply causal mask
|
||||||
|
qk = tl.where(offs_m[:, None] >= (start_n + offs_n[None, :]), qk,
|
||||||
|
float("-inf"))
|
||||||
|
if SLIDING_WINDOW > 0:
|
||||||
|
qk = tl.where(
|
||||||
|
offs_m[:, None] - (start_n + offs_n[None, :])
|
||||||
|
< SLIDING_WINDOW, qk, -10000)
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1)
|
||||||
|
p = tl.exp(qk - m_ij[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij)
|
||||||
|
alpha = tl.exp(m_i - m_i_new)
|
||||||
|
beta = tl.exp(m_ij - m_i_new)
|
||||||
|
l_i_new = alpha * l_i + beta * l_ij
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
p_scale = beta / l_i_new
|
||||||
|
p = p * p_scale[:, None]
|
||||||
|
# scale acc
|
||||||
|
acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v = tl.load(v_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_vbs,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
((start_n + offs_n[:, None]) < cur_batch_query_len),
|
||||||
|
other=0.0)
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
|
||||||
|
acc = tl.dot(p, v, acc=acc, input_precision=IN_PRECISION)
|
||||||
|
# update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
# initialize pointers to output
|
||||||
|
off_o = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_obs +
|
||||||
|
cur_head * stride_oh + offs_d[None, :] * stride_od)
|
||||||
|
out_ptrs = Out + off_o
|
||||||
|
tl.store(out_ptrs,
|
||||||
|
acc,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
(offs_m[:, None] < cur_batch_query_len))
|
||||||
|
return
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_flash_attn_v2(
|
||||||
|
Q,
|
||||||
|
K,
|
||||||
|
V,
|
||||||
|
K_cache,
|
||||||
|
V_cache,
|
||||||
|
B_Loc,
|
||||||
|
sm_scale,
|
||||||
|
B_Start_Loc,
|
||||||
|
B_Seqlen,
|
||||||
|
B_Ctxlen,
|
||||||
|
block_size,
|
||||||
|
x,
|
||||||
|
Out,
|
||||||
|
stride_b_loc_b,
|
||||||
|
stride_b_loc_s,
|
||||||
|
stride_qbs,
|
||||||
|
stride_qh,
|
||||||
|
stride_qd,
|
||||||
|
stride_kbs,
|
||||||
|
stride_kh,
|
||||||
|
stride_kd,
|
||||||
|
stride_vbs,
|
||||||
|
stride_vh,
|
||||||
|
stride_vd,
|
||||||
|
stride_obs,
|
||||||
|
stride_oh,
|
||||||
|
stride_od,
|
||||||
|
stride_k_cache_bs,
|
||||||
|
stride_k_cache_h,
|
||||||
|
stride_k_cache_d,
|
||||||
|
stride_k_cache_bl,
|
||||||
|
stride_k_cache_x,
|
||||||
|
stride_v_cache_bs,
|
||||||
|
stride_v_cache_h,
|
||||||
|
stride_v_cache_d,
|
||||||
|
stride_v_cache_bl,
|
||||||
|
num_queries_per_kv: int,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
):
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head = tl.program_id(1)
|
||||||
|
start_m = tl.program_id(2)
|
||||||
|
|
||||||
|
cur_kv_head = cur_head // num_queries_per_kv
|
||||||
|
|
||||||
|
cur_batch_ctx_len = tl.load(B_Ctxlen + cur_batch)
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
cur_batch_in_all_start_index = tl.load(B_Start_Loc + cur_batch)
|
||||||
|
|
||||||
|
block_start_loc = BLOCK_M * start_m
|
||||||
|
|
||||||
|
# initialize offsets
|
||||||
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
offs_d = tl.arange(0, BLOCK_DMODEL)
|
||||||
|
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||||
|
off_q = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_qbs +
|
||||||
|
cur_head * stride_qh + offs_d[None, :] * stride_qd)
|
||||||
|
|
||||||
|
q = tl.load(Q + off_q,
|
||||||
|
mask=offs_m[:, None]
|
||||||
|
< cur_batch_seq_len - cur_batch_ctx_len,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
# # initialize pointer to m and l
|
||||||
|
m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
|
||||||
|
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
|
||||||
|
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL], dtype=tl.float32)
|
||||||
|
|
||||||
|
for start_n in range(0, cur_batch_ctx_len, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
bn = tl.load(B_Loc + cur_batch * stride_b_loc_b +
|
||||||
|
((start_n + offs_n) // block_size) * stride_b_loc_s,
|
||||||
|
mask=(start_n + offs_n) < cur_batch_ctx_len,
|
||||||
|
other=0)
|
||||||
|
off_k = (bn[None, :] * stride_k_cache_bs +
|
||||||
|
cur_kv_head * stride_k_cache_h +
|
||||||
|
(offs_d[:, None] // x) * stride_k_cache_d +
|
||||||
|
((start_n + offs_n[None, :]) % block_size) *
|
||||||
|
stride_k_cache_bl +
|
||||||
|
(offs_d[:, None] % x) * stride_k_cache_x)
|
||||||
|
off_v = (
|
||||||
|
bn[:, None] * stride_v_cache_bs +
|
||||||
|
cur_kv_head * stride_v_cache_h +
|
||||||
|
offs_d[None, :] * stride_v_cache_d +
|
||||||
|
(start_n + offs_n[:, None]) % block_size * stride_v_cache_bl)
|
||||||
|
k = tl.load(K_cache + off_k,
|
||||||
|
mask=(start_n + offs_n[None, :]) < cur_batch_ctx_len,
|
||||||
|
other=0.0)
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk += tl.dot(q, k)
|
||||||
|
qk = tl.where((start_n + offs_n[None, :]) < cur_batch_ctx_len, qk,
|
||||||
|
float("-inf"))
|
||||||
|
qk *= sm_scale
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1)
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij)
|
||||||
|
p = tl.math.exp(qk - m_i_new[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
|
||||||
|
alpha = tl.math.exp(m_i - m_i_new)
|
||||||
|
l_i_new = alpha * l_i + l_ij
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
# scale acc
|
||||||
|
acc_scale = alpha
|
||||||
|
# acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v = tl.load(V_cache + off_v,
|
||||||
|
mask=(start_n + offs_n[:, None]) < cur_batch_ctx_len,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
acc += tl.dot(p, v)
|
||||||
|
# update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
|
||||||
|
off_k = (offs_n[None, :] * stride_kbs + cur_kv_head * stride_kh +
|
||||||
|
offs_d[:, None] * stride_kd)
|
||||||
|
off_v = (offs_n[:, None] * stride_vbs + cur_kv_head * stride_vh +
|
||||||
|
offs_d[None, :] * stride_vd)
|
||||||
|
k_ptrs = K + off_k
|
||||||
|
v_ptrs = V + off_v
|
||||||
|
|
||||||
|
block_mask = tl.where(
|
||||||
|
block_start_loc < cur_batch_seq_len - cur_batch_ctx_len, 1, 0)
|
||||||
|
|
||||||
|
for start_n in range(0, block_mask * (start_m + 1) * BLOCK_M, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
k = tl.load(k_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_kbs,
|
||||||
|
mask=(start_n + offs_n[None, :])
|
||||||
|
< cur_batch_seq_len - cur_batch_ctx_len,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk += tl.dot(q, k)
|
||||||
|
qk *= sm_scale
|
||||||
|
qk = tl.where(offs_m[:, None] >= (start_n + offs_n[None, :]), qk,
|
||||||
|
float("-inf"))
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1)
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij)
|
||||||
|
p = tl.math.exp(qk - m_i_new[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
|
||||||
|
alpha = tl.math.exp(m_i - m_i_new)
|
||||||
|
l_i_new = alpha * l_i + l_ij
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
# scale acc
|
||||||
|
acc_scale = alpha
|
||||||
|
# acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v = tl.load(v_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_vbs,
|
||||||
|
mask=(start_n + offs_n[:, None])
|
||||||
|
< cur_batch_seq_len - cur_batch_ctx_len,
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
acc += tl.dot(p, v)
|
||||||
|
# update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
|
||||||
|
# acc /= l_i[:, None]
|
||||||
|
# initialize pointers to output
|
||||||
|
off_o = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_obs +
|
||||||
|
cur_head * stride_oh + offs_d[None, :] * stride_od)
|
||||||
|
out_ptrs = Out + off_o
|
||||||
|
tl.store(out_ptrs,
|
||||||
|
acc,
|
||||||
|
mask=offs_m[:, None] < cur_batch_seq_len - cur_batch_ctx_len)
|
||||||
|
return
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_alibi(
|
||||||
|
Q,
|
||||||
|
K,
|
||||||
|
V,
|
||||||
|
K_cache,
|
||||||
|
V_cache,
|
||||||
|
B_Loc,
|
||||||
|
sm_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
B_Start_Loc,
|
||||||
|
B_Seqlen,
|
||||||
|
Alibi_slopes,
|
||||||
|
block_size,
|
||||||
|
x,
|
||||||
|
Out,
|
||||||
|
stride_b_loc_b,
|
||||||
|
stride_b_loc_s,
|
||||||
|
stride_qbs,
|
||||||
|
stride_qh,
|
||||||
|
stride_qd,
|
||||||
|
stride_kbs,
|
||||||
|
stride_kh,
|
||||||
|
stride_kd,
|
||||||
|
stride_vbs,
|
||||||
|
stride_vh,
|
||||||
|
stride_vd,
|
||||||
|
stride_obs,
|
||||||
|
stride_oh,
|
||||||
|
stride_od,
|
||||||
|
stride_k_cache_bs,
|
||||||
|
stride_k_cache_h,
|
||||||
|
stride_k_cache_d,
|
||||||
|
stride_k_cache_bl,
|
||||||
|
stride_k_cache_x,
|
||||||
|
stride_v_cache_bs,
|
||||||
|
stride_v_cache_h,
|
||||||
|
stride_v_cache_d,
|
||||||
|
stride_v_cache_bl,
|
||||||
|
num_queries_per_kv: int,
|
||||||
|
IN_PRECISION: tl.constexpr,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr, # head size
|
||||||
|
BLOCK_DMODEL_PADDED: tl.constexpr, # head size padded to a power of 2
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
SKIP_DECODE: tl.constexpr,
|
||||||
|
):
|
||||||
|
# attn_bias[]
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head = tl.program_id(1)
|
||||||
|
start_m = tl.program_id(2)
|
||||||
|
|
||||||
|
cur_kv_head = cur_head // num_queries_per_kv
|
||||||
|
|
||||||
|
# cur_batch_seq_len: the length of prompts
|
||||||
|
# cur_batch_ctx_len: the length of prefix
|
||||||
|
# cur_batch_in_all_start_index: the start id of the dim=0
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
cur_batch_in_all_start_index = tl.load(B_Start_Loc + cur_batch)
|
||||||
|
cur_batch_in_all_stop_index = tl.load(B_Start_Loc + cur_batch + 1)
|
||||||
|
cur_batch_query_len = (cur_batch_in_all_stop_index -
|
||||||
|
cur_batch_in_all_start_index)
|
||||||
|
cur_batch_ctx_len = cur_batch_seq_len - cur_batch_query_len
|
||||||
|
|
||||||
|
if SKIP_DECODE and cur_batch_query_len == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
block_start_loc = BLOCK_M * start_m
|
||||||
|
|
||||||
|
# initialize offsets
|
||||||
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
offs_d = tl.arange(0, BLOCK_DMODEL_PADDED)
|
||||||
|
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||||
|
off_q = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_qbs +
|
||||||
|
cur_head * stride_qh + offs_d[None, :] * stride_qd)
|
||||||
|
|
||||||
|
dim_mask = tl.where(
|
||||||
|
tl.arange(0, BLOCK_DMODEL_PADDED) < BLOCK_DMODEL, 1, 0).to(tl.int1)
|
||||||
|
|
||||||
|
q = tl.load(Q + off_q,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
(offs_m[:, None] < cur_batch_seq_len - cur_batch_ctx_len),
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
# # initialize pointer to m and l
|
||||||
|
m_i = tl.zeros([BLOCK_M], dtype=tl.float32) - float("inf")
|
||||||
|
l_i = tl.zeros([BLOCK_M], dtype=tl.float32)
|
||||||
|
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL_PADDED], dtype=tl.float32)
|
||||||
|
|
||||||
|
alibi_slope = tl.load(Alibi_slopes + cur_head)
|
||||||
|
alibi_start_q = tl.arange(
|
||||||
|
0, BLOCK_M) + block_start_loc + cur_batch_ctx_len
|
||||||
|
alibi_start_k = 0
|
||||||
|
for start_n in range(0, cur_batch_ctx_len, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
bn = tl.load(B_Loc + cur_batch * stride_b_loc_b +
|
||||||
|
((start_n + offs_n) // block_size) * stride_b_loc_s,
|
||||||
|
mask=(start_n + offs_n) < cur_batch_ctx_len,
|
||||||
|
other=0)
|
||||||
|
off_k = (bn[None, :] * stride_k_cache_bs +
|
||||||
|
cur_kv_head * stride_k_cache_h +
|
||||||
|
(offs_d[:, None] // x) * stride_k_cache_d +
|
||||||
|
((start_n + offs_n[None, :]) % block_size) *
|
||||||
|
stride_k_cache_bl +
|
||||||
|
(offs_d[:, None] % x) * stride_k_cache_x)
|
||||||
|
off_v = (
|
||||||
|
bn[:, None] * stride_v_cache_bs +
|
||||||
|
cur_kv_head * stride_v_cache_h +
|
||||||
|
offs_d[None, :] * stride_v_cache_d +
|
||||||
|
(start_n + offs_n[:, None]) % block_size * stride_v_cache_bl)
|
||||||
|
k_load = tl.load(K_cache + off_k,
|
||||||
|
mask=dim_mask[:, None] &
|
||||||
|
((start_n + offs_n[None, :]) < cur_batch_ctx_len),
|
||||||
|
other=0.0) # [D,N]
|
||||||
|
|
||||||
|
if k_load.dtype.is_fp8():
|
||||||
|
k = (k_load.to(tl.float32) * tl.load(k_scale)).to(q.dtype)
|
||||||
|
else:
|
||||||
|
k = k_load
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk = tl.dot(q, k, acc=qk, input_precision=IN_PRECISION)
|
||||||
|
qk = tl.where((start_n + offs_n[None, :]) < cur_batch_ctx_len, qk,
|
||||||
|
float("-inf"))
|
||||||
|
qk *= sm_scale
|
||||||
|
|
||||||
|
# load alibi
|
||||||
|
alibi = (tl.arange(0, BLOCK_N)[None, :] + alibi_start_k -
|
||||||
|
alibi_start_q[:, None]) * alibi_slope
|
||||||
|
alibi = tl.where(
|
||||||
|
(alibi <= 0) & (alibi_start_q[:, None] < cur_batch_seq_len),
|
||||||
|
alibi, float("-inf"))
|
||||||
|
qk += alibi
|
||||||
|
alibi_start_k += BLOCK_N
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1)
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij)
|
||||||
|
p = tl.math.exp(qk - m_i_new[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
|
||||||
|
alpha = tl.math.exp(m_i - m_i_new)
|
||||||
|
l_i_new = alpha * l_i + l_ij
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
# scale acc
|
||||||
|
acc_scale = alpha
|
||||||
|
# acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v_load = tl.load(V_cache + off_v,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
((start_n + offs_n[:, None]) < cur_batch_ctx_len),
|
||||||
|
other=0.0)
|
||||||
|
if v_load.dtype.is_fp8():
|
||||||
|
v = (v_load.to(tl.float32) * tl.load(v_scale)).to(q.dtype)
|
||||||
|
else:
|
||||||
|
v = v_load
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
|
||||||
|
acc = tl.dot(p, v, acc=acc, input_precision='ieee')
|
||||||
|
# update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
|
||||||
|
off_k = (offs_n[None, :] * stride_kbs + cur_kv_head * stride_kh +
|
||||||
|
offs_d[:, None] * stride_kd)
|
||||||
|
off_v = (offs_n[:, None] * stride_vbs + cur_kv_head * stride_vh +
|
||||||
|
offs_d[None, :] * stride_vd)
|
||||||
|
k_ptrs = K + off_k
|
||||||
|
v_ptrs = V + off_v
|
||||||
|
|
||||||
|
block_mask = tl.where(
|
||||||
|
block_start_loc < cur_batch_seq_len - cur_batch_ctx_len, 1, 0)
|
||||||
|
|
||||||
|
# init alibi
|
||||||
|
alibi_slope = tl.load(Alibi_slopes + cur_head)
|
||||||
|
alibi_start_q = tl.arange(
|
||||||
|
0, BLOCK_M) + block_start_loc + cur_batch_ctx_len
|
||||||
|
alibi_start_k = cur_batch_ctx_len
|
||||||
|
# # init debugger
|
||||||
|
# offset_db_q = tl.arange(0, BLOCK_M) + block_start_loc
|
||||||
|
# offset_db_k = tl.arange(0, BLOCK_N)
|
||||||
|
# calc q[BLOCK_M, BLOCK_MODEL] mul k[prefix_len: , BLOCK_DMODEL]
|
||||||
|
for start_n in range(0, block_mask * (start_m + 1) * BLOCK_M, BLOCK_N):
|
||||||
|
start_n = tl.multiple_of(start_n, BLOCK_N)
|
||||||
|
# -- compute qk ----
|
||||||
|
k = tl.load(k_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_kbs,
|
||||||
|
mask=dim_mask[:, None] &
|
||||||
|
((start_n + offs_n[None, :])
|
||||||
|
< cur_batch_seq_len - cur_batch_ctx_len),
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
qk = tl.dot(q, k, acc=qk, input_precision='ieee')
|
||||||
|
qk *= sm_scale
|
||||||
|
qk = tl.where(offs_m[:, None] >= (start_n + offs_n[None, :]), qk,
|
||||||
|
float("-inf"))
|
||||||
|
|
||||||
|
# load alibi
|
||||||
|
alibi = (tl.arange(0, BLOCK_N)[None, :] + alibi_start_k -
|
||||||
|
alibi_start_q[:, None]) * alibi_slope
|
||||||
|
alibi = tl.where(
|
||||||
|
(alibi <= 0) & (alibi_start_q[:, None] < cur_batch_seq_len),
|
||||||
|
alibi, float("-inf"))
|
||||||
|
qk += alibi
|
||||||
|
alibi_start_k += BLOCK_N
|
||||||
|
|
||||||
|
# -- compute m_ij, p, l_ij
|
||||||
|
m_ij = tl.max(qk, 1)
|
||||||
|
m_i_new = tl.maximum(m_i, m_ij)
|
||||||
|
p = tl.math.exp(qk - m_i_new[:, None])
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
|
||||||
|
alpha = tl.math.exp(m_i - m_i_new)
|
||||||
|
l_i_new = alpha * l_i + l_ij
|
||||||
|
# -- update output accumulator --
|
||||||
|
# scale p
|
||||||
|
# scale acc
|
||||||
|
acc_scale = alpha
|
||||||
|
# acc_scale = l_i / l_i_new * alpha
|
||||||
|
acc = acc * acc_scale[:, None]
|
||||||
|
# update acc
|
||||||
|
v = tl.load(v_ptrs +
|
||||||
|
(cur_batch_in_all_start_index + start_n) * stride_vbs,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
((start_n + offs_n[:, None])
|
||||||
|
< cur_batch_seq_len - cur_batch_ctx_len),
|
||||||
|
other=0.0)
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
|
||||||
|
acc = tl.dot(p, v, acc=acc, input_precision='ieee')
|
||||||
|
# update m_i and l_i
|
||||||
|
l_i = l_i_new
|
||||||
|
m_i = m_i_new
|
||||||
|
|
||||||
|
acc = acc / l_i[:, None]
|
||||||
|
|
||||||
|
# initialize pointers to output
|
||||||
|
off_o = (
|
||||||
|
(cur_batch_in_all_start_index + offs_m[:, None]) * stride_obs +
|
||||||
|
cur_head * stride_oh + offs_d[None, :] * stride_od)
|
||||||
|
out_ptrs = Out + off_o
|
||||||
|
tl.store(out_ptrs,
|
||||||
|
acc,
|
||||||
|
mask=dim_mask[None, :] &
|
||||||
|
(offs_m[:, None] < cur_batch_seq_len - cur_batch_ctx_len))
|
||||||
|
return
|
||||||
|
|
||||||
|
@torch.inference_mode()
|
||||||
|
def context_attention_fwd(q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
o,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_cache,
|
||||||
|
v_cache,
|
||||||
|
b_loc,
|
||||||
|
b_start_loc,
|
||||||
|
b_seq_len,
|
||||||
|
max_seq_len,
|
||||||
|
max_input_len,
|
||||||
|
k_scale: torch.Tensor,
|
||||||
|
v_scale: torch.Tensor,
|
||||||
|
alibi_slopes=None,
|
||||||
|
sliding_window=None,
|
||||||
|
sm_scale=None,
|
||||||
|
skip_decode=False):
|
||||||
|
|
||||||
|
q_dtype_is_f32 = q.dtype is torch.float32
|
||||||
|
# need to reduce num. blocks when using fp32
|
||||||
|
# due to increased use of GPU shared memory
|
||||||
|
# if q.dtype is torch.float32:
|
||||||
|
BLOCK = BASE_BLOCK // 2 if q_dtype_is_f32 else BASE_BLOCK
|
||||||
|
|
||||||
|
# Turing does have tensor core for float32 multiplication
|
||||||
|
# use ieee as fallback for triton kernels work. There is also
|
||||||
|
# warning on vllm/config.py to inform users this fallback
|
||||||
|
# implementation
|
||||||
|
IN_PRECISION = 'ieee' if IS_TURING and q_dtype_is_f32 else None
|
||||||
|
|
||||||
|
# Conversion of FP8 Tensor from uint8 storage to
|
||||||
|
# appropriate torch.dtype for interpretation by Triton
|
||||||
|
if "fp8" in kv_cache_dtype:
|
||||||
|
assert (k_cache.dtype == torch.uint8)
|
||||||
|
assert (v_cache.dtype == torch.uint8)
|
||||||
|
|
||||||
|
if kv_cache_dtype in ("fp8", "fp8_e4m3"):
|
||||||
|
target_dtype = current_platform.fp8_dtype()
|
||||||
|
elif kv_cache_dtype == "fp8_e5m2":
|
||||||
|
target_dtype = torch.float8_e5m2
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported FP8 dtype:", kv_cache_dtype)
|
||||||
|
|
||||||
|
k_cache = k_cache.view(target_dtype)
|
||||||
|
v_cache = v_cache.view(target_dtype)
|
||||||
|
|
||||||
|
if (k_cache.dtype == torch.uint8
|
||||||
|
or v_cache.dtype == torch.uint8 and kv_cache_dtype == "auto"):
|
||||||
|
raise ValueError("kv_cache_dtype='auto' unsupported for\
|
||||||
|
FP8 KV Cache prefill kernel")
|
||||||
|
|
||||||
|
# shape constraints
|
||||||
|
Lq, Lk, Lv = q.shape[-1], k.shape[-1], v.shape[-1]
|
||||||
|
assert Lq == Lk and Lk == Lv
|
||||||
|
# round up Lk to a power of 2 - this is required for Triton block size
|
||||||
|
Lk_padded = triton.next_power_of_2(Lk)
|
||||||
|
|
||||||
|
if sm_scale is None:
|
||||||
|
sm_scale = 1.0 / (Lq**0.5)
|
||||||
|
batch, head = b_seq_len.shape[0], q.shape[1]
|
||||||
|
num_queries_per_kv = q.shape[1] // k.shape[1]
|
||||||
|
|
||||||
|
assert batch + 1 == len(b_start_loc)
|
||||||
|
grid = (batch, head, triton.cdiv(max_input_len, BLOCK)) # batch, head,
|
||||||
|
|
||||||
|
# 0 means "disable"
|
||||||
|
if sliding_window is None or sliding_window <= 0:
|
||||||
|
sliding_window = 0
|
||||||
|
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
_fwd_kernel_alibi[grid](
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
k_cache,
|
||||||
|
v_cache,
|
||||||
|
b_loc,
|
||||||
|
sm_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
b_start_loc,
|
||||||
|
b_seq_len,
|
||||||
|
alibi_slopes,
|
||||||
|
v_cache.shape[3],
|
||||||
|
k_cache.shape[4],
|
||||||
|
o,
|
||||||
|
b_loc.stride(0),
|
||||||
|
b_loc.stride(1),
|
||||||
|
q.stride(0),
|
||||||
|
q.stride(1),
|
||||||
|
q.stride(2),
|
||||||
|
k.stride(0),
|
||||||
|
k.stride(1),
|
||||||
|
k.stride(2),
|
||||||
|
v.stride(0),
|
||||||
|
v.stride(1),
|
||||||
|
v.stride(2),
|
||||||
|
o.stride(0),
|
||||||
|
o.stride(1),
|
||||||
|
o.stride(2),
|
||||||
|
k_cache.stride(0),
|
||||||
|
k_cache.stride(1),
|
||||||
|
k_cache.stride(2),
|
||||||
|
k_cache.stride(3),
|
||||||
|
k_cache.stride(
|
||||||
|
4
|
||||||
|
), #[num_blocks, num_kv_heads, head_size/x, block_size, x]
|
||||||
|
v_cache.stride(0),
|
||||||
|
v_cache.stride(1),
|
||||||
|
v_cache.stride(2),
|
||||||
|
v_cache.stride(
|
||||||
|
3), #[num_blocks, num_kv_heads, head_size, block_size]
|
||||||
|
num_queries_per_kv=num_queries_per_kv,
|
||||||
|
IN_PRECISION=IN_PRECISION,
|
||||||
|
BLOCK_M=BLOCK,
|
||||||
|
BLOCK_DMODEL=Lk,
|
||||||
|
BLOCK_DMODEL_PADDED=Lk_padded,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
SKIP_DECODE=skip_decode,
|
||||||
|
num_warps=NUM_WARPS,
|
||||||
|
num_stages=1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_fwd_kernel[grid](
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
k_cache,
|
||||||
|
v_cache,
|
||||||
|
b_loc,
|
||||||
|
sm_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
b_start_loc,
|
||||||
|
b_seq_len,
|
||||||
|
v_cache.shape[3],
|
||||||
|
k_cache.shape[4],
|
||||||
|
o,
|
||||||
|
b_loc.stride(0),
|
||||||
|
b_loc.stride(1),
|
||||||
|
q.stride(0),
|
||||||
|
q.stride(1),
|
||||||
|
q.stride(2),
|
||||||
|
k.stride(0),
|
||||||
|
k.stride(1),
|
||||||
|
k.stride(2),
|
||||||
|
v.stride(0),
|
||||||
|
v.stride(1),
|
||||||
|
v.stride(2),
|
||||||
|
o.stride(0),
|
||||||
|
o.stride(1),
|
||||||
|
o.stride(2),
|
||||||
|
k_cache.stride(0),
|
||||||
|
k_cache.stride(1),
|
||||||
|
k_cache.stride(2),
|
||||||
|
k_cache.stride(3),
|
||||||
|
k_cache.stride(
|
||||||
|
4), #[num_blocks, num_kv_heads, head_size/x, block_size, x]
|
||||||
|
v_cache.stride(0),
|
||||||
|
v_cache.stride(1),
|
||||||
|
v_cache.stride(2),
|
||||||
|
v_cache.stride(
|
||||||
|
3), #[num_blocks, num_kv_heads, head_size, block_size]
|
||||||
|
num_queries_per_kv=num_queries_per_kv,
|
||||||
|
IN_PRECISION=IN_PRECISION,
|
||||||
|
BLOCK_M=BLOCK,
|
||||||
|
BLOCK_DMODEL=Lk,
|
||||||
|
BLOCK_DMODEL_PADDED=Lk_padded,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
SLIDING_WINDOW=sliding_window,
|
||||||
|
SKIP_DECODE=skip_decode,
|
||||||
|
num_warps=NUM_WARPS,
|
||||||
|
num_stages=1,
|
||||||
|
)
|
||||||
|
return
|
||||||
674
vllm/attention/ops/triton_decode_attention.py
Normal file
674
vllm/attention/ops/triton_decode_attention.py
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# Adapted from
|
||||||
|
# https://github.com/sgl-project/sglang/blob/9f635ea50de920aa507f486daafba26a5b837574/python/sglang/srt/layers/attention/triton_ops/decode_attention.py
|
||||||
|
# which was originally adapted from
|
||||||
|
# https://github.com/ModelTC/lightllm/blob/96353e868a840db4d103138caf15ed9dbea8c186/lightllm/models/deepseek2/triton_kernel/gqa_flash_decoding_stage1.py
|
||||||
|
# https://github.com/ModelTC/lightllm/blob/96353e868a840db4d103138caf15ed9dbea8c186/lightllm/models/deepseek2/triton_kernel/gqa_flash_decoding_stage2.py
|
||||||
|
|
||||||
|
# Changes:
|
||||||
|
# - Add support for page size >= 1.
|
||||||
|
|
||||||
|
# Copyright 2025 vLLM Team
|
||||||
|
# Copyright 2023-2024 SGLang Team
|
||||||
|
# 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.
|
||||||
|
# ==============================================================================
|
||||||
|
"""
|
||||||
|
Memory-efficient attention for decoding.
|
||||||
|
It supports page size >= 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
is_hip_ = current_platform.is_rocm()
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# TODO: Remove this when triton>=3.2.0. This issue will not affect performance
|
||||||
|
# and accuracy.
|
||||||
|
logger.warning(
|
||||||
|
"The following error message 'operation scheduled before its operands' "
|
||||||
|
"can be ignored.")
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def tanh(x):
|
||||||
|
# Tanh is just a scaled sigmoid
|
||||||
|
return 2 * tl.sigmoid(2 * x) - 1
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_stage1(
|
||||||
|
Q,
|
||||||
|
K_Buffer,
|
||||||
|
V_Buffer,
|
||||||
|
sm_scale,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
Att_Out,
|
||||||
|
stride_req_to_tokens_b,
|
||||||
|
stride_qbs,
|
||||||
|
stride_qh,
|
||||||
|
stride_buf_kbs,
|
||||||
|
stride_buf_kh,
|
||||||
|
stride_buf_vbs,
|
||||||
|
stride_buf_vh,
|
||||||
|
stride_mid_ob,
|
||||||
|
stride_mid_oh,
|
||||||
|
stride_mid_os,
|
||||||
|
kv_group_num: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr,
|
||||||
|
BLOCK_DV: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
NUM_KV_SPLITS: tl.constexpr,
|
||||||
|
PAGE_SIZE: tl.constexpr,
|
||||||
|
logit_cap: tl.constexpr,
|
||||||
|
Lk: tl.constexpr,
|
||||||
|
Lv: tl.constexpr,
|
||||||
|
):
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head = tl.program_id(1)
|
||||||
|
split_kv_id = tl.program_id(2)
|
||||||
|
|
||||||
|
cur_kv_head = cur_head // kv_group_num
|
||||||
|
|
||||||
|
offs_d = tl.arange(0, BLOCK_DMODEL)
|
||||||
|
offs_dv = tl.arange(0, BLOCK_DV)
|
||||||
|
mask_d = offs_d < Lk
|
||||||
|
mask_dv = offs_dv < Lv
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
cur_batch_req_idx = cur_batch
|
||||||
|
|
||||||
|
off_q = cur_batch * stride_qbs + cur_head * stride_qh + offs_d
|
||||||
|
q = tl.load(Q + off_q, mask=mask_d, other=0.0)
|
||||||
|
|
||||||
|
kv_len_per_split = tl.cdiv(cur_batch_seq_len, NUM_KV_SPLITS)
|
||||||
|
split_kv_start = kv_len_per_split * split_kv_id
|
||||||
|
split_kv_end = tl.minimum(split_kv_start + kv_len_per_split,
|
||||||
|
cur_batch_seq_len)
|
||||||
|
|
||||||
|
e_max = -float("inf")
|
||||||
|
e_sum = 0.0
|
||||||
|
acc = tl.zeros([BLOCK_DV], dtype=tl.float32)
|
||||||
|
|
||||||
|
if split_kv_end > split_kv_start:
|
||||||
|
for start_n in range(split_kv_start, split_kv_end, BLOCK_N):
|
||||||
|
offs_n = start_n + tl.arange(0, BLOCK_N)
|
||||||
|
kv_page_number = tl.load(
|
||||||
|
Req_to_tokens + stride_req_to_tokens_b * cur_batch_req_idx +
|
||||||
|
offs_n // PAGE_SIZE,
|
||||||
|
mask=offs_n < split_kv_end,
|
||||||
|
other=0,
|
||||||
|
)
|
||||||
|
kv_loc = kv_page_number * PAGE_SIZE + offs_n % PAGE_SIZE
|
||||||
|
offs_buf_k = (kv_loc[:, None] * stride_buf_kbs +
|
||||||
|
cur_kv_head * stride_buf_kh + offs_d[None, :])
|
||||||
|
k = tl.load(
|
||||||
|
K_Buffer + offs_buf_k,
|
||||||
|
mask=(offs_n[:, None] < split_kv_end) & (mask_d[None, :]),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
qk = tl.sum(q[None, :] * k, 1)
|
||||||
|
qk *= sm_scale
|
||||||
|
|
||||||
|
if logit_cap > 0:
|
||||||
|
qk = logit_cap * tanh(qk / logit_cap)
|
||||||
|
|
||||||
|
qk = tl.where(offs_n < split_kv_end, qk, float("-inf"))
|
||||||
|
|
||||||
|
offs_buf_v = (kv_loc[:, None] * stride_buf_vbs +
|
||||||
|
cur_kv_head * stride_buf_vh + offs_dv[None, :])
|
||||||
|
v = tl.load(
|
||||||
|
V_Buffer + offs_buf_v,
|
||||||
|
mask=(offs_n[:, None] < split_kv_end) & (mask_dv[None, :]),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
n_e_max = tl.maximum(tl.max(qk, 0), e_max)
|
||||||
|
re_scale = tl.exp(e_max - n_e_max)
|
||||||
|
p = tl.exp(qk - n_e_max)
|
||||||
|
acc *= re_scale
|
||||||
|
acc += tl.sum(p[:, None] * v, 0)
|
||||||
|
|
||||||
|
e_sum = e_sum * re_scale + tl.sum(p, 0)
|
||||||
|
e_max = n_e_max
|
||||||
|
|
||||||
|
offs_mid_o = (cur_batch * stride_mid_ob + cur_head * stride_mid_oh +
|
||||||
|
split_kv_id * stride_mid_os + offs_dv)
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
Att_Out + offs_mid_o,
|
||||||
|
acc / e_sum,
|
||||||
|
mask=(mask_dv),
|
||||||
|
)
|
||||||
|
|
||||||
|
offs_mid_o_1 = (cur_batch * stride_mid_ob + cur_head * stride_mid_oh +
|
||||||
|
split_kv_id * stride_mid_os + Lv)
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
Att_Out + offs_mid_o_1,
|
||||||
|
e_max + tl.log(e_sum),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_att_m_fwd(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
att_out,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
):
|
||||||
|
BLOCK = 64 if not is_hip_ else 8
|
||||||
|
|
||||||
|
NUM_KV_SPLITS = num_kv_splits
|
||||||
|
Lk = k_buffer.shape[-1]
|
||||||
|
Lv = v_buffer.shape[-1]
|
||||||
|
|
||||||
|
batch, head_num = q.shape[0], q.shape[1]
|
||||||
|
|
||||||
|
grid = (batch, head_num, NUM_KV_SPLITS)
|
||||||
|
kv_group_num = q.shape[1] // k_buffer.shape[-2]
|
||||||
|
|
||||||
|
num_warps = 4
|
||||||
|
if kv_group_num != 1:
|
||||||
|
num_warps = 1 if is_hip_ else 2
|
||||||
|
|
||||||
|
BLOCK_DMODEL = triton.next_power_of_2(Lk)
|
||||||
|
BLOCK_DV = triton.next_power_of_2(Lv)
|
||||||
|
|
||||||
|
_fwd_kernel_stage1[grid](
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
sm_scale,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
att_out,
|
||||||
|
Req_to_tokens.stride(0),
|
||||||
|
q.stride(0),
|
||||||
|
q.stride(1),
|
||||||
|
k_buffer.stride(-3), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
k_buffer.stride(-2), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
v_buffer.stride(-3), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
v_buffer.stride(-2), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
att_out.stride(0),
|
||||||
|
att_out.stride(1),
|
||||||
|
att_out.stride(2),
|
||||||
|
kv_group_num=kv_group_num,
|
||||||
|
BLOCK_DMODEL=BLOCK_DMODEL,
|
||||||
|
BLOCK_DV=BLOCK_DV,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
NUM_KV_SPLITS=NUM_KV_SPLITS,
|
||||||
|
PAGE_SIZE=page_size,
|
||||||
|
logit_cap=logit_cap,
|
||||||
|
num_warps=num_warps,
|
||||||
|
num_stages=2,
|
||||||
|
Lk=Lk,
|
||||||
|
Lv=Lv,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_grouped_kernel_stage1(
|
||||||
|
Q,
|
||||||
|
K_Buffer,
|
||||||
|
V_Buffer,
|
||||||
|
sm_scale,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
Att_Out,
|
||||||
|
stride_req_to_tokens_b,
|
||||||
|
stride_qbs,
|
||||||
|
stride_qh,
|
||||||
|
stride_buf_kbs,
|
||||||
|
stride_buf_kh,
|
||||||
|
stride_buf_vbs,
|
||||||
|
stride_buf_vh,
|
||||||
|
stride_mid_ob,
|
||||||
|
stride_mid_oh,
|
||||||
|
stride_mid_os,
|
||||||
|
kv_group_num: tl.constexpr,
|
||||||
|
q_head_num: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr,
|
||||||
|
BLOCK_DPE: tl.constexpr,
|
||||||
|
BLOCK_DV: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
BLOCK_H: tl.constexpr,
|
||||||
|
NUM_KV_SPLITS: tl.constexpr,
|
||||||
|
PAGE_SIZE: tl.constexpr,
|
||||||
|
logit_cap: tl.constexpr,
|
||||||
|
Lk: tl.constexpr,
|
||||||
|
Lv: tl.constexpr,
|
||||||
|
):
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head_id = tl.program_id(1)
|
||||||
|
cur_kv_head = cur_head_id // tl.cdiv(kv_group_num, BLOCK_H)
|
||||||
|
split_kv_id = tl.program_id(2)
|
||||||
|
|
||||||
|
if kv_group_num > BLOCK_H:
|
||||||
|
VALID_BLOCK_H: tl.constexpr = BLOCK_H
|
||||||
|
else:
|
||||||
|
VALID_BLOCK_H: tl.constexpr = kv_group_num
|
||||||
|
cur_head = cur_head_id * VALID_BLOCK_H + tl.arange(0, BLOCK_H)
|
||||||
|
mask_h = cur_head < (cur_head_id + 1) * VALID_BLOCK_H
|
||||||
|
mask_h = mask_h & (cur_head < q_head_num)
|
||||||
|
|
||||||
|
offs_d = tl.arange(0, BLOCK_DMODEL)
|
||||||
|
offs_dv = tl.arange(0, BLOCK_DV)
|
||||||
|
mask_d = offs_d < Lk
|
||||||
|
mask_dv = offs_dv < Lv
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
cur_batch_req_idx = cur_batch
|
||||||
|
|
||||||
|
offs_q = cur_batch * stride_qbs + cur_head[:, None] * stride_qh + offs_d[
|
||||||
|
None, :]
|
||||||
|
q = tl.load(Q + offs_q,
|
||||||
|
mask=(mask_h[:, None]) & (mask_d[None, :]),
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
if BLOCK_DPE > 0:
|
||||||
|
offs_dpe = BLOCK_DMODEL + tl.arange(0, BLOCK_DPE)
|
||||||
|
mask_dpe = offs_dpe < Lk
|
||||||
|
off_qpe = (cur_batch * stride_qbs + cur_head[:, None] * stride_qh +
|
||||||
|
offs_dpe[None, :])
|
||||||
|
qpe = tl.load(Q + off_qpe,
|
||||||
|
mask=(mask_h[:, None]) & (mask_dpe[None, :]),
|
||||||
|
other=0.0)
|
||||||
|
|
||||||
|
kv_len_per_split = tl.cdiv(cur_batch_seq_len, NUM_KV_SPLITS)
|
||||||
|
split_kv_start = kv_len_per_split * split_kv_id
|
||||||
|
split_kv_end = tl.minimum(split_kv_start + kv_len_per_split,
|
||||||
|
cur_batch_seq_len)
|
||||||
|
|
||||||
|
e_max = tl.zeros([BLOCK_H], dtype=tl.float32) - float("inf")
|
||||||
|
e_sum = tl.zeros([BLOCK_H], dtype=tl.float32)
|
||||||
|
acc = tl.zeros([BLOCK_H, BLOCK_DV], dtype=tl.float32)
|
||||||
|
|
||||||
|
if split_kv_end > split_kv_start:
|
||||||
|
for start_n in range(split_kv_start, split_kv_end, BLOCK_N):
|
||||||
|
offs_n = start_n + tl.arange(0, BLOCK_N)
|
||||||
|
kv_page_number = tl.load(
|
||||||
|
Req_to_tokens + stride_req_to_tokens_b * cur_batch_req_idx +
|
||||||
|
offs_n // PAGE_SIZE,
|
||||||
|
mask=offs_n < split_kv_end,
|
||||||
|
other=0,
|
||||||
|
)
|
||||||
|
kv_loc = kv_page_number * PAGE_SIZE + offs_n % PAGE_SIZE
|
||||||
|
offs_buf_k = (kv_loc[None, :] * stride_buf_kbs +
|
||||||
|
cur_kv_head * stride_buf_kh + offs_d[:, None])
|
||||||
|
k = tl.load(
|
||||||
|
K_Buffer + offs_buf_k,
|
||||||
|
mask=(offs_n[None, :] < split_kv_end) & (mask_d[:, None]),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
qk = tl.dot(q, k.to(q.dtype))
|
||||||
|
if BLOCK_DPE > 0:
|
||||||
|
offs_buf_kpe = (kv_loc[None, :] * stride_buf_kbs +
|
||||||
|
cur_kv_head * stride_buf_kh +
|
||||||
|
offs_dpe[:, None])
|
||||||
|
kpe = tl.load(
|
||||||
|
K_Buffer + offs_buf_kpe,
|
||||||
|
mask=(offs_n[None, :] < split_kv_end) &
|
||||||
|
(mask_dpe[:, None]),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
qk += tl.dot(qpe, kpe.to(qpe.dtype))
|
||||||
|
qk *= sm_scale
|
||||||
|
|
||||||
|
if logit_cap > 0:
|
||||||
|
qk = logit_cap * tanh(qk / logit_cap)
|
||||||
|
|
||||||
|
qk = tl.where(mask_h[:, None] & (offs_n[None, :] < split_kv_end),
|
||||||
|
qk, float("-inf"))
|
||||||
|
|
||||||
|
offs_buf_v = (kv_loc[:, None] * stride_buf_vbs +
|
||||||
|
cur_kv_head * stride_buf_vh + offs_dv[None, :])
|
||||||
|
v = tl.load(
|
||||||
|
V_Buffer + offs_buf_v,
|
||||||
|
mask=(offs_n[:, None] < split_kv_end) & (mask_dv[None, :]),
|
||||||
|
other=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
n_e_max = tl.maximum(tl.max(qk, 1), e_max)
|
||||||
|
re_scale = tl.exp(e_max - n_e_max)
|
||||||
|
p = tl.exp(qk - n_e_max[:, None])
|
||||||
|
acc *= re_scale[:, None]
|
||||||
|
acc += tl.dot(p.to(v.dtype), v)
|
||||||
|
|
||||||
|
e_sum = e_sum * re_scale + tl.sum(p, 1)
|
||||||
|
e_max = n_e_max
|
||||||
|
|
||||||
|
offs_mid_o = (cur_batch * stride_mid_ob +
|
||||||
|
cur_head[:, None] * stride_mid_oh +
|
||||||
|
split_kv_id * stride_mid_os + offs_dv[None, :])
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
Att_Out + offs_mid_o,
|
||||||
|
acc / e_sum[:, None],
|
||||||
|
mask=(mask_h[:, None]) & (mask_dv[None, :]),
|
||||||
|
)
|
||||||
|
|
||||||
|
offs_mid_o_1 = (cur_batch * stride_mid_ob + cur_head * stride_mid_oh +
|
||||||
|
split_kv_id * stride_mid_os + Lv)
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
Att_Out + offs_mid_o_1,
|
||||||
|
e_max + tl.log(e_sum),
|
||||||
|
mask=mask_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_grouped_att_m_fwd(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
att_out,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
):
|
||||||
|
BLOCK = 32
|
||||||
|
Lk = k_buffer.shape[-1]
|
||||||
|
Lv = v_buffer.shape[-1]
|
||||||
|
|
||||||
|
# [TODO] work around shmem limit on MI3xx
|
||||||
|
if is_hip_ and Lk >= 576:
|
||||||
|
BLOCK = 16
|
||||||
|
|
||||||
|
if Lk == 576:
|
||||||
|
BLOCK_DMODEL = 512
|
||||||
|
BLOCK_DPE = 64
|
||||||
|
elif Lk == 288:
|
||||||
|
BLOCK_DMODEL = 256
|
||||||
|
BLOCK_DPE = 32
|
||||||
|
else:
|
||||||
|
BLOCK_DMODEL = triton.next_power_of_2(Lk)
|
||||||
|
BLOCK_DPE = 0
|
||||||
|
BLOCK_DV = triton.next_power_of_2(Lv)
|
||||||
|
|
||||||
|
batch, head_num = q.shape[0], q.shape[1]
|
||||||
|
kv_group_num = q.shape[1] // k_buffer.shape[-2]
|
||||||
|
|
||||||
|
BLOCK_H = 16
|
||||||
|
NUM_KV_SPLITS = num_kv_splits
|
||||||
|
grid = (
|
||||||
|
batch,
|
||||||
|
triton.cdiv(head_num, min(BLOCK_H, kv_group_num)),
|
||||||
|
NUM_KV_SPLITS,
|
||||||
|
)
|
||||||
|
|
||||||
|
extra_kargs = {}
|
||||||
|
num_stages = 2
|
||||||
|
if is_hip_:
|
||||||
|
# https://rocm.docs.amd.com/en/latest/how-to/rocm-for-ai/inference-optimization/workload.html#mi300x-triton-kernel-performance-optimization
|
||||||
|
# https://github.com/triton-lang/triton/blob/main/third_party/amd/backend/compiler.py
|
||||||
|
extra_kargs = {
|
||||||
|
"waves_per_eu": 1,
|
||||||
|
"matrix_instr_nonkdim": 16,
|
||||||
|
"kpack": 2
|
||||||
|
}
|
||||||
|
num_stages = 1
|
||||||
|
|
||||||
|
_fwd_grouped_kernel_stage1[grid](
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
sm_scale,
|
||||||
|
Req_to_tokens,
|
||||||
|
B_Seqlen,
|
||||||
|
att_out,
|
||||||
|
Req_to_tokens.stride(0),
|
||||||
|
q.stride(0),
|
||||||
|
q.stride(1),
|
||||||
|
k_buffer.stride(-3), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
k_buffer.stride(-2), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
v_buffer.stride(-3), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
v_buffer.stride(-2), # Assume (..., PAGE_SIZE, NUM_HEADS, HEAD_DIM)
|
||||||
|
att_out.stride(0),
|
||||||
|
att_out.stride(1),
|
||||||
|
att_out.stride(2),
|
||||||
|
kv_group_num=kv_group_num,
|
||||||
|
q_head_num=head_num,
|
||||||
|
BLOCK_DMODEL=BLOCK_DMODEL,
|
||||||
|
BLOCK_DPE=BLOCK_DPE,
|
||||||
|
BLOCK_DV=BLOCK_DV,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
BLOCK_H=BLOCK_H,
|
||||||
|
NUM_KV_SPLITS=NUM_KV_SPLITS,
|
||||||
|
PAGE_SIZE=page_size,
|
||||||
|
logit_cap=logit_cap,
|
||||||
|
num_warps=4,
|
||||||
|
num_stages=num_stages,
|
||||||
|
Lk=Lk,
|
||||||
|
Lv=Lv,
|
||||||
|
**extra_kargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _fwd_kernel_stage2(
|
||||||
|
Mid_O,
|
||||||
|
o,
|
||||||
|
B_Seqlen,
|
||||||
|
stride_mid_ob,
|
||||||
|
stride_mid_oh,
|
||||||
|
stride_mid_os,
|
||||||
|
stride_obs,
|
||||||
|
stride_oh,
|
||||||
|
NUM_KV_SPLITS: tl.constexpr,
|
||||||
|
BLOCK_DV: tl.constexpr,
|
||||||
|
Lv: tl.constexpr,
|
||||||
|
):
|
||||||
|
cur_batch = tl.program_id(0)
|
||||||
|
cur_head = tl.program_id(1)
|
||||||
|
|
||||||
|
cur_batch_seq_len = tl.load(B_Seqlen + cur_batch)
|
||||||
|
|
||||||
|
offs_d = tl.arange(0, BLOCK_DV)
|
||||||
|
mask_d = offs_d < Lv
|
||||||
|
|
||||||
|
e_sum = 0.0
|
||||||
|
e_max = -float("inf")
|
||||||
|
acc = tl.zeros([BLOCK_DV], dtype=tl.float32)
|
||||||
|
|
||||||
|
offs_v = cur_batch * stride_mid_ob + cur_head * stride_mid_oh + offs_d
|
||||||
|
offs_logic = cur_batch * stride_mid_ob + cur_head * stride_mid_oh + Lv
|
||||||
|
|
||||||
|
for split_kv_id in range(0, NUM_KV_SPLITS):
|
||||||
|
kv_len_per_split = tl.cdiv(cur_batch_seq_len, NUM_KV_SPLITS)
|
||||||
|
split_kv_start = kv_len_per_split * split_kv_id
|
||||||
|
split_kv_end = tl.minimum(split_kv_start + kv_len_per_split,
|
||||||
|
cur_batch_seq_len)
|
||||||
|
|
||||||
|
if split_kv_end > split_kv_start:
|
||||||
|
tv = tl.load(Mid_O + offs_v + split_kv_id * stride_mid_os,
|
||||||
|
mask=mask_d,
|
||||||
|
other=0.0)
|
||||||
|
tlogic = tl.load(Mid_O + offs_logic + split_kv_id * stride_mid_os)
|
||||||
|
n_e_max = tl.maximum(tlogic, e_max)
|
||||||
|
|
||||||
|
old_scale = tl.exp(e_max - n_e_max)
|
||||||
|
acc *= old_scale
|
||||||
|
exp_logic = tl.exp(tlogic - n_e_max)
|
||||||
|
acc += exp_logic * tv
|
||||||
|
|
||||||
|
e_sum = e_sum * old_scale + exp_logic
|
||||||
|
e_max = n_e_max
|
||||||
|
|
||||||
|
tl.store(
|
||||||
|
o + cur_batch * stride_obs + cur_head * stride_oh + offs_d,
|
||||||
|
acc / e_sum,
|
||||||
|
mask=mask_d,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_softmax_reducev_fwd(
|
||||||
|
logits,
|
||||||
|
q,
|
||||||
|
o,
|
||||||
|
v_buffer,
|
||||||
|
b_seq_len,
|
||||||
|
num_kv_splits,
|
||||||
|
):
|
||||||
|
batch, head_num = q.shape[0], q.shape[1]
|
||||||
|
Lv = v_buffer.shape[-1]
|
||||||
|
BLOCK_DV = triton.next_power_of_2(Lv)
|
||||||
|
|
||||||
|
NUM_KV_SPLITS = num_kv_splits
|
||||||
|
|
||||||
|
extra_kargs = {}
|
||||||
|
if is_hip_:
|
||||||
|
# https://rocm.docs.amd.com/en/docs-6.2.0/how-to/llm-fine-tuning-optimization/optimizing-triton-kernel.html
|
||||||
|
# https://github.com/triton-lang/triton/blob/main/third_party/amd/backend/compiler.py
|
||||||
|
extra_kargs = {
|
||||||
|
"waves_per_eu": 4,
|
||||||
|
"matrix_instr_nonkdim": 16,
|
||||||
|
"kpack": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
grid = (batch, head_num)
|
||||||
|
_fwd_kernel_stage2[grid](
|
||||||
|
logits,
|
||||||
|
o,
|
||||||
|
b_seq_len,
|
||||||
|
logits.stride(0),
|
||||||
|
logits.stride(1),
|
||||||
|
logits.stride(2),
|
||||||
|
o.stride(0),
|
||||||
|
o.stride(1),
|
||||||
|
NUM_KV_SPLITS=NUM_KV_SPLITS,
|
||||||
|
BLOCK_DV=BLOCK_DV,
|
||||||
|
Lv=Lv,
|
||||||
|
num_warps=4,
|
||||||
|
num_stages=2,
|
||||||
|
**extra_kargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_attention_fwd_normal(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
o,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
attn_logits,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap=0.0,
|
||||||
|
):
|
||||||
|
_decode_att_m_fwd(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
attn_logits,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
)
|
||||||
|
_decode_softmax_reducev_fwd(attn_logits, q, o, v_buffer, b_seq_len,
|
||||||
|
num_kv_splits)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_attention_fwd_grouped(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
o,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
attn_logits,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap=0.0,
|
||||||
|
):
|
||||||
|
_decode_grouped_att_m_fwd(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
attn_logits,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
)
|
||||||
|
_decode_softmax_reducev_fwd(attn_logits, q, o, v_buffer, b_seq_len,
|
||||||
|
num_kv_splits)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_attention_fwd(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
o,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
attn_logits,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size=1,
|
||||||
|
logit_cap=0.0,
|
||||||
|
):
|
||||||
|
assert num_kv_splits == attn_logits.shape[2]
|
||||||
|
kv_group_num = q.shape[1] // v_buffer.shape[-2]
|
||||||
|
|
||||||
|
if kv_group_num == 1:
|
||||||
|
# MHA
|
||||||
|
decode_attention_fwd_normal(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
o,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
attn_logits,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# GQA/MQA/MLA
|
||||||
|
decode_attention_fwd_grouped(
|
||||||
|
q,
|
||||||
|
k_buffer,
|
||||||
|
v_buffer,
|
||||||
|
o,
|
||||||
|
req_to_token,
|
||||||
|
b_seq_len,
|
||||||
|
attn_logits,
|
||||||
|
num_kv_splits,
|
||||||
|
sm_scale,
|
||||||
|
page_size,
|
||||||
|
logit_cap,
|
||||||
|
)
|
||||||
821
vllm/attention/ops/triton_flash_attention.py
Normal file
821
vllm/attention/ops/triton_flash_attention.py
Normal file
@@ -0,0 +1,821 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""
|
||||||
|
Fused Attention
|
||||||
|
===============
|
||||||
|
|
||||||
|
This is a Triton implementation of the Flash Attention v2 algorithm from Tri Dao
|
||||||
|
(https://tridao.me/publications/flash2/flash2.pdf)
|
||||||
|
Credits: OpenAI kernel team, AMD ML Frameworks Triton team
|
||||||
|
|
||||||
|
Features supported:
|
||||||
|
|
||||||
|
1) Fwd with causal masking
|
||||||
|
2) Any sequence lengths without padding (currently fwd kernel only)
|
||||||
|
3) Support for different sequence lengths for q and k
|
||||||
|
4) Nested tensor API currently does not support dropout or bias.
|
||||||
|
|
||||||
|
Not currently supported:
|
||||||
|
|
||||||
|
1) Non power of two head dims
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
torch_dtype: tl.constexpr = torch.float16
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def cdiv_fn(x, y):
|
||||||
|
return (x + y - 1) // y
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def max_fn(x, y):
|
||||||
|
return tl.math.max(x, y)
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def dropout_offsets(philox_seed, philox_offset, dropout_p, m, n, stride):
|
||||||
|
ms = tl.arange(0, m)
|
||||||
|
ns = tl.arange(0, n)
|
||||||
|
return philox_offset + ms[:, None] * stride + ns[None, :]
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def dropout_rng(philox_seed, philox_offset, dropout_p, m, n, stride):
|
||||||
|
rng_offsets = dropout_offsets(philox_seed, philox_offset, dropout_p, m, n,
|
||||||
|
stride).to(tl.uint32)
|
||||||
|
# TODO: use tl.randint for better performance
|
||||||
|
return tl.rand(philox_seed, rng_offsets)
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def dropout_mask(philox_seed, philox_offset, dropout_p, m, n, stride):
|
||||||
|
rng_output = dropout_rng(philox_seed, philox_offset, dropout_p, m, n,
|
||||||
|
stride)
|
||||||
|
rng_keep = rng_output > dropout_p
|
||||||
|
return rng_keep
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def load_fn(block_ptr, first, second, pad):
|
||||||
|
if first and second:
|
||||||
|
tensor = tl.load(block_ptr, boundary_check=(0, 1), padding_option=pad)
|
||||||
|
elif first:
|
||||||
|
tensor = tl.load(block_ptr, boundary_check=(0, ), padding_option=pad)
|
||||||
|
elif second:
|
||||||
|
tensor = tl.load(block_ptr, boundary_check=(1, ), padding_option=pad)
|
||||||
|
else:
|
||||||
|
tensor = tl.load(block_ptr)
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def _attn_fwd_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
K_block_ptr,
|
||||||
|
V_block_ptr,
|
||||||
|
start_m,
|
||||||
|
actual_seqlen_k,
|
||||||
|
dropout_p,
|
||||||
|
philox_seed,
|
||||||
|
batch_philox_offset,
|
||||||
|
encoded_softmax_block_ptr,
|
||||||
|
block_min,
|
||||||
|
block_max,
|
||||||
|
offs_n_causal,
|
||||||
|
masked_blocks,
|
||||||
|
n_extra_tokens,
|
||||||
|
bias_ptr,
|
||||||
|
IS_CAUSAL: tl.constexpr,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
OFFS_M: tl.constexpr,
|
||||||
|
OFFS_N: tl.constexpr,
|
||||||
|
PRE_LOAD_V: tl.constexpr,
|
||||||
|
MASK_STEPS: tl.constexpr,
|
||||||
|
ENABLE_DROPOUT: tl.constexpr,
|
||||||
|
RETURN_ENCODED_SOFTMAX: tl.constexpr,
|
||||||
|
PADDED_HEAD: tl.constexpr,
|
||||||
|
):
|
||||||
|
# loop over k, v, and update accumulator
|
||||||
|
for start_n in range(block_min, block_max, BLOCK_N):
|
||||||
|
# For padded blocks, we will overrun the tensor size if
|
||||||
|
# we load all BLOCK_N. For others, the blocks are all within range.
|
||||||
|
k = load_fn(
|
||||||
|
K_block_ptr,
|
||||||
|
PADDED_HEAD,
|
||||||
|
MASK_STEPS and (n_extra_tokens != 0),
|
||||||
|
"zero",
|
||||||
|
)
|
||||||
|
if PRE_LOAD_V:
|
||||||
|
v = load_fn(
|
||||||
|
V_block_ptr,
|
||||||
|
MASK_STEPS and (n_extra_tokens != 0),
|
||||||
|
PADDED_HEAD,
|
||||||
|
"zero",
|
||||||
|
)
|
||||||
|
qk = tl.zeros([BLOCK_M, BLOCK_N], dtype=tl.float32)
|
||||||
|
# We start from end of seqlen_k so only the first iteration would need
|
||||||
|
# to be checked for padding if it is not a multiple of block_n
|
||||||
|
# TODO: This can be optimized to only be true for the padded block.
|
||||||
|
if MASK_STEPS: # noqa: SIM102
|
||||||
|
# If this is the last block / iteration, we want to
|
||||||
|
# mask if the sequence length is not a multiple of block size
|
||||||
|
# a solution is to always do BLOCK_M // BLOCK_N + 1 steps
|
||||||
|
# if not is_modulo_mn. last step might get wasted but that is okay.
|
||||||
|
# check if this masking works for that case.
|
||||||
|
if (start_n + BLOCK_N == block_max) and (n_extra_tokens != 0):
|
||||||
|
boundary_m = tl.full([BLOCK_M],
|
||||||
|
actual_seqlen_k,
|
||||||
|
dtype=tl.int32)
|
||||||
|
size_n = start_n + OFFS_N[None, :]
|
||||||
|
mask = size_n < boundary_m[:, None]
|
||||||
|
qk = tl.where(mask, qk, float("-inf"))
|
||||||
|
if IS_CAUSAL:
|
||||||
|
causal_boundary = start_n + offs_n_causal
|
||||||
|
causal_mask = OFFS_M[:, None] >= causal_boundary[None, :]
|
||||||
|
qk = tl.where(causal_mask, qk, float("-inf"))
|
||||||
|
# -- compute qk ----
|
||||||
|
qk += tl.dot(q, k)
|
||||||
|
if bias_ptr is not None:
|
||||||
|
bias = load_fn(bias_ptr, False, MASK_STEPS
|
||||||
|
and (n_extra_tokens != 0), "zero")
|
||||||
|
# While bias is added after multiplying qk with sm_scale, our
|
||||||
|
# optimization to use 2^x instead of e^x results in an additional
|
||||||
|
# scale factor of log2(e) which we must also multiply the bias with.
|
||||||
|
qk += bias * 1.44269504089
|
||||||
|
m_ij = tl.maximum(m_i, tl.max(qk, 1))
|
||||||
|
qk = qk - m_ij[:, None]
|
||||||
|
p = tl.math.exp2(qk)
|
||||||
|
|
||||||
|
# CAVEAT: Must update l_ij before applying dropout
|
||||||
|
l_ij = tl.sum(p, 1)
|
||||||
|
if ENABLE_DROPOUT:
|
||||||
|
philox_offset = (batch_philox_offset +
|
||||||
|
start_m * BLOCK_M * actual_seqlen_k + start_n -
|
||||||
|
BLOCK_N)
|
||||||
|
keep = dropout_mask(
|
||||||
|
philox_seed,
|
||||||
|
philox_offset,
|
||||||
|
dropout_p,
|
||||||
|
BLOCK_M,
|
||||||
|
BLOCK_N,
|
||||||
|
actual_seqlen_k,
|
||||||
|
)
|
||||||
|
if RETURN_ENCODED_SOFTMAX:
|
||||||
|
tl.store(
|
||||||
|
encoded_softmax_block_ptr,
|
||||||
|
tl.where(keep, p,
|
||||||
|
-p).to(encoded_softmax_block_ptr.type.element_ty),
|
||||||
|
)
|
||||||
|
p = tl.where(keep, p, 0.0)
|
||||||
|
elif RETURN_ENCODED_SOFTMAX:
|
||||||
|
tl.store(
|
||||||
|
encoded_softmax_block_ptr,
|
||||||
|
p.to(encoded_softmax_block_ptr.type.element_ty),
|
||||||
|
)
|
||||||
|
# -- update output accumulator --
|
||||||
|
alpha = tl.math.exp2(m_i - m_ij)
|
||||||
|
acc = acc * alpha[:, None]
|
||||||
|
if not PRE_LOAD_V:
|
||||||
|
v = load_fn(
|
||||||
|
V_block_ptr,
|
||||||
|
MASK_STEPS and (n_extra_tokens != 0),
|
||||||
|
PADDED_HEAD,
|
||||||
|
"zero",
|
||||||
|
)
|
||||||
|
# -- update m_i and l_i
|
||||||
|
l_i = l_i * alpha + l_ij
|
||||||
|
# update m_i and l_i
|
||||||
|
m_i = m_ij
|
||||||
|
acc += tl.dot(p.to(V_block_ptr.type.element_ty), v)
|
||||||
|
V_block_ptr = tl.advance(V_block_ptr, (BLOCK_N, 0))
|
||||||
|
K_block_ptr = tl.advance(K_block_ptr, (0, BLOCK_N))
|
||||||
|
if bias_ptr is not None:
|
||||||
|
bias_ptr = tl.advance(bias_ptr, (0, BLOCK_N))
|
||||||
|
if RETURN_ENCODED_SOFTMAX:
|
||||||
|
encoded_softmax_block_ptr = tl.advance(encoded_softmax_block_ptr,
|
||||||
|
(0, BLOCK_N))
|
||||||
|
return acc, l_i, m_i
|
||||||
|
|
||||||
|
|
||||||
|
@triton.autotune(
|
||||||
|
configs=[
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 256,
|
||||||
|
"BLOCK_N": 64,
|
||||||
|
"waves_per_eu": 2,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=8,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 128,
|
||||||
|
"BLOCK_N": 128,
|
||||||
|
"waves_per_eu": 2,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=4,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 256,
|
||||||
|
"BLOCK_N": 128,
|
||||||
|
"waves_per_eu": 2,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=8,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 128,
|
||||||
|
"BLOCK_N": 64,
|
||||||
|
"waves_per_eu": 1,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=4,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 128,
|
||||||
|
"BLOCK_N": 64,
|
||||||
|
"waves_per_eu": 3,
|
||||||
|
"PRE_LOAD_V": True,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=4,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 128,
|
||||||
|
"BLOCK_N": 64,
|
||||||
|
"waves_per_eu": 3,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=4,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 64,
|
||||||
|
"BLOCK_N": 64,
|
||||||
|
"waves_per_eu": 4,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=8,
|
||||||
|
),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 32,
|
||||||
|
"BLOCK_N": 32,
|
||||||
|
"waves_per_eu": 4,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=8,
|
||||||
|
),
|
||||||
|
# TODO: This config fails with head_size not pow2 with data mismatches.
|
||||||
|
# triton.Config({'BLOCK_M': 32, 'BLOCK_N': 16, 'waves_per_eu': 1,
|
||||||
|
# 'PRE_LOAD_V': False}, num_stages=1, num_warps=4),
|
||||||
|
triton.Config(
|
||||||
|
{
|
||||||
|
"BLOCK_M": 16,
|
||||||
|
"BLOCK_N": 16,
|
||||||
|
"waves_per_eu": 1,
|
||||||
|
"PRE_LOAD_V": False,
|
||||||
|
},
|
||||||
|
num_stages=1,
|
||||||
|
num_warps=4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
key=['IS_CAUSAL', 'dropout_p', 'BLOCK_DMODEL'],
|
||||||
|
)
|
||||||
|
@triton.jit
|
||||||
|
def attn_fwd(
|
||||||
|
Q,
|
||||||
|
K,
|
||||||
|
V,
|
||||||
|
bias,
|
||||||
|
sm_scale,
|
||||||
|
L,
|
||||||
|
Out,
|
||||||
|
stride_qz,
|
||||||
|
stride_qh,
|
||||||
|
stride_qm,
|
||||||
|
stride_qk,
|
||||||
|
stride_kz,
|
||||||
|
stride_kh,
|
||||||
|
stride_kn,
|
||||||
|
stride_kk,
|
||||||
|
stride_vz,
|
||||||
|
stride_vh,
|
||||||
|
stride_vk,
|
||||||
|
stride_vn,
|
||||||
|
stride_oz,
|
||||||
|
stride_oh,
|
||||||
|
stride_om,
|
||||||
|
stride_on,
|
||||||
|
stride_bz,
|
||||||
|
stride_bh,
|
||||||
|
stride_bm,
|
||||||
|
stride_bn,
|
||||||
|
cu_seqlens_q,
|
||||||
|
cu_seqlens_k,
|
||||||
|
dropout_p,
|
||||||
|
philox_seed,
|
||||||
|
philox_offset_base,
|
||||||
|
encoded_softmax,
|
||||||
|
HQ: tl.constexpr,
|
||||||
|
HK: tl.constexpr,
|
||||||
|
ACTUAL_BLOCK_DMODEL: tl.constexpr,
|
||||||
|
MAX_SEQLENS_Q: tl.constexpr,
|
||||||
|
MAX_SEQLENS_K: tl.constexpr,
|
||||||
|
VARLEN: tl.constexpr,
|
||||||
|
IS_CAUSAL: tl.constexpr,
|
||||||
|
BLOCK_M: tl.constexpr,
|
||||||
|
BLOCK_DMODEL: tl.constexpr,
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
PRE_LOAD_V: tl.constexpr,
|
||||||
|
BIAS_TYPE: tl.constexpr,
|
||||||
|
ENABLE_DROPOUT: tl.constexpr,
|
||||||
|
RETURN_ENCODED_SOFTMAX: tl.constexpr,
|
||||||
|
):
|
||||||
|
start_m = tl.program_id(0)
|
||||||
|
off_h_q = tl.program_id(1)
|
||||||
|
off_z = tl.program_id(2)
|
||||||
|
offs_m = start_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||||
|
offs_n = tl.arange(0, BLOCK_N)
|
||||||
|
if VARLEN:
|
||||||
|
cu_seqlens_q_start = tl.load(cu_seqlens_q + off_z)
|
||||||
|
cu_seqlens_q_end = tl.load(cu_seqlens_q + off_z + 1)
|
||||||
|
seqlen_q = cu_seqlens_q_end - cu_seqlens_q_start
|
||||||
|
# We have a one-size-fits-all grid in id(0). Some seqlens might be too
|
||||||
|
# small for all start_m so for those we return early.
|
||||||
|
if start_m * BLOCK_M > seqlen_q:
|
||||||
|
return
|
||||||
|
cu_seqlens_k_start = tl.load(cu_seqlens_k + off_z)
|
||||||
|
cu_seqlens_k_end = tl.load(cu_seqlens_k + off_z + 1)
|
||||||
|
seqlen_k = cu_seqlens_k_end - cu_seqlens_k_start
|
||||||
|
else:
|
||||||
|
cu_seqlens_q_start = 0
|
||||||
|
cu_seqlens_k_start = 0
|
||||||
|
seqlen_q = MAX_SEQLENS_Q
|
||||||
|
seqlen_k = MAX_SEQLENS_K
|
||||||
|
|
||||||
|
# Now we compute whether we need to exit early due to causal masking.
|
||||||
|
# This is because for seqlen_q > seqlen_k, M rows of the attn scores
|
||||||
|
# are completely masked, resulting in 0s written to the output, and
|
||||||
|
# inf written to LSE. We don't need to do any GEMMs in this case.
|
||||||
|
# This block of code determines what N is, and if this WG is operating
|
||||||
|
# on those M rows.
|
||||||
|
n_blocks = cdiv_fn(seqlen_k, BLOCK_N)
|
||||||
|
if IS_CAUSAL:
|
||||||
|
# If seqlen_q == seqlen_k, the attn scores are a square matrix.
|
||||||
|
# If seqlen_q != seqlen_k, attn scores are rectangular which means
|
||||||
|
# the causal mask boundary is bottom right aligned, and ends at either
|
||||||
|
# the top edge (seqlen_q < seqlen_k) or left edge.
|
||||||
|
# This captures the decrease in n_blocks if we have a rectangular attn
|
||||||
|
# matrix
|
||||||
|
n_blocks_seqlen = cdiv_fn(
|
||||||
|
(start_m + 1) * BLOCK_M + seqlen_k - seqlen_q, BLOCK_N)
|
||||||
|
# This is what adjusts the block_max for the current WG, only
|
||||||
|
# if IS_CAUSAL. Otherwise we want to always iterate through all n_blocks
|
||||||
|
n_blocks = min(n_blocks, n_blocks_seqlen)
|
||||||
|
# If we have no blocks after adjusting for seqlen deltas, this WG is
|
||||||
|
# part of the blocks that are all 0. We exit early.
|
||||||
|
if n_blocks <= 0:
|
||||||
|
o_offset = (off_z * stride_oz + cu_seqlens_q_start * stride_om +
|
||||||
|
off_h_q * stride_oh)
|
||||||
|
O_block_ptr = tl.make_block_ptr(
|
||||||
|
base=Out + o_offset,
|
||||||
|
shape=(seqlen_q, BLOCK_DMODEL),
|
||||||
|
strides=(stride_om, stride_on),
|
||||||
|
offsets=(start_m * BLOCK_M, 0),
|
||||||
|
block_shape=(BLOCK_M, BLOCK_DMODEL),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL], dtype=Out.type.element_ty)
|
||||||
|
# We still need to write 0s to the result
|
||||||
|
# tl.store(O_block_ptr,
|
||||||
|
# acc.to(Out.type.element_ty), boundary_check=(0,1))
|
||||||
|
# l_ptrs = L + off_z * HQ * MAX_SEQLENS_Q + off_h_q * MAX_SEQLENS_Q
|
||||||
|
# + offs_m
|
||||||
|
# We store inf to LSE, not -inf because in the bwd pass,
|
||||||
|
# we subtract this
|
||||||
|
# from qk which makes it -inf, such that exp(qk - inf) = 0
|
||||||
|
# for these masked blocks.
|
||||||
|
# l = tl.full([BLOCK_M], value=float("inf"), dtype=tl.float32)
|
||||||
|
# tl.store(l_ptrs, l)
|
||||||
|
# TODO: Should dropout and return encoded softmax be handled here?
|
||||||
|
return
|
||||||
|
|
||||||
|
# If MQA / GQA, set the K and V head offsets appropriately.
|
||||||
|
GROUP_SIZE: tl.constexpr = HQ // HK
|
||||||
|
off_h_k = off_h_q // GROUP_SIZE if GROUP_SIZE != 1 else off_h_q
|
||||||
|
|
||||||
|
n_extra_tokens = 0
|
||||||
|
if seqlen_k < BLOCK_N:
|
||||||
|
n_extra_tokens = BLOCK_N - seqlen_k
|
||||||
|
elif seqlen_k % BLOCK_N:
|
||||||
|
n_extra_tokens = seqlen_k % BLOCK_N
|
||||||
|
padded_head = ACTUAL_BLOCK_DMODEL != BLOCK_DMODEL
|
||||||
|
|
||||||
|
# Compute pointers for all the tensors used in this kernel.
|
||||||
|
q_offset = (off_z * stride_qz + off_h_q * stride_qh +
|
||||||
|
cu_seqlens_q_start * stride_qm)
|
||||||
|
Q_block_ptr = tl.make_block_ptr(
|
||||||
|
base=Q + q_offset,
|
||||||
|
shape=(seqlen_q, ACTUAL_BLOCK_DMODEL),
|
||||||
|
strides=(stride_qm, stride_qk),
|
||||||
|
offsets=(start_m * BLOCK_M, 0),
|
||||||
|
block_shape=(BLOCK_M, BLOCK_DMODEL),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
k_offset = (off_z * stride_kz + off_h_k * stride_kh +
|
||||||
|
cu_seqlens_k_start * stride_kn)
|
||||||
|
K_block_ptr = tl.make_block_ptr(
|
||||||
|
base=K + k_offset,
|
||||||
|
shape=(ACTUAL_BLOCK_DMODEL, seqlen_k),
|
||||||
|
strides=(stride_kk, stride_kn),
|
||||||
|
offsets=(0, 0),
|
||||||
|
block_shape=(BLOCK_DMODEL, BLOCK_N),
|
||||||
|
order=(0, 1),
|
||||||
|
)
|
||||||
|
v_offset = (off_z * stride_vz + off_h_k * stride_vh +
|
||||||
|
cu_seqlens_k_start * stride_vk)
|
||||||
|
V_block_ptr = tl.make_block_ptr(
|
||||||
|
base=V + v_offset,
|
||||||
|
shape=(seqlen_k, ACTUAL_BLOCK_DMODEL),
|
||||||
|
strides=(stride_vk, stride_vn),
|
||||||
|
offsets=(0, 0),
|
||||||
|
block_shape=(BLOCK_N, BLOCK_DMODEL),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
if BIAS_TYPE != 0:
|
||||||
|
bias_ptr = tl.make_block_ptr(
|
||||||
|
base=bias + off_h_q * stride_bh,
|
||||||
|
shape=(seqlen_q, seqlen_k),
|
||||||
|
strides=(stride_bm, stride_bn),
|
||||||
|
offsets=(start_m * BLOCK_M, 0),
|
||||||
|
block_shape=(BLOCK_M, BLOCK_N),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bias_ptr = None
|
||||||
|
if ENABLE_DROPOUT:
|
||||||
|
batch_philox_offset = philox_offset_base \
|
||||||
|
+ (off_z * HQ + off_h_q) \
|
||||||
|
* seqlen_q * seqlen_k
|
||||||
|
else:
|
||||||
|
batch_philox_offset = 0
|
||||||
|
# We can ask to return the dropout mask without actually doing any dropout.
|
||||||
|
# In this case, we return an invalid pointer so indicate the mask is not i
|
||||||
|
# valid.
|
||||||
|
# TODO: Fix encoded softmax. It currently uses just h_q in the base offset.
|
||||||
|
if RETURN_ENCODED_SOFTMAX:
|
||||||
|
encoded_softmax_block_ptr = tl.make_block_ptr(
|
||||||
|
base=encoded_softmax + off_h_q * seqlen_q * seqlen_k,
|
||||||
|
shape=(seqlen_q, seqlen_k),
|
||||||
|
strides=(seqlen_k, 1),
|
||||||
|
offsets=(start_m * BLOCK_M, 0),
|
||||||
|
block_shape=(BLOCK_M, BLOCK_N),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
encoded_softmax_block_ptr = 0
|
||||||
|
# initialize pointer to m and l
|
||||||
|
m_i = tl.full([BLOCK_M], float("-inf"), dtype=tl.float32)
|
||||||
|
l_i = tl.full([BLOCK_M], 1.0, dtype=tl.float32)
|
||||||
|
acc = tl.zeros([BLOCK_M, BLOCK_DMODEL], dtype=tl.float32)
|
||||||
|
# scale sm_scale by log_2(e) and use 2^x in the loop as we do not
|
||||||
|
# have native e^x support in HW.
|
||||||
|
qk_scale = sm_scale * 1.44269504089
|
||||||
|
# Q is loaded once at the beginning and shared by all N blocks.
|
||||||
|
q = load_fn(Q_block_ptr, True, padded_head, "zero")
|
||||||
|
q = (q * qk_scale).to(Q_block_ptr.type.element_ty)
|
||||||
|
|
||||||
|
# Here we compute how many full and masked blocks we have.
|
||||||
|
padded_block_k = n_extra_tokens != 0
|
||||||
|
is_modulo_mn = not padded_block_k and (seqlen_q % BLOCK_M == 0)
|
||||||
|
if IS_CAUSAL:
|
||||||
|
# There are always at least BLOCK_M // BLOCK_N masked blocks.
|
||||||
|
# Additionally there might be one more due to dissimilar seqlens.
|
||||||
|
masked_blocks = BLOCK_M // BLOCK_N + (not is_modulo_mn)
|
||||||
|
else:
|
||||||
|
# Padding on Q does not need to be masked in the FA loop.
|
||||||
|
masked_blocks = padded_block_k
|
||||||
|
# if IS_CAUSAL, not is_modulo_mn does not always result in an additional
|
||||||
|
# block. In this case we might exceed n_blocks so pick the min.
|
||||||
|
masked_blocks = min(masked_blocks, n_blocks)
|
||||||
|
n_full_blocks = n_blocks - masked_blocks
|
||||||
|
block_min = 0
|
||||||
|
block_max = n_blocks * BLOCK_N
|
||||||
|
# Compute for full blocks. Here we set causal to false regardless of its
|
||||||
|
# value because there is no masking. Similarly we do not need padding.
|
||||||
|
if n_full_blocks > 0:
|
||||||
|
block_max = (n_blocks - masked_blocks) * BLOCK_N
|
||||||
|
acc, l_i, m_i = _attn_fwd_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
K_block_ptr,
|
||||||
|
V_block_ptr,
|
||||||
|
start_m,
|
||||||
|
seqlen_k,
|
||||||
|
dropout_p,
|
||||||
|
philox_seed,
|
||||||
|
batch_philox_offset,
|
||||||
|
encoded_softmax_block_ptr,
|
||||||
|
# _, _, offs_n_causal, masked_blocks, n_extra_tokens, _
|
||||||
|
block_min,
|
||||||
|
block_max,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bias_ptr,
|
||||||
|
# IS_CAUSAL, ....
|
||||||
|
False,
|
||||||
|
BLOCK_M,
|
||||||
|
BLOCK_DMODEL,
|
||||||
|
BLOCK_N,
|
||||||
|
offs_m,
|
||||||
|
offs_n,
|
||||||
|
# _, MASK_STEPS, ...
|
||||||
|
PRE_LOAD_V,
|
||||||
|
False,
|
||||||
|
ENABLE_DROPOUT,
|
||||||
|
RETURN_ENCODED_SOFTMAX,
|
||||||
|
padded_head,
|
||||||
|
)
|
||||||
|
block_min = block_max
|
||||||
|
block_max = n_blocks * BLOCK_N
|
||||||
|
|
||||||
|
tl.debug_barrier()
|
||||||
|
# Remaining blocks, if any, are full / not masked.
|
||||||
|
if masked_blocks > 0:
|
||||||
|
offs_n_causal = offs_n + (seqlen_q - seqlen_k) if IS_CAUSAL else 0
|
||||||
|
K_block_ptr = tl.advance(K_block_ptr, (0, n_full_blocks * BLOCK_N))
|
||||||
|
V_block_ptr = tl.advance(V_block_ptr, (n_full_blocks * BLOCK_N, 0))
|
||||||
|
if bias_ptr is not None:
|
||||||
|
bias_ptr = tl.advance(bias_ptr, (0, n_full_blocks * BLOCK_N))
|
||||||
|
if RETURN_ENCODED_SOFTMAX:
|
||||||
|
encoded_softmax_block_ptr = tl.advance(encoded_softmax_block_ptr,
|
||||||
|
(0, n_full_blocks))
|
||||||
|
acc, l_i, m_i = _attn_fwd_inner(
|
||||||
|
acc,
|
||||||
|
l_i,
|
||||||
|
m_i,
|
||||||
|
q,
|
||||||
|
K_block_ptr,
|
||||||
|
V_block_ptr,
|
||||||
|
start_m,
|
||||||
|
seqlen_k,
|
||||||
|
dropout_p,
|
||||||
|
philox_seed,
|
||||||
|
batch_philox_offset,
|
||||||
|
encoded_softmax_block_ptr,
|
||||||
|
block_min,
|
||||||
|
block_max,
|
||||||
|
offs_n_causal,
|
||||||
|
masked_blocks,
|
||||||
|
n_extra_tokens,
|
||||||
|
bias_ptr,
|
||||||
|
IS_CAUSAL,
|
||||||
|
BLOCK_M,
|
||||||
|
BLOCK_DMODEL,
|
||||||
|
BLOCK_N,
|
||||||
|
offs_m,
|
||||||
|
offs_n,
|
||||||
|
# _, MASK_STEPS, ...
|
||||||
|
PRE_LOAD_V,
|
||||||
|
True,
|
||||||
|
ENABLE_DROPOUT,
|
||||||
|
RETURN_ENCODED_SOFTMAX,
|
||||||
|
padded_head,
|
||||||
|
)
|
||||||
|
# epilogue
|
||||||
|
acc = acc / l_i[:, None]
|
||||||
|
if ENABLE_DROPOUT:
|
||||||
|
acc = acc / (1 - dropout_p)
|
||||||
|
# If seqlen_q > seqlen_k but the delta is not a multiple of BLOCK_M,
|
||||||
|
# then we have one block with a row of all NaNs which come from computing
|
||||||
|
# softmax over a row of all -infs (-inf - inf = NaN). We check for that here
|
||||||
|
# and store 0s where there are NaNs as these rows should've been zeroed out.
|
||||||
|
end_m_idx = (start_m + 1) * BLOCK_M
|
||||||
|
start_m_idx = start_m * BLOCK_M
|
||||||
|
causal_start_idx = seqlen_q - seqlen_k
|
||||||
|
acc = acc.to(Out.type.element_ty)
|
||||||
|
if IS_CAUSAL: # noqa: SIM102
|
||||||
|
if causal_start_idx > start_m_idx and causal_start_idx < end_m_idx:
|
||||||
|
out_mask_boundary = tl.full((BLOCK_DMODEL, ),
|
||||||
|
causal_start_idx,
|
||||||
|
dtype=tl.int32)
|
||||||
|
mask_m_offsets = start_m_idx + tl.arange(0, BLOCK_M)
|
||||||
|
out_ptrs_mask = (mask_m_offsets[:, None]
|
||||||
|
>= out_mask_boundary[None, :])
|
||||||
|
z = 0.0
|
||||||
|
acc = tl.where(out_ptrs_mask, acc, z.to(acc.type.element_ty))
|
||||||
|
# write back LSE
|
||||||
|
# l_ptrs = L + off_z * HQ * MAX_SEQLENS_Q + off_h_q * MAX_SEQLENS_Q + offs_m
|
||||||
|
# If seqlen_q not multiple of BLOCK_M, we need to mask out the last
|
||||||
|
# few rows. This is only true for the last M block. For others,
|
||||||
|
# overflow_size will be -ve
|
||||||
|
# overflow_size = end_m_idx - seqlen_q
|
||||||
|
# if overflow_size > 0:
|
||||||
|
# boundary = tl.full((BLOCK_M,), BLOCK_M - overflow_size, dtype=tl.int32)
|
||||||
|
# # This is a > check because mask being 0 blocks the store.
|
||||||
|
# l_ptrs_mask = boundary > tl.arange(0, BLOCK_M)
|
||||||
|
# tl.store(l_ptrs, m_i + tl.math.log2(l_i), mask=l_ptrs_mask)
|
||||||
|
# else:
|
||||||
|
# tl.store(l_ptrs, m_i + tl.math.log2(l_i))
|
||||||
|
|
||||||
|
# write back O
|
||||||
|
o_offset = (off_z * stride_oz + cu_seqlens_q_start * stride_om +
|
||||||
|
off_h_q * stride_oh)
|
||||||
|
O_block_ptr = tl.make_block_ptr(
|
||||||
|
base=Out + o_offset,
|
||||||
|
shape=(seqlen_q, ACTUAL_BLOCK_DMODEL),
|
||||||
|
strides=(stride_om, stride_on),
|
||||||
|
offsets=(start_m * BLOCK_M, 0),
|
||||||
|
block_shape=(BLOCK_M, BLOCK_DMODEL),
|
||||||
|
order=(1, 0),
|
||||||
|
)
|
||||||
|
# Need boundary check on this to make sure the padding from the
|
||||||
|
# Q and KV tensors in both dims are not part of what we store back.
|
||||||
|
# TODO: Do the boundary check optionally.
|
||||||
|
tl.store(O_block_ptr, acc, boundary_check=(0, 1))
|
||||||
|
|
||||||
|
|
||||||
|
def check_args(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
o,
|
||||||
|
varlen=True,
|
||||||
|
max_seqlens=None,
|
||||||
|
cu_seqlens_q=None,
|
||||||
|
cu_seqlens_k=None,
|
||||||
|
):
|
||||||
|
assert q.dim() == k.dim() and q.dim() == v.dim()
|
||||||
|
if varlen:
|
||||||
|
assert q.dim() == 3
|
||||||
|
total_q, nheads_q, head_size = q.shape
|
||||||
|
total_k, nheads_k, _ = k.shape
|
||||||
|
assert cu_seqlens_q is not None
|
||||||
|
assert cu_seqlens_k is not None
|
||||||
|
assert len(cu_seqlens_q) == len(cu_seqlens_k)
|
||||||
|
else:
|
||||||
|
assert q.dim() == 4
|
||||||
|
batch, nheads_q, seqlen_q, head_size = q.shape
|
||||||
|
_, nheads_k, seqlen_k, _ = k.shape
|
||||||
|
assert max_seqlens > 0
|
||||||
|
assert k.shape == v.shape
|
||||||
|
assert q.shape[-1] == k.shape[-1] and q.shape[-1] == v.shape[-1]
|
||||||
|
# TODO: Change assert if we support qkl f8 and v f16
|
||||||
|
assert q.dtype == k.dtype and q.dtype == v.dtype
|
||||||
|
assert head_size <= 256
|
||||||
|
assert o.shape == q.shape
|
||||||
|
assert (nheads_q % nheads_k) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class _attention(torch.autograd.Function):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def forward(
|
||||||
|
ctx,
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
o,
|
||||||
|
cu_seqlens_q,
|
||||||
|
cu_seqlens_k,
|
||||||
|
max_seqlens_q,
|
||||||
|
max_seqlens_k,
|
||||||
|
causal=False,
|
||||||
|
sm_scale=1.0,
|
||||||
|
bias=None,
|
||||||
|
):
|
||||||
|
if o is None:
|
||||||
|
o = torch.empty_like(q, dtype=v.dtype)
|
||||||
|
|
||||||
|
check_args(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
o,
|
||||||
|
varlen=True,
|
||||||
|
cu_seqlens_q=cu_seqlens_q,
|
||||||
|
cu_seqlens_k=cu_seqlens_k,
|
||||||
|
)
|
||||||
|
if True: # varlen
|
||||||
|
total_q, nheads_q, head_size = q.shape
|
||||||
|
total_k, nheads_k, _ = k.shape
|
||||||
|
batch = len(cu_seqlens_q) - 1
|
||||||
|
q_strides = (0, q.stride(1), q.stride(0), q.stride(2))
|
||||||
|
k_strides = (0, k.stride(1), k.stride(0), k.stride(2))
|
||||||
|
v_strides = (0, v.stride(1), v.stride(0), v.stride(2))
|
||||||
|
o_strides = (0, o.stride(1), o.stride(0), o.stride(2))
|
||||||
|
else:
|
||||||
|
batch, seqlen_q, nheads_q, head_size = q.shape
|
||||||
|
_, seqlen_k, nheads_k, _ = k.shape
|
||||||
|
q_strides = (q.stride(0), q.stride(2), q.stride(1), q.stride(3))
|
||||||
|
k_strides = (k.stride(0), k.stride(2), k.stride(1), k.stride(3))
|
||||||
|
v_strides = (v.stride(0), v.stride(2), v.stride(1), v.stride(3))
|
||||||
|
o_strides = (o.stride(0), o.stride(2), o.stride(1), o.stride(3))
|
||||||
|
|
||||||
|
# Get closest power of 2 over or equal to 32.
|
||||||
|
unpadded_head_dims = {32, 64, 128, 256}
|
||||||
|
if head_size not in unpadded_head_dims:
|
||||||
|
padded_d_model = None
|
||||||
|
for i in unpadded_head_dims:
|
||||||
|
if i > head_size:
|
||||||
|
padded_d_model = i
|
||||||
|
break
|
||||||
|
assert padded_d_model is not None
|
||||||
|
else:
|
||||||
|
padded_d_model = head_size
|
||||||
|
|
||||||
|
grid = lambda META: (
|
||||||
|
triton.cdiv(max_seqlens_q, META["BLOCK_M"]),
|
||||||
|
nheads_q,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
|
||||||
|
encoded_softmax = None
|
||||||
|
|
||||||
|
# Seed the RNG so we get reproducible results for testing.
|
||||||
|
philox_seed = 0x1BF52
|
||||||
|
philox_offset = 0x1D4B42
|
||||||
|
|
||||||
|
if bias is not None:
|
||||||
|
bias_strides = (
|
||||||
|
bias.stride(0),
|
||||||
|
bias.stride(1),
|
||||||
|
bias.stride(2),
|
||||||
|
bias.stride(3),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
bias_strides = (0, 0, 0, 0)
|
||||||
|
|
||||||
|
attn_fwd[grid](
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
bias,
|
||||||
|
sm_scale,
|
||||||
|
None,
|
||||||
|
o,
|
||||||
|
*q_strides,
|
||||||
|
*k_strides,
|
||||||
|
*v_strides,
|
||||||
|
*o_strides,
|
||||||
|
*bias_strides,
|
||||||
|
cu_seqlens_q,
|
||||||
|
cu_seqlens_k,
|
||||||
|
dropout_p=0.0,
|
||||||
|
philox_seed=philox_seed,
|
||||||
|
philox_offset_base=philox_offset,
|
||||||
|
encoded_softmax=encoded_softmax,
|
||||||
|
HQ=nheads_q,
|
||||||
|
HK=nheads_k,
|
||||||
|
ACTUAL_BLOCK_DMODEL=head_size,
|
||||||
|
MAX_SEQLENS_Q=max_seqlens_q,
|
||||||
|
MAX_SEQLENS_K=max_seqlens_k,
|
||||||
|
IS_CAUSAL=causal,
|
||||||
|
VARLEN=True,
|
||||||
|
BLOCK_DMODEL=padded_d_model,
|
||||||
|
BIAS_TYPE=0 if bias is None else 1,
|
||||||
|
ENABLE_DROPOUT=False,
|
||||||
|
RETURN_ENCODED_SOFTMAX=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.grid = grid
|
||||||
|
ctx.sm_scale = sm_scale
|
||||||
|
ctx.BLOCK_DMODEL = head_size
|
||||||
|
ctx.causal = causal
|
||||||
|
ctx.dropout_p = 0.0
|
||||||
|
ctx.philox_seed = philox_seed
|
||||||
|
ctx.philox_offset = philox_offset
|
||||||
|
ctx.encoded_softmax = encoded_softmax
|
||||||
|
ctx.return_encoded_softmax = False
|
||||||
|
return o, encoded_softmax
|
||||||
|
|
||||||
|
|
||||||
|
triton_attention = _attention.apply
|
||||||
93
vllm/attention/ops/triton_merge_attn_states.py
Normal file
93
vllm/attention/ops/triton_merge_attn_states.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import triton
|
||||||
|
import triton.language as tl
|
||||||
|
|
||||||
|
|
||||||
|
# Implements section 2.2 of https://www.arxiv.org/pdf/2501.01005
|
||||||
|
# can be used to combine partial attention results (in the split-KV case)
|
||||||
|
def merge_attn_states(
|
||||||
|
output: torch.Tensor,
|
||||||
|
prefix_output: torch.Tensor,
|
||||||
|
prefix_lse: torch.Tensor,
|
||||||
|
suffix_output: torch.Tensor,
|
||||||
|
suffix_lse: torch.Tensor,
|
||||||
|
output_lse: Optional[torch.Tensor] = None,
|
||||||
|
) -> None:
|
||||||
|
num_tokens = output.shape[0]
|
||||||
|
num_query_heads = output.shape[1]
|
||||||
|
head_size = output.shape[2]
|
||||||
|
padded_head_size = triton.next_power_of_2(head_size)
|
||||||
|
|
||||||
|
# TODO(woosuk): Use CUDA kernel instead of Triton to minimize CPU overhead.
|
||||||
|
merge_attn_states_kernel[(num_tokens, num_query_heads)](
|
||||||
|
output,
|
||||||
|
output_lse,
|
||||||
|
prefix_output,
|
||||||
|
prefix_lse,
|
||||||
|
suffix_output,
|
||||||
|
suffix_lse,
|
||||||
|
head_size,
|
||||||
|
padded_head_size,
|
||||||
|
output_lse is not None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@triton.jit
|
||||||
|
def merge_attn_states_kernel(
|
||||||
|
output, # [NUM_TOKENS, NUM_HEADS, HEAD_SIZE]
|
||||||
|
output_lse, # [NUM_HEADS, NUM_TOKENS]
|
||||||
|
prefix_output, # [NUM_TOKENS, NUM_HEADS, HEAD_SIZE]
|
||||||
|
prefix_lse, # [NUM_HEADS, NUM_TOKENS]
|
||||||
|
suffix_output, # [NUM_TOKENS, NUM_HEADS, HEAD_SIZE]
|
||||||
|
suffix_lse, # [NUM_HEADS, NUM_TOKENS]
|
||||||
|
HEAD_SIZE: tl.constexpr,
|
||||||
|
PADDED_HEAD_SIZE: tl.constexpr,
|
||||||
|
OUTPUT_LSE: tl.constexpr,
|
||||||
|
):
|
||||||
|
token_idx = tl.program_id(0)
|
||||||
|
num_tokens = tl.num_programs(0)
|
||||||
|
head_idx = tl.program_id(1)
|
||||||
|
num_heads = tl.num_programs(1)
|
||||||
|
|
||||||
|
p_lse = tl.load(prefix_lse + head_idx * num_tokens + token_idx)
|
||||||
|
s_lse = tl.load(suffix_lse + head_idx * num_tokens + token_idx)
|
||||||
|
|
||||||
|
# FA2 and FA3 have different behavior for when the sum-exp is 0, this namely
|
||||||
|
# arises with 0 len seqlens. FA3 returns -inf here while FA2 returns inf.
|
||||||
|
# If we see an inf assume FA2 and convert inf to -inf for consistency
|
||||||
|
# and correctness. Inf generally doesn't make sense in this context outside
|
||||||
|
# of undefined-behavior/FA2-case, so I think this a safe assumption.
|
||||||
|
p_lse = float('-inf') if p_lse == float('inf') else p_lse
|
||||||
|
s_lse = float('-inf') if s_lse == float('inf') else s_lse
|
||||||
|
|
||||||
|
max_lse = tl.maximum(p_lse, s_lse)
|
||||||
|
p_lse = p_lse - max_lse
|
||||||
|
s_lse = s_lse - max_lse
|
||||||
|
out_se = (tl.exp(p_lse) + tl.exp(s_lse))
|
||||||
|
|
||||||
|
if OUTPUT_LSE:
|
||||||
|
out_lse = tl.log(out_se) + max_lse
|
||||||
|
tl.store(output_lse + head_idx * num_tokens + token_idx, out_lse)
|
||||||
|
|
||||||
|
head_arange = tl.arange(0, PADDED_HEAD_SIZE)
|
||||||
|
head_mask = head_arange < HEAD_SIZE
|
||||||
|
p_out = tl.load(prefix_output + token_idx * num_heads * HEAD_SIZE +
|
||||||
|
head_idx * HEAD_SIZE + head_arange,
|
||||||
|
mask=head_mask)
|
||||||
|
s_out = tl.load(suffix_output + token_idx * num_heads * HEAD_SIZE +
|
||||||
|
head_idx * HEAD_SIZE + head_arange,
|
||||||
|
mask=head_mask)
|
||||||
|
|
||||||
|
# NOTE(woosuk): Be careful with the numerical stability.
|
||||||
|
# We should compute the scale first, and then multiply it with the output.
|
||||||
|
# Do not multiply the output with tl.exp(p_lse) or tl.exp(s_lse) directly.
|
||||||
|
p_scale = tl.exp(p_lse) / out_se
|
||||||
|
s_scale = tl.exp(s_lse) / out_se
|
||||||
|
out = p_out * p_scale + s_out * s_scale
|
||||||
|
tl.store(output + token_idx * num_heads * HEAD_SIZE +
|
||||||
|
head_idx * HEAD_SIZE + head_arange,
|
||||||
|
out,
|
||||||
|
mask=head_mask)
|
||||||
186
vllm/attention/selector.py
Normal file
186
vllm/attention/selector.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from functools import cache
|
||||||
|
from typing import Generator, Optional, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.attention.backends.abstract import AttentionBackend
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.platforms import _Backend, current_platform
|
||||||
|
from vllm.utils import STR_BACKEND_ENV_VAR, resolve_obj_by_qualname
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def backend_name_to_enum(backend_name: str) -> Optional[_Backend]:
|
||||||
|
"""
|
||||||
|
Convert a string backend name to a _Backend enum value.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
* _Backend: enum value if backend_name is a valid in-tree type
|
||||||
|
* None: otherwise it's an invalid in-tree type or an out-of-tree platform is
|
||||||
|
loaded.
|
||||||
|
"""
|
||||||
|
assert backend_name is not None
|
||||||
|
return _Backend[backend_name] if backend_name in _Backend.__members__ else \
|
||||||
|
None
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_variable_attn_backend() -> Optional[_Backend]:
|
||||||
|
'''
|
||||||
|
Get the backend override specified by the vLLM attention
|
||||||
|
backend environment variable, if one is specified.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* _Backend enum value if an override is specified
|
||||||
|
* None otherwise
|
||||||
|
'''
|
||||||
|
backend_name = os.environ.get(STR_BACKEND_ENV_VAR)
|
||||||
|
return (None
|
||||||
|
if backend_name is None else backend_name_to_enum(backend_name))
|
||||||
|
|
||||||
|
|
||||||
|
# Global state allows a particular choice of backend
|
||||||
|
# to be forced, overriding the logic which auto-selects
|
||||||
|
# a backend based on system & workload configuration
|
||||||
|
# (default behavior if this variable is None)
|
||||||
|
#
|
||||||
|
# THIS SELECTION TAKES PRECEDENCE OVER THE
|
||||||
|
# VLLM_ATTENTION_BACKEND ENVIRONMENT VARIABLE
|
||||||
|
forced_attn_backend: Optional[_Backend] = None
|
||||||
|
|
||||||
|
|
||||||
|
def global_force_attn_backend(attn_backend: Optional[_Backend]) -> None:
|
||||||
|
'''
|
||||||
|
Force all attention operations to use a specified backend.
|
||||||
|
|
||||||
|
Passing `None` for the argument re-enables automatic
|
||||||
|
backend selection.,
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_backend: backend selection (None to revert to auto)
|
||||||
|
'''
|
||||||
|
global forced_attn_backend
|
||||||
|
forced_attn_backend = attn_backend
|
||||||
|
|
||||||
|
|
||||||
|
def get_global_forced_attn_backend() -> Optional[_Backend]:
|
||||||
|
'''
|
||||||
|
Get the currently-forced choice of attention backend,
|
||||||
|
or None if auto-selection is currently enabled.
|
||||||
|
'''
|
||||||
|
return forced_attn_backend
|
||||||
|
|
||||||
|
|
||||||
|
def get_attn_backend(
|
||||||
|
head_size: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
kv_cache_dtype: Optional[str],
|
||||||
|
block_size: int,
|
||||||
|
is_attention_free: bool,
|
||||||
|
is_blocksparse: bool = False,
|
||||||
|
use_mla: bool = False,
|
||||||
|
) -> Type[AttentionBackend]:
|
||||||
|
"""Selects which attention backend to use and lazily imports it."""
|
||||||
|
# Accessing envs.* behind an @lru_cache decorator can cause the wrong
|
||||||
|
# value to be returned from the cache if the value changes between calls.
|
||||||
|
# To avoid this, we read envs.VLLM_USE_V1 here and pass it explicitly to the
|
||||||
|
# private function.
|
||||||
|
return _cached_get_attn_backend(
|
||||||
|
head_size=head_size,
|
||||||
|
dtype=dtype,
|
||||||
|
kv_cache_dtype=kv_cache_dtype,
|
||||||
|
block_size=block_size,
|
||||||
|
is_attention_free=is_attention_free,
|
||||||
|
is_blocksparse=is_blocksparse,
|
||||||
|
use_v1=envs.VLLM_USE_V1,
|
||||||
|
use_mla=use_mla,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def _cached_get_attn_backend(
|
||||||
|
head_size: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
kv_cache_dtype: Optional[str],
|
||||||
|
block_size: int,
|
||||||
|
is_attention_free: bool,
|
||||||
|
is_blocksparse: bool = False,
|
||||||
|
use_v1: bool = False,
|
||||||
|
use_mla: bool = False,
|
||||||
|
) -> Type[AttentionBackend]:
|
||||||
|
if is_blocksparse:
|
||||||
|
logger.info("Using BlocksparseFlashAttention backend.")
|
||||||
|
from vllm.attention.backends.blocksparse_attn import (
|
||||||
|
BlocksparseFlashAttentionBackend)
|
||||||
|
return BlocksparseFlashAttentionBackend
|
||||||
|
|
||||||
|
# If there are no attention layers (e.g. we are running Mamba),
|
||||||
|
# use the placeholder NO_ATTENTION
|
||||||
|
if is_attention_free:
|
||||||
|
from vllm.attention.backends.placeholder_attn import (
|
||||||
|
PlaceholderAttentionBackend)
|
||||||
|
return PlaceholderAttentionBackend
|
||||||
|
|
||||||
|
# Check whether a particular choice of backend was
|
||||||
|
# previously forced.
|
||||||
|
#
|
||||||
|
# THIS SELECTION OVERRIDES THE VLLM_ATTENTION_BACKEND
|
||||||
|
# ENVIRONMENT VARIABLE.
|
||||||
|
selected_backend = None
|
||||||
|
backend_by_global_setting: Optional[_Backend] = (
|
||||||
|
get_global_forced_attn_backend())
|
||||||
|
if backend_by_global_setting is not None:
|
||||||
|
selected_backend = backend_by_global_setting
|
||||||
|
else:
|
||||||
|
# Check the environment variable and override if specified
|
||||||
|
backend_by_env_var: Optional[str] = envs.VLLM_ATTENTION_BACKEND
|
||||||
|
if backend_by_env_var is not None:
|
||||||
|
selected_backend = backend_name_to_enum(backend_by_env_var)
|
||||||
|
|
||||||
|
# get device-specific attn_backend
|
||||||
|
attention_cls = current_platform.get_attn_backend_cls(
|
||||||
|
selected_backend, head_size, dtype, kv_cache_dtype, block_size, use_v1,
|
||||||
|
use_mla)
|
||||||
|
if not attention_cls:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid attention backend for {current_platform.device_name}")
|
||||||
|
return resolve_obj_by_qualname(attention_cls)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def global_force_attn_backend_context_manager(
|
||||||
|
attn_backend: _Backend) -> Generator[None, None, None]:
|
||||||
|
'''
|
||||||
|
Globally force a vLLM attention backend override within a
|
||||||
|
context manager, reverting the global attention backend
|
||||||
|
override to its prior state upon exiting the context
|
||||||
|
manager.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* attn_backend: attention backend to force
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
* Generator
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Save the current state of the global backend override (if any)
|
||||||
|
original_value = get_global_forced_attn_backend()
|
||||||
|
|
||||||
|
# Globally force the new backend override
|
||||||
|
global_force_attn_backend(attn_backend)
|
||||||
|
|
||||||
|
# Yield control back to the enclosed code block
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
# Revert the original global backend override, if any
|
||||||
|
global_force_attn_backend(original_value)
|
||||||
73
vllm/beam_search.py
Normal file
73
vllm/beam_search.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional, Union
|
||||||
|
|
||||||
|
from vllm.sequence import Logprob
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.multimodal import MultiModalDataDict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BeamSearchSequence:
|
||||||
|
"""A sequence for beam search.
|
||||||
|
It keeps track of the tokens and the log probability of the sequence.
|
||||||
|
The text field is optional and will only be filled when the sequence is
|
||||||
|
about to be returned to the user.
|
||||||
|
"""
|
||||||
|
# The tokens includes the prompt.
|
||||||
|
tokens: list[int]
|
||||||
|
logprobs: list[dict[int, Logprob]]
|
||||||
|
cum_logprob: float = 0.0
|
||||||
|
text: Optional[str] = None
|
||||||
|
finish_reason: Optional[str] = None
|
||||||
|
stop_reason: Union[int, str, None] = None
|
||||||
|
multi_modal_data: Optional["MultiModalDataDict"] = None
|
||||||
|
mm_processor_kwargs: Optional[dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BeamSearchOutput:
|
||||||
|
"""The output of beam search.
|
||||||
|
It contains the list of the best beam search sequences.
|
||||||
|
The length of the list is equal to the beam width.
|
||||||
|
"""
|
||||||
|
sequences: list[BeamSearchSequence]
|
||||||
|
|
||||||
|
|
||||||
|
class BeamSearchInstance:
|
||||||
|
|
||||||
|
def __init__(self, prompt_tokens: list[int]):
|
||||||
|
self.beams: list[BeamSearchSequence] = [
|
||||||
|
BeamSearchSequence(tokens=prompt_tokens, logprobs=[])
|
||||||
|
]
|
||||||
|
self.completed: list[BeamSearchSequence] = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_beam_search_score(
|
||||||
|
tokens: list[int],
|
||||||
|
cumulative_logprob: float,
|
||||||
|
eos_token_id: int,
|
||||||
|
length_penalty: float = 1.0,
|
||||||
|
) -> float:
|
||||||
|
"""Calculate the beam search score with length penalty.
|
||||||
|
|
||||||
|
Adapted from
|
||||||
|
|
||||||
|
https://github.com/huggingface/transformers/blob/ccb92be23def445f2afdea94c31286f84b89eb5b/src/transformers/generation/beam_search.py#L938
|
||||||
|
"""
|
||||||
|
seq_len = len(tokens)
|
||||||
|
if tokens[-1] == eos_token_id:
|
||||||
|
seq_len -= 1
|
||||||
|
|
||||||
|
return cumulative_logprob / (seq_len**length_penalty)
|
||||||
|
|
||||||
|
|
||||||
|
def create_sort_beams_key_function(eos_token_id: int, length_penalty: float):
|
||||||
|
|
||||||
|
def sort_beams_key(x: BeamSearchSequence) -> float:
|
||||||
|
return get_beam_search_score(x.tokens, x.cum_logprob, eos_token_id,
|
||||||
|
length_penalty)
|
||||||
|
|
||||||
|
return sort_beams_key
|
||||||
160
vllm/benchmarks/endpoint_request_func.py
Normal file
160
vllm/benchmarks/endpoint_request_func.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""The request function for API endpoints."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from tqdm.asyncio import tqdm
|
||||||
|
|
||||||
|
AIOHTTP_TIMEOUT = aiohttp.ClientTimeout(total=6 * 60 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RequestFuncInput:
|
||||||
|
"""The input for the request function."""
|
||||||
|
prompt: str
|
||||||
|
api_url: str
|
||||||
|
prompt_len: int
|
||||||
|
output_len: int
|
||||||
|
model: str
|
||||||
|
model_name: Optional[str] = None
|
||||||
|
best_of: int = 1
|
||||||
|
logprobs: Optional[int] = None
|
||||||
|
extra_body: Optional[dict] = None
|
||||||
|
multi_modal_content: Optional[dict] = None
|
||||||
|
ignore_eos: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RequestFuncOutput:
|
||||||
|
"""The output of the request function including metrics."""
|
||||||
|
generated_text: str = ""
|
||||||
|
success: bool = False
|
||||||
|
latency: float = 0.0
|
||||||
|
output_tokens: int = 0
|
||||||
|
ttft: float = 0.0 # Time to first token
|
||||||
|
itl: list[float] = field(
|
||||||
|
default_factory=list) # list of inter-token latencies
|
||||||
|
tpot: float = 0.0 # avg next-token latencies
|
||||||
|
prompt_len: int = 0
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
async def async_request_openai_completions(
|
||||||
|
request_func_input: RequestFuncInput,
|
||||||
|
pbar: Optional[tqdm] = None,
|
||||||
|
) -> RequestFuncOutput:
|
||||||
|
"""The async request function for the OpenAI Completions API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request_func_input: The input for the request function.
|
||||||
|
pbar: The progress bar to display the progress.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The output of the request function.
|
||||||
|
"""
|
||||||
|
api_url = request_func_input.api_url
|
||||||
|
assert api_url.endswith(
|
||||||
|
("completions", "profile")
|
||||||
|
), "OpenAI Completions API URL must end with 'completions' or 'profile'."
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(trust_env=True,
|
||||||
|
timeout=AIOHTTP_TIMEOUT) as session:
|
||||||
|
payload = {
|
||||||
|
"model": request_func_input.model_name \
|
||||||
|
if request_func_input.model_name else request_func_input.model,
|
||||||
|
"prompt": request_func_input.prompt,
|
||||||
|
"temperature": 0.0,
|
||||||
|
"best_of": request_func_input.best_of,
|
||||||
|
"max_tokens": request_func_input.output_len,
|
||||||
|
"logprobs": request_func_input.logprobs,
|
||||||
|
"stream": True,
|
||||||
|
"stream_options": {
|
||||||
|
"include_usage": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if request_func_input.ignore_eos:
|
||||||
|
payload["ignore_eos"] = request_func_input.ignore_eos
|
||||||
|
if request_func_input.extra_body:
|
||||||
|
payload.update(request_func_input.extra_body)
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {os.environ.get('OPENAI_API_KEY')}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output = RequestFuncOutput()
|
||||||
|
output.prompt_len = request_func_input.prompt_len
|
||||||
|
|
||||||
|
generated_text = ""
|
||||||
|
st = time.perf_counter()
|
||||||
|
most_recent_timestamp = st
|
||||||
|
try:
|
||||||
|
async with session.post(url=api_url, json=payload,
|
||||||
|
headers=headers) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
first_chunk_received = False
|
||||||
|
async for chunk_bytes in response.content:
|
||||||
|
chunk_bytes = chunk_bytes.strip()
|
||||||
|
if not chunk_bytes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
chunk = chunk_bytes.decode("utf-8").removeprefix(
|
||||||
|
"data: ")
|
||||||
|
if chunk != "[DONE]":
|
||||||
|
data = json.loads(chunk)
|
||||||
|
|
||||||
|
# NOTE: Some completion API might have a last
|
||||||
|
# usage summary response without a token so we
|
||||||
|
# want to check a token was generated
|
||||||
|
if choices := data.get("choices"):
|
||||||
|
# Note that text could be empty here
|
||||||
|
# e.g. for special tokens
|
||||||
|
text = choices[0].get("text")
|
||||||
|
timestamp = time.perf_counter()
|
||||||
|
# First token
|
||||||
|
if not first_chunk_received:
|
||||||
|
first_chunk_received = True
|
||||||
|
ttft = time.perf_counter() - st
|
||||||
|
output.ttft = ttft
|
||||||
|
|
||||||
|
# Decoding phase
|
||||||
|
else:
|
||||||
|
output.itl.append(timestamp -
|
||||||
|
most_recent_timestamp)
|
||||||
|
|
||||||
|
most_recent_timestamp = timestamp
|
||||||
|
generated_text += text or ""
|
||||||
|
elif usage := data.get("usage"):
|
||||||
|
output.output_tokens = usage.get(
|
||||||
|
"completion_tokens")
|
||||||
|
if first_chunk_received:
|
||||||
|
output.success = True
|
||||||
|
else:
|
||||||
|
output.success = False
|
||||||
|
output.error = (
|
||||||
|
"Never received a valid chunk to calculate TTFT."
|
||||||
|
"This response will be marked as failed!")
|
||||||
|
output.generated_text = generated_text
|
||||||
|
output.latency = most_recent_timestamp - st
|
||||||
|
else:
|
||||||
|
output.error = response.reason or ""
|
||||||
|
output.success = False
|
||||||
|
except Exception:
|
||||||
|
output.success = False
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
output.error = "".join(traceback.format_exception(*exc_info))
|
||||||
|
|
||||||
|
if pbar:
|
||||||
|
pbar.update(1)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Add more request functions for different API protocols.
|
||||||
|
ASYNC_REQUEST_FUNCS = {
|
||||||
|
"openai-comp": async_request_openai_completions,
|
||||||
|
}
|
||||||
925
vllm/benchmarks/serve.py
Normal file
925
vllm/benchmarks/serve.py
Normal file
@@ -0,0 +1,925 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
r"""Benchmark online serving throughput.
|
||||||
|
|
||||||
|
On the server side, run one of the following commands
|
||||||
|
to launch the vLLM OpenAI API server:
|
||||||
|
vllm serve <your_model> <engine arguments>
|
||||||
|
|
||||||
|
On the client side, run:
|
||||||
|
vllm bench serve \
|
||||||
|
--endpoint-type <endpoint_type. Default 'openi-comp'> \
|
||||||
|
--label <benchmark result label. Default using endpoint_type> \
|
||||||
|
--model <your_model> \
|
||||||
|
--dataset-name <dataset_name. Default 'random'> \
|
||||||
|
--request-rate <request_rate. Default inf> \
|
||||||
|
--num-prompts <num_prompts. Default 1000>
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import gc
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import warnings
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from tqdm.asyncio import tqdm
|
||||||
|
from transformers import PreTrainedTokenizerBase
|
||||||
|
|
||||||
|
from vllm.benchmarks.endpoint_request_func import (ASYNC_REQUEST_FUNCS,
|
||||||
|
RequestFuncInput,
|
||||||
|
RequestFuncOutput)
|
||||||
|
from vllm.benchmarks.utils import (convert_to_pytorch_benchmark_format,
|
||||||
|
write_to_json)
|
||||||
|
from vllm.transformers_utils.tokenizer import get_tokenizer
|
||||||
|
|
||||||
|
MILLISECONDS_TO_SECONDS_CONVERSION = 1000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BenchmarkMetrics:
|
||||||
|
completed: int
|
||||||
|
total_input: int
|
||||||
|
total_output: int
|
||||||
|
request_throughput: float
|
||||||
|
request_goodput: float
|
||||||
|
output_throughput: float
|
||||||
|
total_token_throughput: float
|
||||||
|
mean_ttft_ms: float
|
||||||
|
median_ttft_ms: float
|
||||||
|
std_ttft_ms: float
|
||||||
|
percentiles_ttft_ms: list[tuple[float, float]]
|
||||||
|
mean_tpot_ms: float
|
||||||
|
median_tpot_ms: float
|
||||||
|
std_tpot_ms: float
|
||||||
|
percentiles_tpot_ms: list[tuple[float, float]]
|
||||||
|
mean_itl_ms: float
|
||||||
|
median_itl_ms: float
|
||||||
|
std_itl_ms: float
|
||||||
|
percentiles_itl_ms: list[tuple[float, float]]
|
||||||
|
# E2EL stands for end-to-end latency per request.
|
||||||
|
# It is the time taken on the client side from sending
|
||||||
|
# a request to receiving a complete response.
|
||||||
|
mean_e2el_ms: float
|
||||||
|
median_e2el_ms: float
|
||||||
|
std_e2el_ms: float
|
||||||
|
percentiles_e2el_ms: list[tuple[float, float]]
|
||||||
|
|
||||||
|
|
||||||
|
def sample_random_requests(
|
||||||
|
prefix_len: int,
|
||||||
|
input_len: int,
|
||||||
|
output_len: int,
|
||||||
|
num_prompts: int,
|
||||||
|
range_ratio: float,
|
||||||
|
tokenizer: PreTrainedTokenizerBase,
|
||||||
|
) -> list[tuple[str, int, int]]:
|
||||||
|
prefix_token_ids = np.random.randint(0,
|
||||||
|
tokenizer.vocab_size,
|
||||||
|
size=prefix_len).tolist()
|
||||||
|
|
||||||
|
input_lens = np.random.randint(
|
||||||
|
int(input_len * range_ratio),
|
||||||
|
input_len + 1,
|
||||||
|
size=num_prompts,
|
||||||
|
)
|
||||||
|
output_lens = np.random.randint(
|
||||||
|
int(output_len * range_ratio),
|
||||||
|
output_len + 1,
|
||||||
|
size=num_prompts,
|
||||||
|
)
|
||||||
|
offsets = np.random.randint(0, tokenizer.vocab_size, size=num_prompts)
|
||||||
|
input_requests = []
|
||||||
|
for i in range(num_prompts):
|
||||||
|
prompt = tokenizer.decode(prefix_token_ids +
|
||||||
|
[(offsets[i] + i + j) % tokenizer.vocab_size
|
||||||
|
for j in range(input_lens[i])])
|
||||||
|
|
||||||
|
input_requests.append((prompt, int(prefix_len + input_lens[i]),
|
||||||
|
int(output_lens[i]), None))
|
||||||
|
|
||||||
|
return input_requests
|
||||||
|
|
||||||
|
|
||||||
|
async def get_request(
|
||||||
|
input_requests: list[tuple[str, int, int]],
|
||||||
|
request_rate: float,
|
||||||
|
burstiness: float = 1.0,
|
||||||
|
) -> AsyncGenerator[tuple[str, int, int], None]:
|
||||||
|
"""
|
||||||
|
Asynchronously generates requests at a specified rate
|
||||||
|
with OPTIONAL burstiness.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_requests:
|
||||||
|
A list of input requests, each represented as a tuple.
|
||||||
|
request_rate:
|
||||||
|
The rate at which requests are generated (requests/s).
|
||||||
|
burstiness (optional):
|
||||||
|
The burstiness factor of the request generation.
|
||||||
|
Only takes effect when request_rate is not inf.
|
||||||
|
Default value is 1, which follows a Poisson process.
|
||||||
|
Otherwise, the request intervals follow a gamma distribution.
|
||||||
|
A lower burstiness value (0 < burstiness < 1) results
|
||||||
|
in more bursty requests, while a higher burstiness value
|
||||||
|
(burstiness > 1) results in a more uniform arrival of requests.
|
||||||
|
"""
|
||||||
|
input_requests = iter(input_requests)
|
||||||
|
|
||||||
|
# Calculate scale parameter theta to maintain the desired request_rate.
|
||||||
|
assert burstiness > 0, (
|
||||||
|
f"A positive burstiness factor is expected, but given {burstiness}.")
|
||||||
|
theta = 1.0 / (request_rate * burstiness)
|
||||||
|
|
||||||
|
for request in input_requests:
|
||||||
|
yield request
|
||||||
|
|
||||||
|
if request_rate == float("inf"):
|
||||||
|
# If the request rate is infinity, then we don't need to wait.
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sample the request interval from the gamma distribution.
|
||||||
|
# If burstiness is 1, it follows exponential distribution.
|
||||||
|
interval = np.random.gamma(shape=burstiness, scale=theta)
|
||||||
|
# The next request will be sent after the interval.
|
||||||
|
await asyncio.sleep(interval)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_metrics(
|
||||||
|
input_requests: list[tuple[str, int, int]],
|
||||||
|
outputs: list[RequestFuncOutput],
|
||||||
|
dur_s: float,
|
||||||
|
tokenizer: PreTrainedTokenizerBase,
|
||||||
|
selected_percentiles: list[float],
|
||||||
|
goodput_config_dict: dict[str, float],
|
||||||
|
) -> tuple[BenchmarkMetrics, list[int]]:
|
||||||
|
"""Calculate the metrics for the benchmark.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_requests: The input requests.
|
||||||
|
outputs: The outputs of the requests.
|
||||||
|
dur_s: The duration of the benchmark.
|
||||||
|
tokenizer: The tokenizer to use.
|
||||||
|
selected_percentiles: The percentiles to select.
|
||||||
|
goodput_config_dict: The goodput configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple of the benchmark metrics and the actual output lengths.
|
||||||
|
"""
|
||||||
|
actual_output_lens: list[int] = []
|
||||||
|
total_input = 0
|
||||||
|
completed = 0
|
||||||
|
good_completed = 0
|
||||||
|
itls: list[float] = []
|
||||||
|
tpots: list[float] = []
|
||||||
|
all_tpots: list[float] = []
|
||||||
|
ttfts: list[float] = []
|
||||||
|
e2els: list[float] = []
|
||||||
|
for i in range(len(outputs)):
|
||||||
|
if outputs[i].success:
|
||||||
|
output_len = outputs[i].output_tokens
|
||||||
|
|
||||||
|
if output_len is None:
|
||||||
|
# We use the tokenizer to count the number of output tokens
|
||||||
|
# for some serving backends instead of looking at
|
||||||
|
# len(outputs[i].itl) since multiple output tokens may be
|
||||||
|
# bundled together
|
||||||
|
# Note : this may inflate the output token count slightly
|
||||||
|
output_len = len(
|
||||||
|
tokenizer(outputs[i].generated_text,
|
||||||
|
add_special_tokens=False).input_ids)
|
||||||
|
actual_output_lens.append(output_len)
|
||||||
|
total_input += input_requests[i][1]
|
||||||
|
tpot = 0
|
||||||
|
if output_len > 1:
|
||||||
|
latency_minus_ttft = outputs[i].latency - outputs[i].ttft
|
||||||
|
tpot = latency_minus_ttft / (output_len - 1)
|
||||||
|
tpots.append(tpot)
|
||||||
|
# Note: if output_len <= 1, we regard tpot as 0 for goodput
|
||||||
|
all_tpots.append(tpot)
|
||||||
|
itls += outputs[i].itl
|
||||||
|
ttfts.append(outputs[i].ttft)
|
||||||
|
e2els.append(outputs[i].latency)
|
||||||
|
completed += 1
|
||||||
|
else:
|
||||||
|
actual_output_lens.append(0)
|
||||||
|
|
||||||
|
if goodput_config_dict:
|
||||||
|
valid_metrics = []
|
||||||
|
slo_values = []
|
||||||
|
|
||||||
|
if "ttft" in goodput_config_dict:
|
||||||
|
valid_metrics.append(ttfts)
|
||||||
|
slo_values.append(goodput_config_dict["ttft"] /
|
||||||
|
MILLISECONDS_TO_SECONDS_CONVERSION)
|
||||||
|
if "tpot" in goodput_config_dict:
|
||||||
|
valid_metrics.append(all_tpots)
|
||||||
|
slo_values.append(goodput_config_dict["tpot"] /
|
||||||
|
MILLISECONDS_TO_SECONDS_CONVERSION)
|
||||||
|
if "e2el" in goodput_config_dict:
|
||||||
|
valid_metrics.append(e2els)
|
||||||
|
slo_values.append(goodput_config_dict["e2el"] /
|
||||||
|
MILLISECONDS_TO_SECONDS_CONVERSION)
|
||||||
|
|
||||||
|
for req_metric in zip(*valid_metrics):
|
||||||
|
is_good_req = all([s >= r for s, r in zip(slo_values, req_metric)])
|
||||||
|
if is_good_req:
|
||||||
|
good_completed += 1
|
||||||
|
|
||||||
|
if completed == 0:
|
||||||
|
warnings.warn(
|
||||||
|
"All requests failed. This is likely due to a misconfiguration "
|
||||||
|
"on the benchmark arguments.",
|
||||||
|
stacklevel=2)
|
||||||
|
metrics = BenchmarkMetrics(
|
||||||
|
completed=completed,
|
||||||
|
total_input=total_input,
|
||||||
|
total_output=sum(actual_output_lens),
|
||||||
|
request_throughput=completed / dur_s,
|
||||||
|
request_goodput=good_completed / dur_s,
|
||||||
|
output_throughput=sum(actual_output_lens) / dur_s,
|
||||||
|
total_token_throughput=(total_input + sum(actual_output_lens)) / dur_s,
|
||||||
|
mean_ttft_ms=np.mean(ttfts or 0) *
|
||||||
|
1000, # ttfts is empty if streaming is not supported by the endpoint
|
||||||
|
std_ttft_ms=np.std(ttfts or 0) * 1000,
|
||||||
|
median_ttft_ms=np.median(ttfts or 0) * 1000,
|
||||||
|
percentiles_ttft_ms=[(p, np.percentile(ttfts or 0, p) * 1000)
|
||||||
|
for p in selected_percentiles],
|
||||||
|
mean_tpot_ms=np.mean(tpots or 0) * 1000,
|
||||||
|
std_tpot_ms=np.std(tpots or 0) * 1000,
|
||||||
|
median_tpot_ms=np.median(tpots or 0) * 1000,
|
||||||
|
percentiles_tpot_ms=[(p, np.percentile(tpots or 0, p) * 1000)
|
||||||
|
for p in selected_percentiles],
|
||||||
|
mean_itl_ms=np.mean(itls or 0) * 1000,
|
||||||
|
std_itl_ms=np.std(itls or 0) * 1000,
|
||||||
|
median_itl_ms=np.median(itls or 0) * 1000,
|
||||||
|
percentiles_itl_ms=[(p, np.percentile(itls or 0, p) * 1000)
|
||||||
|
for p in selected_percentiles],
|
||||||
|
mean_e2el_ms=np.mean(e2els or 0) * 1000,
|
||||||
|
std_e2el_ms=np.std(e2els or 0) * 1000,
|
||||||
|
median_e2el_ms=np.median(e2els or 0) * 1000,
|
||||||
|
percentiles_e2el_ms=[(p, np.percentile(e2els or 0, p) * 1000)
|
||||||
|
for p in selected_percentiles],
|
||||||
|
)
|
||||||
|
|
||||||
|
return metrics, actual_output_lens
|
||||||
|
|
||||||
|
|
||||||
|
async def benchmark(
|
||||||
|
endpoint_type: str,
|
||||||
|
api_url: str,
|
||||||
|
base_url: str,
|
||||||
|
model_id: str,
|
||||||
|
model_name: str,
|
||||||
|
tokenizer: PreTrainedTokenizerBase,
|
||||||
|
input_requests: list[tuple[str, int, int]],
|
||||||
|
logprobs: Optional[int],
|
||||||
|
best_of: int,
|
||||||
|
request_rate: float,
|
||||||
|
burstiness: float,
|
||||||
|
disable_tqdm: bool,
|
||||||
|
profile: bool,
|
||||||
|
selected_percentile_metrics: list[str],
|
||||||
|
selected_percentiles: list[str],
|
||||||
|
ignore_eos: bool,
|
||||||
|
goodput_config_dict: dict[str, float],
|
||||||
|
max_concurrency: Optional[int],
|
||||||
|
lora_modules: Optional[list[str]],
|
||||||
|
):
|
||||||
|
if endpoint_type in ASYNC_REQUEST_FUNCS:
|
||||||
|
request_func = ASYNC_REQUEST_FUNCS[endpoint_type]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown endpoint_type: {endpoint_type}")
|
||||||
|
|
||||||
|
print("Starting initial single prompt test run...")
|
||||||
|
test_prompt, test_prompt_len, test_output_len, test_mm_content = (
|
||||||
|
input_requests[0])
|
||||||
|
if endpoint_type != "openai-chat" and test_mm_content is not None:
|
||||||
|
# multi-modal benchmark is only available on OpenAI Chat endpoint.
|
||||||
|
raise ValueError("Multi-modal content is only supported on "
|
||||||
|
"'openai-chat' endpoint_type.")
|
||||||
|
test_input = RequestFuncInput(
|
||||||
|
model=model_id,
|
||||||
|
model_name=model_name,
|
||||||
|
prompt=test_prompt,
|
||||||
|
api_url=api_url,
|
||||||
|
prompt_len=test_prompt_len,
|
||||||
|
output_len=test_output_len,
|
||||||
|
logprobs=logprobs,
|
||||||
|
best_of=best_of,
|
||||||
|
multi_modal_content=test_mm_content,
|
||||||
|
ignore_eos=ignore_eos,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_output = await request_func(request_func_input=test_input)
|
||||||
|
if not test_output.success:
|
||||||
|
raise ValueError(
|
||||||
|
"Initial test run failed - Please make sure benchmark arguments "
|
||||||
|
f"are correctly specified. Error: {test_output.error}")
|
||||||
|
else:
|
||||||
|
print("Initial test run completed. Starting main benchmark run...")
|
||||||
|
|
||||||
|
if lora_modules:
|
||||||
|
# For each input request, choose a LoRA module at random.
|
||||||
|
lora_modules = iter(
|
||||||
|
[random.choice(lora_modules) for _ in range(len(input_requests))])
|
||||||
|
|
||||||
|
if profile:
|
||||||
|
print("Starting profiler...")
|
||||||
|
profile_input = RequestFuncInput(model=model_id,
|
||||||
|
model_name=model_name,
|
||||||
|
prompt=test_prompt,
|
||||||
|
api_url=base_url + "/start_profile",
|
||||||
|
prompt_len=test_prompt_len,
|
||||||
|
output_len=test_output_len,
|
||||||
|
logprobs=logprobs,
|
||||||
|
best_of=best_of,
|
||||||
|
multi_modal_content=test_mm_content,
|
||||||
|
ignore_eos=ignore_eos)
|
||||||
|
profile_output = await request_func(request_func_input=profile_input)
|
||||||
|
if profile_output.success:
|
||||||
|
print("Profiler started")
|
||||||
|
|
||||||
|
if burstiness == 1.0:
|
||||||
|
distribution = "Poisson process"
|
||||||
|
else:
|
||||||
|
distribution = "Gamma distribution"
|
||||||
|
|
||||||
|
print(f"Traffic request rate: {request_rate}")
|
||||||
|
print(f"Burstiness factor: {burstiness} ({distribution})")
|
||||||
|
print(f"Maximum request concurrency: {max_concurrency}")
|
||||||
|
|
||||||
|
pbar = None if disable_tqdm else tqdm(total=len(input_requests))
|
||||||
|
|
||||||
|
# This can be used once the minimum Python version is 3.10 or higher,
|
||||||
|
# and it will simplify the code in limited_request_func.
|
||||||
|
# semaphore = (asyncio.Semaphore(max_concurrency)
|
||||||
|
# if max_concurrency else contextlib.nullcontext())
|
||||||
|
semaphore = (asyncio.Semaphore(max_concurrency)
|
||||||
|
if max_concurrency else None)
|
||||||
|
|
||||||
|
async def limited_request_func(request_func_input, pbar):
|
||||||
|
if semaphore is None:
|
||||||
|
return await request_func(request_func_input=request_func_input,
|
||||||
|
pbar=pbar)
|
||||||
|
async with semaphore:
|
||||||
|
return await request_func(request_func_input=request_func_input,
|
||||||
|
pbar=pbar)
|
||||||
|
|
||||||
|
benchmark_start_time = time.perf_counter()
|
||||||
|
tasks: list[asyncio.Task] = []
|
||||||
|
async for request in get_request(input_requests, request_rate, burstiness):
|
||||||
|
prompt, prompt_len, output_len, mm_content = request
|
||||||
|
req_model_id, req_model_name = model_id, model_name
|
||||||
|
if lora_modules:
|
||||||
|
req_lora_module = next(lora_modules)
|
||||||
|
req_model_id, req_model_name = req_lora_module, req_lora_module
|
||||||
|
|
||||||
|
request_func_input = RequestFuncInput(model=req_model_id,
|
||||||
|
model_name=req_model_name,
|
||||||
|
prompt=prompt,
|
||||||
|
api_url=api_url,
|
||||||
|
prompt_len=prompt_len,
|
||||||
|
output_len=output_len,
|
||||||
|
logprobs=logprobs,
|
||||||
|
best_of=best_of,
|
||||||
|
multi_modal_content=mm_content,
|
||||||
|
ignore_eos=ignore_eos)
|
||||||
|
tasks.append(
|
||||||
|
asyncio.create_task(
|
||||||
|
limited_request_func(request_func_input=request_func_input,
|
||||||
|
pbar=pbar)))
|
||||||
|
outputs: list[RequestFuncOutput] = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
if profile:
|
||||||
|
print("Stopping profiler...")
|
||||||
|
profile_input = RequestFuncInput(
|
||||||
|
model=model_id,
|
||||||
|
prompt=test_prompt,
|
||||||
|
api_url=base_url + "/stop_profile",
|
||||||
|
prompt_len=test_prompt_len,
|
||||||
|
output_len=test_output_len,
|
||||||
|
logprobs=logprobs,
|
||||||
|
best_of=best_of,
|
||||||
|
)
|
||||||
|
profile_output = await request_func(request_func_input=profile_input)
|
||||||
|
if profile_output.success:
|
||||||
|
print("Profiler stopped")
|
||||||
|
|
||||||
|
if pbar is not None:
|
||||||
|
pbar.close()
|
||||||
|
|
||||||
|
benchmark_duration = time.perf_counter() - benchmark_start_time
|
||||||
|
|
||||||
|
metrics, actual_output_lens = calculate_metrics(
|
||||||
|
input_requests=input_requests,
|
||||||
|
outputs=outputs,
|
||||||
|
dur_s=benchmark_duration,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
selected_percentiles=selected_percentiles,
|
||||||
|
goodput_config_dict=goodput_config_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("{s:{c}^{n}}".format(s=' Serving Benchmark Result ', n=50, c='='))
|
||||||
|
print("{:<40} {:<10}".format("Successful requests:", metrics.completed))
|
||||||
|
print("{:<40} {:<10.2f}".format("Benchmark duration (s):",
|
||||||
|
benchmark_duration))
|
||||||
|
print("{:<40} {:<10}".format("Total input tokens:", metrics.total_input))
|
||||||
|
print("{:<40} {:<10}".format("Total generated tokens:",
|
||||||
|
metrics.total_output))
|
||||||
|
print("{:<40} {:<10.2f}".format("Request throughput (req/s):",
|
||||||
|
metrics.request_throughput))
|
||||||
|
if goodput_config_dict:
|
||||||
|
print("{:<40} {:<10.2f}".format("Request goodput (req/s):",
|
||||||
|
metrics.request_goodput))
|
||||||
|
print("{:<40} {:<10.2f}".format("Output token throughput (tok/s):",
|
||||||
|
metrics.output_throughput))
|
||||||
|
print("{:<40} {:<10.2f}".format("Total Token throughput (tok/s):",
|
||||||
|
metrics.total_token_throughput))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"duration": benchmark_duration,
|
||||||
|
"completed": metrics.completed,
|
||||||
|
"total_input_tokens": metrics.total_input,
|
||||||
|
"total_output_tokens": metrics.total_output,
|
||||||
|
"request_throughput": metrics.request_throughput,
|
||||||
|
"request_goodput:":
|
||||||
|
metrics.request_goodput if goodput_config_dict else None,
|
||||||
|
"output_throughput": metrics.output_throughput,
|
||||||
|
"total_token_throughput": metrics.total_token_throughput,
|
||||||
|
"input_lens": [output.prompt_len for output in outputs],
|
||||||
|
"output_lens": actual_output_lens,
|
||||||
|
"ttfts": [output.ttft for output in outputs],
|
||||||
|
"itls": [output.itl for output in outputs],
|
||||||
|
"generated_texts": [output.generated_text for output in outputs],
|
||||||
|
"errors": [output.error for output in outputs],
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_one_metric(
|
||||||
|
# E.g., "ttft"
|
||||||
|
metric_attribute_name: str,
|
||||||
|
# E.g., "TTFT"
|
||||||
|
metric_name: str,
|
||||||
|
# E.g., "Time to First Token"
|
||||||
|
metric_header: str,
|
||||||
|
):
|
||||||
|
# This function prints and adds statistics of the specified
|
||||||
|
# metric.
|
||||||
|
if metric_attribute_name not in selected_percentile_metrics:
|
||||||
|
return
|
||||||
|
print("{s:{c}^{n}}".format(s=metric_header, n=50, c='-'))
|
||||||
|
print("{:<40} {:<10.2f}".format(
|
||||||
|
f"Mean {metric_name} (ms):",
|
||||||
|
getattr(metrics, f"mean_{metric_attribute_name}_ms")))
|
||||||
|
print("{:<40} {:<10.2f}".format(
|
||||||
|
f"Median {metric_name} (ms):",
|
||||||
|
getattr(metrics, f"median_{metric_attribute_name}_ms")))
|
||||||
|
result[f"mean_{metric_attribute_name}_ms"] = getattr(
|
||||||
|
metrics, f"mean_{metric_attribute_name}_ms")
|
||||||
|
result[f"median_{metric_attribute_name}_ms"] = getattr(
|
||||||
|
metrics, f"median_{metric_attribute_name}_ms")
|
||||||
|
result[f"std_{metric_attribute_name}_ms"] = getattr(
|
||||||
|
metrics, f"std_{metric_attribute_name}_ms")
|
||||||
|
for p, value in getattr(metrics,
|
||||||
|
f"percentiles_{metric_attribute_name}_ms"):
|
||||||
|
p_word = str(int(p)) if int(p) == p else str(p)
|
||||||
|
print("{:<40} {:<10.2f}".format(f"P{p_word} {metric_name} (ms):",
|
||||||
|
value))
|
||||||
|
result[f"p{p_word}_{metric_attribute_name}_ms"] = value
|
||||||
|
|
||||||
|
process_one_metric("ttft", "TTFT", "Time to First Token")
|
||||||
|
process_one_metric("tpot", "TPOT",
|
||||||
|
"Time per Output Token (excl. 1st token)")
|
||||||
|
process_one_metric("itl", "ITL", "Inter-token Latency")
|
||||||
|
process_one_metric("e2el", "E2EL", "End-to-end Latency")
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def check_goodput_args(args):
|
||||||
|
# Check and parse goodput arguments
|
||||||
|
goodput_config_dict = {}
|
||||||
|
VALID_NAMES = ["ttft", "tpot", "e2el"]
|
||||||
|
if args.goodput:
|
||||||
|
goodput_config_dict = parse_goodput(args.goodput)
|
||||||
|
for slo_name, slo_val in goodput_config_dict.items():
|
||||||
|
if slo_name not in VALID_NAMES:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid metric name found, {slo_name}: {slo_val}. "
|
||||||
|
"The service level objective name should be one of "
|
||||||
|
f"{str(VALID_NAMES)}. ")
|
||||||
|
if slo_val < 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid value found, {slo_name}: {slo_val}. "
|
||||||
|
"The service level objective value should be "
|
||||||
|
"non-negative.")
|
||||||
|
return goodput_config_dict
|
||||||
|
|
||||||
|
|
||||||
|
def parse_goodput(slo_pairs):
|
||||||
|
goodput_config_dict = {}
|
||||||
|
try:
|
||||||
|
for slo_pair in slo_pairs:
|
||||||
|
slo_name, slo_val = slo_pair.split(":")
|
||||||
|
goodput_config_dict[slo_name] = float(slo_val)
|
||||||
|
except ValueError as err:
|
||||||
|
raise argparse.ArgumentTypeError(
|
||||||
|
"Invalid format found for service level objectives. "
|
||||||
|
"Specify service level objectives for goodput as \"KEY:VALUE\" "
|
||||||
|
"pairs, where the key is a metric name, and the value is a "
|
||||||
|
"number in milliseconds.") from err
|
||||||
|
return goodput_config_dict
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_pytorch_benchmark_format(args: argparse.Namespace,
|
||||||
|
results: dict[str, Any],
|
||||||
|
file_name: str) -> None:
|
||||||
|
metrics = [
|
||||||
|
"median_ttft_ms", "mean_ttft_ms", "std_ttft_ms", "p99_ttft_ms",
|
||||||
|
"mean_tpot_ms", "median_tpot_ms", "std_tpot_ms", "p99_tpot_ms",
|
||||||
|
"median_itl_ms", "mean_itl_ms", "std_itl_ms", "p99_itl_ms"
|
||||||
|
]
|
||||||
|
# These raw data might be useful, but they are rather big. They can be added
|
||||||
|
# later if needed
|
||||||
|
ignored_metrics = ["ttfts", "itls", "generated_texts", "errors"]
|
||||||
|
pt_records = convert_to_pytorch_benchmark_format(
|
||||||
|
args=args,
|
||||||
|
metrics={k: [results[k]]
|
||||||
|
for k in metrics},
|
||||||
|
extra_info={
|
||||||
|
k: results[k]
|
||||||
|
for k in results if k not in metrics and k not in ignored_metrics
|
||||||
|
})
|
||||||
|
if pt_records:
|
||||||
|
# Don't use json suffix here as we don't want CI to pick it up
|
||||||
|
pt_file = f"{os.path.splitext(file_name)[0]}.pytorch.json"
|
||||||
|
write_to_json(pt_file, pt_records)
|
||||||
|
|
||||||
|
|
||||||
|
def add_cli_args(parser: argparse.ArgumentParser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--endpoint-type",
|
||||||
|
type=str,
|
||||||
|
default="openai-comp",
|
||||||
|
choices=list(ASYNC_REQUEST_FUNCS.keys()),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--label",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="The label (prefix) of the benchmark results. If not specified, "
|
||||||
|
"the endpoint type will be used as the label.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--base-url",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Server or API base url if not using http host and port.",
|
||||||
|
)
|
||||||
|
# Use 127.0.0.1 here instead of localhost to force the use of ipv4
|
||||||
|
parser.add_argument("--host", type=str, default="127.0.0.1")
|
||||||
|
parser.add_argument("--port", type=int, default=8000)
|
||||||
|
parser.add_argument(
|
||||||
|
"--endpoint",
|
||||||
|
type=str,
|
||||||
|
default="/v1/completions",
|
||||||
|
help="API endpoint.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dataset-name",
|
||||||
|
type=str,
|
||||||
|
default="random",
|
||||||
|
choices=["random"],
|
||||||
|
help="Name of the dataset to benchmark on.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-concurrency",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help="Maximum number of concurrent requests. This can be used "
|
||||||
|
"to help simulate an environment where a higher level component "
|
||||||
|
"is enforcing a maximum number of concurrent requests. While the "
|
||||||
|
"--request-rate argument controls the rate at which requests are "
|
||||||
|
"initiated, this argument will control how many are actually allowed "
|
||||||
|
"to execute at a time. This means that when used in combination, the "
|
||||||
|
"actual request rate may be lower than specified with --request-rate, "
|
||||||
|
"if the server is not processing requests fast enough to keep up.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
type=str,
|
||||||
|
required=True,
|
||||||
|
help="Name of the model.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tokenizer",
|
||||||
|
type=str,
|
||||||
|
help=
|
||||||
|
"Name or path of the tokenizer, if not using the default tokenizer.", # noqa: E501
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--best-of",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Generates `best_of` sequences per prompt and "
|
||||||
|
"returns the best one.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--use-beam-search", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--num-prompts",
|
||||||
|
type=int,
|
||||||
|
default=1000,
|
||||||
|
help="Number of prompts to process.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--logprobs",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help=("Number of logprobs-per-token to compute & return as part of "
|
||||||
|
"the request. If unspecified, then either (1) if beam search "
|
||||||
|
"is disabled, no logprobs are computed & a single dummy "
|
||||||
|
"logprob is returned for each token; or (2) if beam search "
|
||||||
|
"is enabled 1 logprob per token is computed"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--request-rate",
|
||||||
|
type=float,
|
||||||
|
default=float("inf"),
|
||||||
|
help="Number of requests per second. If this is inf, "
|
||||||
|
"then all the requests are sent at time 0. "
|
||||||
|
"Otherwise, we use Poisson process or gamma distribution "
|
||||||
|
"to synthesize the request arrival times.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--burstiness",
|
||||||
|
type=float,
|
||||||
|
default=1.0,
|
||||||
|
help="Burstiness factor of the request generation. "
|
||||||
|
"Only take effect when request_rate is not inf. "
|
||||||
|
"Default value is 1, which follows Poisson process. "
|
||||||
|
"Otherwise, the request intervals follow a gamma distribution. "
|
||||||
|
"A lower burstiness value (0 < burstiness < 1) results in more "
|
||||||
|
"bursty requests. A higher burstiness value (burstiness > 1) "
|
||||||
|
"results in a more uniform arrival of requests.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--seed", type=int, default=0)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trust-remote-code",
|
||||||
|
action="store_true",
|
||||||
|
help="Trust remote code from huggingface",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-tqdm",
|
||||||
|
action="store_true",
|
||||||
|
help="Specify to disable tqdm progress bar.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--profile",
|
||||||
|
action="store_true",
|
||||||
|
help="Use Torch Profiler. The endpoint must be launched with "
|
||||||
|
"VLLM_TORCH_PROFILER_DIR to enable profiler.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--save-result",
|
||||||
|
action="store_true",
|
||||||
|
help="Specify to save benchmark results to a json file",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--metadata",
|
||||||
|
metavar="KEY=VALUE",
|
||||||
|
nargs="*",
|
||||||
|
help="Key-value pairs (e.g, --metadata version=0.3.3 tp=1) "
|
||||||
|
"for metadata of this run to be saved in the result JSON file "
|
||||||
|
"for record keeping purposes.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--result-dir",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Specify directory to save benchmark json results."
|
||||||
|
"If not specified, results are saved in the current directory.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--result-filename",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Specify the filename to save benchmark json results."
|
||||||
|
"If not specified, results will be saved in "
|
||||||
|
"{label}-{args.request_rate}qps-{base_model_id}-{current_dt}.json" # noqa
|
||||||
|
" format.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ignore-eos",
|
||||||
|
action="store_true",
|
||||||
|
help="Set ignore_eos flag when sending the benchmark request."
|
||||||
|
"Warning: ignore_eos is not supported in deepspeed_mii and tgi.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--percentile-metrics",
|
||||||
|
type=str,
|
||||||
|
default="ttft,tpot,itl",
|
||||||
|
help="Comma-seperated list of selected metrics to report percentils. "
|
||||||
|
"This argument specifies the metrics to report percentiles. "
|
||||||
|
"Allowed metric names are \"ttft\", \"tpot\", \"itl\", \"e2el\". ")
|
||||||
|
parser.add_argument(
|
||||||
|
"--metric-percentiles",
|
||||||
|
type=str,
|
||||||
|
default="99",
|
||||||
|
help="Comma-seperated list of percentiles for selected metrics. "
|
||||||
|
"To report 25-th, 50-th, and 75-th percentiles, use \"25,50,75\". "
|
||||||
|
"Use \"--percentile-metrics\" to select metrics.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--goodput",
|
||||||
|
nargs="+",
|
||||||
|
required=False,
|
||||||
|
help="Specify service level objectives for goodput as \"KEY:VALUE\" "
|
||||||
|
"pairs, where the key is a metric name, and the value is in "
|
||||||
|
"milliseconds. Multiple \"KEY:VALUE\" pairs can be provided, "
|
||||||
|
"separated by spaces. Allowed request level metric names are "
|
||||||
|
"\"ttft\", \"tpot\", \"e2el\". For more context on the definition of "
|
||||||
|
"goodput, refer to DistServe paper: https://arxiv.org/pdf/2401.09670 "
|
||||||
|
"and the blog: https://hao-ai-lab.github.io/blogs/distserve")
|
||||||
|
|
||||||
|
random_group = parser.add_argument_group("random dataset options")
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-input-len",
|
||||||
|
type=int,
|
||||||
|
default=1024,
|
||||||
|
help=
|
||||||
|
"Number of input tokens per request, used only for random sampling.",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-output-len",
|
||||||
|
type=int,
|
||||||
|
default=128,
|
||||||
|
help=
|
||||||
|
"Number of output tokens per request, used only for random sampling.",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-range-ratio",
|
||||||
|
type=float,
|
||||||
|
default=1.0,
|
||||||
|
help="Range of sampled ratio of input/output length, "
|
||||||
|
"used only for random sampling.",
|
||||||
|
)
|
||||||
|
random_group.add_argument(
|
||||||
|
"--random-prefix-len",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Number of fixed prefix tokens before random "
|
||||||
|
" context. The length range of context in a random "
|
||||||
|
" request is [random-prefix-len, "
|
||||||
|
" random-prefix-len + random-prefix-len * random-range-ratio).")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--tokenizer-mode',
|
||||||
|
type=str,
|
||||||
|
default="auto",
|
||||||
|
choices=['auto', 'slow', 'mistral', 'custom'],
|
||||||
|
help='The tokenizer mode.\n\n* "auto" will use the '
|
||||||
|
'fast tokenizer if available.\n* "slow" will '
|
||||||
|
'always use the slow tokenizer. \n* '
|
||||||
|
'"mistral" will always use the `mistral_common` tokenizer. \n*'
|
||||||
|
'"custom" will use --tokenizer to select the preregistered tokenizer.')
|
||||||
|
|
||||||
|
parser.add_argument("--served-model-name",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="The model name used in the API. "
|
||||||
|
"If not specified, the model name will be the "
|
||||||
|
"same as the ``--model`` argument. ")
|
||||||
|
|
||||||
|
parser.add_argument("--lora-modules",
|
||||||
|
nargs='+',
|
||||||
|
default=None,
|
||||||
|
help="A subset of LoRA module names passed in when "
|
||||||
|
"launching the server. For each request, the "
|
||||||
|
"script chooses a LoRA module at random.")
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: argparse.Namespace):
|
||||||
|
print(args)
|
||||||
|
random.seed(args.seed)
|
||||||
|
np.random.seed(args.seed)
|
||||||
|
|
||||||
|
endpoint_type = args.endpoint_type
|
||||||
|
label = args.label
|
||||||
|
model_id = args.model
|
||||||
|
model_name = args.served_model_name
|
||||||
|
tokenizer_id = args.tokenizer if args.tokenizer is not None else args.model
|
||||||
|
tokenizer_mode = args.tokenizer_mode
|
||||||
|
|
||||||
|
if args.base_url is not None:
|
||||||
|
api_url = f"{args.base_url}{args.endpoint}"
|
||||||
|
base_url = f"{args.base_url}"
|
||||||
|
else:
|
||||||
|
api_url = f"http://{args.host}:{args.port}{args.endpoint}"
|
||||||
|
base_url = f"http://{args.host}:{args.port}"
|
||||||
|
|
||||||
|
tokenizer = get_tokenizer(tokenizer_id,
|
||||||
|
tokenizer_mode=tokenizer_mode,
|
||||||
|
trust_remote_code=args.trust_remote_code)
|
||||||
|
# TODO: This should be refactored to use the benchmark_dataset.py
|
||||||
|
# in later PRs.
|
||||||
|
if args.dataset_name is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Please specify '--dataset-name' and the corresponding "
|
||||||
|
"'--dataset-path' if required.")
|
||||||
|
elif args.dataset_name == "random":
|
||||||
|
input_requests = sample_random_requests(
|
||||||
|
prefix_len=args.random_prefix_len,
|
||||||
|
input_len=args.random_input_len,
|
||||||
|
output_len=args.random_output_len,
|
||||||
|
num_prompts=args.num_prompts,
|
||||||
|
range_ratio=args.random_range_ratio,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown dataset: {args.dataset_name}")
|
||||||
|
|
||||||
|
goodput_config_dict = check_goodput_args(args)
|
||||||
|
|
||||||
|
# Avoid GC processing "static" data - reduce pause times.
|
||||||
|
gc.collect()
|
||||||
|
gc.freeze()
|
||||||
|
|
||||||
|
benchmark_result = asyncio.run(
|
||||||
|
benchmark(
|
||||||
|
endpoint_type=endpoint_type,
|
||||||
|
api_url=api_url,
|
||||||
|
base_url=base_url,
|
||||||
|
model_id=model_id,
|
||||||
|
model_name=model_name,
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
input_requests=input_requests,
|
||||||
|
logprobs=args.logprobs,
|
||||||
|
best_of=args.best_of,
|
||||||
|
request_rate=args.request_rate,
|
||||||
|
burstiness=args.burstiness,
|
||||||
|
disable_tqdm=args.disable_tqdm,
|
||||||
|
profile=args.profile,
|
||||||
|
selected_percentile_metrics=args.percentile_metrics.split(","),
|
||||||
|
selected_percentiles=[
|
||||||
|
float(p) for p in args.metric_percentiles.split(",")
|
||||||
|
],
|
||||||
|
ignore_eos=args.ignore_eos,
|
||||||
|
goodput_config_dict=goodput_config_dict,
|
||||||
|
max_concurrency=args.max_concurrency,
|
||||||
|
lora_modules=args.lora_modules,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Save config and results to json
|
||||||
|
if args.save_result:
|
||||||
|
result_json: dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
current_dt = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
result_json["date"] = current_dt
|
||||||
|
result_json["endpoint_type"] = endpoint_type
|
||||||
|
result_json["label"] = label
|
||||||
|
result_json["model_id"] = model_id
|
||||||
|
result_json["tokenizer_id"] = tokenizer_id
|
||||||
|
result_json["best_of"] = args.best_of
|
||||||
|
result_json["num_prompts"] = args.num_prompts
|
||||||
|
|
||||||
|
# Metadata
|
||||||
|
if args.metadata:
|
||||||
|
for item in args.metadata:
|
||||||
|
if "=" in item:
|
||||||
|
kvstring = item.split("=")
|
||||||
|
result_json[kvstring[0].strip()] = kvstring[1].strip()
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid metadata format. Please use KEY=VALUE format."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Traffic
|
||||||
|
result_json["request_rate"] = (args.request_rate if args.request_rate
|
||||||
|
< float("inf") else "inf")
|
||||||
|
result_json["burstiness"] = args.burstiness
|
||||||
|
result_json["max_concurrency"] = args.max_concurrency
|
||||||
|
|
||||||
|
# Merge with benchmark result
|
||||||
|
result_json = {**result_json, **benchmark_result}
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
base_model_id = model_id.split("/")[-1]
|
||||||
|
max_concurrency_str = (f"-concurrency{args.max_concurrency}"
|
||||||
|
if args.max_concurrency is not None else "")
|
||||||
|
label = label or endpoint_type
|
||||||
|
file_name = f"{label}-{args.request_rate}qps{max_concurrency_str}-{base_model_id}-{current_dt}.json" #noqa
|
||||||
|
if args.result_filename:
|
||||||
|
file_name = args.result_filename
|
||||||
|
if args.result_dir:
|
||||||
|
file_name = os.path.join(args.result_dir, file_name)
|
||||||
|
with open(file_name, "w", encoding='utf-8') as outfile:
|
||||||
|
json.dump(result_json, outfile)
|
||||||
|
save_to_pytorch_benchmark_format(args, result_json, file_name)
|
||||||
69
vllm/benchmarks/utils.py
Normal file
69
vllm/benchmarks/utils.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_pytorch_benchmark_format(args: argparse.Namespace,
|
||||||
|
metrics: dict[str, list],
|
||||||
|
extra_info: dict[str, Any]) -> list:
|
||||||
|
"""
|
||||||
|
Save the benchmark results in the format used by PyTorch OSS benchmark with
|
||||||
|
on metric per record
|
||||||
|
https://github.com/pytorch/pytorch/wiki/How-to-integrate-with-PyTorch-OSS-benchmark-database
|
||||||
|
"""
|
||||||
|
records = []
|
||||||
|
if not os.environ.get("SAVE_TO_PYTORCH_BENCHMARK_FORMAT", False):
|
||||||
|
return records
|
||||||
|
|
||||||
|
for name, benchmark_values in metrics.items():
|
||||||
|
record = {
|
||||||
|
"benchmark": {
|
||||||
|
"name": "vLLM benchmark",
|
||||||
|
"extra_info": {
|
||||||
|
"args": vars(args),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": args.model,
|
||||||
|
},
|
||||||
|
"metric": {
|
||||||
|
"name": name,
|
||||||
|
"benchmark_values": benchmark_values,
|
||||||
|
"extra_info": extra_info,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tp = record["benchmark"]["extra_info"]["args"].get(
|
||||||
|
"tensor_parallel_size")
|
||||||
|
# Save tensor_parallel_size parameter if it's part of the metadata
|
||||||
|
if not tp and "tensor_parallel_size" in extra_info:
|
||||||
|
record["benchmark"]["extra_info"]["args"][
|
||||||
|
"tensor_parallel_size"] = extra_info["tensor_parallel_size"]
|
||||||
|
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
class InfEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
|
def clear_inf(self, o: Any):
|
||||||
|
if isinstance(o, dict):
|
||||||
|
return {k: self.clear_inf(v) for k, v in o.items()}
|
||||||
|
elif isinstance(o, list):
|
||||||
|
return [self.clear_inf(v) for v in o]
|
||||||
|
elif isinstance(o, float) and math.isinf(o):
|
||||||
|
return "inf"
|
||||||
|
return o
|
||||||
|
|
||||||
|
def iterencode(self, o: Any, *args, **kwargs) -> Any:
|
||||||
|
return super().iterencode(self.clear_inf(o), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def write_to_json(filename: str, records: list) -> None:
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
json.dump(records, f, cls=InfEncoder)
|
||||||
0
vllm/compilation/__init__.py
Normal file
0
vllm/compilation/__init__.py
Normal file
711
vllm/compilation/backends.py
Normal file
711
vllm/compilation/backends.py
Normal file
@@ -0,0 +1,711 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import dataclasses
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
import time
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.fx as fx
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.config import CompilationConfig, VllmConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.utils import weak_ref_tensors
|
||||||
|
|
||||||
|
from .compiler_interface import EagerAdaptor, InductorAdaptor
|
||||||
|
from .counter import compilation_counter
|
||||||
|
from .inductor_pass import InductorPass
|
||||||
|
from .monitor import end_monitoring_torch_compile
|
||||||
|
from .pass_manager import PostGradPassManager
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CompilerManager:
|
||||||
|
"""
|
||||||
|
A manager to manage the compilation process, including
|
||||||
|
caching the compiled graph, loading the compiled graph,
|
||||||
|
and compiling the graph.
|
||||||
|
|
||||||
|
The cache is a dict mapping
|
||||||
|
`(runtime_shape, graph_index, backend_name)`
|
||||||
|
to `any_data` returned from the compiler.
|
||||||
|
|
||||||
|
When serializing the cache, we save it to a Python file
|
||||||
|
for readability. We don't use json here because json doesn't
|
||||||
|
support int as key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, use_inductor: bool):
|
||||||
|
self.cache: Dict[Tuple[Optional[int], int, str], Any] = dict()
|
||||||
|
cls = InductorAdaptor if use_inductor else EagerAdaptor
|
||||||
|
self.compiler = cls()
|
||||||
|
|
||||||
|
def compute_hash(self, vllm_config: VllmConfig) -> str:
|
||||||
|
return self.compiler.compute_hash(vllm_config)
|
||||||
|
|
||||||
|
def initialize_cache(self, cache_dir: str, disable_cache: bool = False):
|
||||||
|
self.disable_cache = disable_cache
|
||||||
|
self.cache_dir = cache_dir
|
||||||
|
self.cache_file_path = os.path.join(cache_dir, "vllm_compile_cache.py")
|
||||||
|
|
||||||
|
if not disable_cache and os.path.exists(self.cache_file_path):
|
||||||
|
# load the cache from the file
|
||||||
|
with open(self.cache_file_path) as f:
|
||||||
|
# we use ast.literal_eval to parse the data
|
||||||
|
# because it is a safe way to parse Python literals.
|
||||||
|
# do not use eval(), it is unsafe.
|
||||||
|
self.cache = ast.literal_eval(f.read())
|
||||||
|
|
||||||
|
self.compiler.initialize_cache(cache_dir=cache_dir,
|
||||||
|
disable_cache=disable_cache)
|
||||||
|
|
||||||
|
def save_to_file(self):
|
||||||
|
if self.disable_cache:
|
||||||
|
return
|
||||||
|
with open(self.cache_file_path, "w") as f:
|
||||||
|
printer = pprint.PrettyPrinter(indent=4)
|
||||||
|
data = printer.pformat(self.cache)
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
def load(self,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
graph_index: int,
|
||||||
|
runtime_shape: Optional[int] = None) -> Optional[Callable]:
|
||||||
|
if (runtime_shape, graph_index, self.compiler.name) not in self.cache:
|
||||||
|
return None
|
||||||
|
handle = self.cache[(runtime_shape, graph_index, self.compiler.name)]
|
||||||
|
compiled_graph = self.compiler.load(handle, graph, example_inputs,
|
||||||
|
graph_index, runtime_shape)
|
||||||
|
logger.debug(
|
||||||
|
"Directly load the %s-th graph for shape %s from %s via "
|
||||||
|
"handle %s", graph_index, str(runtime_shape), self.compiler.name,
|
||||||
|
handle)
|
||||||
|
return compiled_graph
|
||||||
|
|
||||||
|
def compile(self,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs,
|
||||||
|
additional_inductor_config,
|
||||||
|
compilation_config: CompilationConfig,
|
||||||
|
graph_index: int = 0,
|
||||||
|
num_graphs: int = 1,
|
||||||
|
runtime_shape: Optional[int] = None) -> Any:
|
||||||
|
if graph_index == 0:
|
||||||
|
# before compiling the first graph, record the start time
|
||||||
|
global compilation_start_time
|
||||||
|
compilation_start_time = time.time()
|
||||||
|
|
||||||
|
compilation_counter.num_backend_compilations += 1
|
||||||
|
|
||||||
|
compiled_graph = None
|
||||||
|
|
||||||
|
# try to load from the cache
|
||||||
|
compiled_graph = self.load(graph, example_inputs, graph_index,
|
||||||
|
runtime_shape)
|
||||||
|
if compiled_graph is not None:
|
||||||
|
if graph_index == 0:
|
||||||
|
# adds some info logging for the first graph
|
||||||
|
logger.info("Directly load the compiled graph for shape %s "
|
||||||
|
"from the cache", str(runtime_shape)) # noqa
|
||||||
|
return compiled_graph
|
||||||
|
|
||||||
|
# no compiler cached the graph, or the cache is disabled,
|
||||||
|
# we need to compile it
|
||||||
|
compiled_graph, handle = self.compiler.compile(
|
||||||
|
graph, example_inputs, additional_inductor_config, runtime_shape)
|
||||||
|
|
||||||
|
assert compiled_graph is not None, "Failed to compile the graph"
|
||||||
|
|
||||||
|
# store the artifact in the cache
|
||||||
|
if handle is not None:
|
||||||
|
self.cache[(runtime_shape, graph_index,
|
||||||
|
self.compiler.name)] = handle
|
||||||
|
if graph_index == 0:
|
||||||
|
# adds some info logging for the first graph
|
||||||
|
logger.info("Cache the graph of shape %s for later use",
|
||||||
|
str(runtime_shape))
|
||||||
|
logger.debug(
|
||||||
|
"store the %s-th graph for shape %s from %s via handle %s",
|
||||||
|
graph_index, str(runtime_shape), self.compiler.name, handle)
|
||||||
|
|
||||||
|
# after compiling the last graph, record the end time
|
||||||
|
if graph_index == num_graphs - 1:
|
||||||
|
now = time.time()
|
||||||
|
elapsed = now - compilation_start_time
|
||||||
|
compilation_config.compilation_time += elapsed
|
||||||
|
if runtime_shape is None:
|
||||||
|
logger.info("Compiling a graph for general shape takes %.2f s",
|
||||||
|
elapsed)
|
||||||
|
else:
|
||||||
|
logger.info("Compiling a graph for shape %s takes %.2f s",
|
||||||
|
runtime_shape, elapsed)
|
||||||
|
|
||||||
|
return compiled_graph
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class SplitItem:
|
||||||
|
submod_name: str
|
||||||
|
graph_id: int
|
||||||
|
is_splitting_graph: bool
|
||||||
|
graph: fx.GraphModule
|
||||||
|
|
||||||
|
|
||||||
|
def split_graph(graph: fx.GraphModule,
|
||||||
|
ops: List[str]) -> Tuple[fx.GraphModule, List[SplitItem]]:
|
||||||
|
# split graph by ops
|
||||||
|
subgraph_id = 0
|
||||||
|
node_to_subgraph_id = {}
|
||||||
|
split_op_graphs = []
|
||||||
|
for node in graph.graph.nodes:
|
||||||
|
if node.op in ("output", "placeholder"):
|
||||||
|
continue
|
||||||
|
if node.op == 'call_function' and str(node.target) in ops:
|
||||||
|
subgraph_id += 1
|
||||||
|
node_to_subgraph_id[node] = subgraph_id
|
||||||
|
split_op_graphs.append(subgraph_id)
|
||||||
|
subgraph_id += 1
|
||||||
|
else:
|
||||||
|
node_to_subgraph_id[node] = subgraph_id
|
||||||
|
|
||||||
|
# `keep_original_order` is important!
|
||||||
|
# otherwise pytorch might reorder the nodes and
|
||||||
|
# the semantics of the graph will change when we
|
||||||
|
# have mutations in the graph
|
||||||
|
split_gm = torch.fx.passes.split_module.split_module(
|
||||||
|
graph,
|
||||||
|
None,
|
||||||
|
lambda node: node_to_subgraph_id[node],
|
||||||
|
keep_original_order=True)
|
||||||
|
|
||||||
|
outputs = []
|
||||||
|
|
||||||
|
names = [name for (name, module) in split_gm.named_modules()]
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
if "." in name or name == "":
|
||||||
|
# recursive child module or the root module
|
||||||
|
continue
|
||||||
|
|
||||||
|
module = getattr(split_gm, name)
|
||||||
|
|
||||||
|
graph_id = int(name.replace("submod_", ""))
|
||||||
|
outputs.append(
|
||||||
|
SplitItem(name, graph_id, (graph_id in split_op_graphs), module))
|
||||||
|
|
||||||
|
# sort by intetger graph_id, rather than string name
|
||||||
|
outputs.sort(key=lambda x: x.graph_id)
|
||||||
|
|
||||||
|
return split_gm, outputs
|
||||||
|
|
||||||
|
|
||||||
|
# we share the global graph pool among all the backends
|
||||||
|
global_graph_pool = None
|
||||||
|
|
||||||
|
compilation_start_time = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
class PiecewiseCompileInterpreter(torch.fx.Interpreter):
|
||||||
|
"""Code adapted from `torch.fx.passes.shape_prop.ShapeProp`.
|
||||||
|
It runs the given graph with fake inputs, and compile some
|
||||||
|
submodules specified by `compile_submod_names` with the given
|
||||||
|
compilation configs.
|
||||||
|
|
||||||
|
NOTE: the order in `compile_submod_names` matters, because
|
||||||
|
it will be used to determine the order of the compiled piecewise
|
||||||
|
graphs. The first graph will handle logging, and the last graph
|
||||||
|
has some special cudagraph output handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, module: torch.fx.GraphModule,
|
||||||
|
compile_submod_names: List[str], vllm_config: VllmConfig,
|
||||||
|
graph_pool, vllm_backend: "VllmBackend"):
|
||||||
|
super().__init__(module)
|
||||||
|
from torch._guards import detect_fake_mode
|
||||||
|
self.fake_mode = detect_fake_mode()
|
||||||
|
self.compile_submod_names = compile_submod_names
|
||||||
|
self.compilation_config = vllm_config.compilation_config
|
||||||
|
self.graph_pool = graph_pool
|
||||||
|
self.vllm_config = vllm_config
|
||||||
|
self.vllm_backend = vllm_backend
|
||||||
|
|
||||||
|
def run(self, *args):
|
||||||
|
fake_args = [
|
||||||
|
self.fake_mode.from_tensor(t) if isinstance(t, torch.Tensor) else t
|
||||||
|
for t in args
|
||||||
|
]
|
||||||
|
with self.fake_mode:
|
||||||
|
return super().run(*fake_args)
|
||||||
|
|
||||||
|
def call_module(self, target: torch.fx.node.Target,
|
||||||
|
args: Tuple[torch.fx.node.Argument,
|
||||||
|
...], kwargs: Dict[str, Any]) -> Any:
|
||||||
|
assert isinstance(target, str)
|
||||||
|
output = super().call_module(target, args, kwargs)
|
||||||
|
|
||||||
|
if target in self.compile_submod_names:
|
||||||
|
index = self.compile_submod_names.index(target)
|
||||||
|
submod = self.fetch_attr(target)
|
||||||
|
sym_shape_indices = [
|
||||||
|
i for i, x in enumerate(args) if isinstance(x, torch.SymInt)
|
||||||
|
]
|
||||||
|
global compilation_start_time
|
||||||
|
compiled_graph_for_general_shape = self.vllm_backend.\
|
||||||
|
compiler_manager.compile(
|
||||||
|
submod,
|
||||||
|
args,
|
||||||
|
self.compilation_config.inductor_compile_config,
|
||||||
|
self.compilation_config,
|
||||||
|
graph_index=index,
|
||||||
|
num_graphs=len(self.compile_submod_names),
|
||||||
|
runtime_shape=None)
|
||||||
|
|
||||||
|
self.module.__dict__[target] = PiecewiseBackend(
|
||||||
|
submod, self.vllm_config, self.graph_pool, index,
|
||||||
|
len(self.compile_submod_names), sym_shape_indices,
|
||||||
|
compiled_graph_for_general_shape, self.vllm_backend)
|
||||||
|
|
||||||
|
compilation_counter.num_piecewise_capturable_graphs_seen += 1
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class VllmBackend:
|
||||||
|
"""The compilation backend for `torch.compile` with vLLM.
|
||||||
|
It is used for compilation level of `CompilationLevel.PIECEWISE`,
|
||||||
|
where we customize the compilation.
|
||||||
|
|
||||||
|
The major work of this backend is to split the graph into
|
||||||
|
piecewise graphs, and pass them to the piecewise backend.
|
||||||
|
|
||||||
|
This backend also adds the PostGradPassManager to Inductor config,
|
||||||
|
which handles the post-grad passes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
vllm_config: VllmConfig
|
||||||
|
compilation_config: CompilationConfig
|
||||||
|
graph_pool: Any
|
||||||
|
_called: bool = False
|
||||||
|
# the graph we compiled
|
||||||
|
graph: fx.GraphModule
|
||||||
|
# the stiching graph module for all the piecewise graphs
|
||||||
|
split_gm: fx.GraphModule
|
||||||
|
piecewise_graphs: List[SplitItem]
|
||||||
|
returned_callable: Callable
|
||||||
|
# Inductor passes to run on the graph pre-defunctionalization
|
||||||
|
post_grad_passes: Sequence[Callable]
|
||||||
|
sym_tensor_indices: List[int]
|
||||||
|
input_buffers: List[torch.Tensor]
|
||||||
|
compiler_manager: CompilerManager
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vllm_config: VllmConfig,
|
||||||
|
):
|
||||||
|
global global_graph_pool
|
||||||
|
if global_graph_pool is None:
|
||||||
|
global_graph_pool = torch.cuda.graph_pool_handle()
|
||||||
|
|
||||||
|
# TODO: in the future, if we want to use multiple
|
||||||
|
# streams, it might not be safe to share a global pool.
|
||||||
|
# only investigate this when we use multiple streams
|
||||||
|
self.graph_pool = global_graph_pool
|
||||||
|
|
||||||
|
# Passes to run on the graph post-grad.
|
||||||
|
self.post_grad_pass_manager = PostGradPassManager()
|
||||||
|
|
||||||
|
self.sym_tensor_indices = []
|
||||||
|
self.input_buffers = []
|
||||||
|
|
||||||
|
self.vllm_config = vllm_config
|
||||||
|
self.compilation_config = vllm_config.compilation_config
|
||||||
|
|
||||||
|
self.compiler_manager: CompilerManager = CompilerManager(
|
||||||
|
self.compilation_config.use_inductor)
|
||||||
|
|
||||||
|
# `torch.compile` is JIT compiled, so we don't need to
|
||||||
|
# do anything here
|
||||||
|
|
||||||
|
def configure_post_pass(self):
|
||||||
|
config = self.compilation_config
|
||||||
|
self.post_grad_pass_manager.configure(config.pass_config)
|
||||||
|
|
||||||
|
# Post-grad custom passes are run using the post_grad_custom_post_pass
|
||||||
|
# hook. If a pass for that hook exists, add it to the pass manager.
|
||||||
|
inductor_config = config.inductor_compile_config
|
||||||
|
PASS_KEY = "post_grad_custom_post_pass"
|
||||||
|
if PASS_KEY in inductor_config:
|
||||||
|
# Config should automatically wrap all inductor passes
|
||||||
|
assert isinstance(inductor_config[PASS_KEY], InductorPass)
|
||||||
|
self.post_grad_pass_manager.add(inductor_config[PASS_KEY])
|
||||||
|
inductor_config[PASS_KEY] = self.post_grad_pass_manager
|
||||||
|
|
||||||
|
def __call__(self, graph: fx.GraphModule, example_inputs) -> Callable:
|
||||||
|
|
||||||
|
vllm_config = self.vllm_config
|
||||||
|
if not self.compilation_config.cache_dir:
|
||||||
|
# no provided cache dir, generate one based on the known factors
|
||||||
|
# that affects the compilation. if none of the factors change,
|
||||||
|
# the cache dir will be the same so that we can reuse the compiled
|
||||||
|
# graph.
|
||||||
|
|
||||||
|
factors = []
|
||||||
|
# 0. factors come from the env, for example, The values of
|
||||||
|
# VLLM_PP_LAYER_PARTITION will affects the computation graph.
|
||||||
|
env_hash = envs.compute_hash()
|
||||||
|
factors.append(env_hash)
|
||||||
|
|
||||||
|
# 1. factors come from the vllm_config (it mainly summarizes how the
|
||||||
|
# model is created)
|
||||||
|
config_hash = vllm_config.compute_hash()
|
||||||
|
factors.append(config_hash)
|
||||||
|
|
||||||
|
# 2. factors come from the code files that are traced by Dynamo (
|
||||||
|
# it mainly summarizes how the model is used in forward pass)
|
||||||
|
forward_code_files = list(
|
||||||
|
sorted(self.compilation_config.traced_files))
|
||||||
|
self.compilation_config.traced_files.clear()
|
||||||
|
logger.debug(
|
||||||
|
"Traced files (to be considered for compilation cache):\n%s",
|
||||||
|
"\n".join(forward_code_files))
|
||||||
|
hash_content = []
|
||||||
|
for filepath in forward_code_files:
|
||||||
|
hash_content.append(filepath)
|
||||||
|
with open(filepath) as f:
|
||||||
|
hash_content.append(f.read())
|
||||||
|
import hashlib
|
||||||
|
code_hash = hashlib.md5("\n".join(hash_content).encode(),
|
||||||
|
usedforsecurity=False).hexdigest()
|
||||||
|
factors.append(code_hash)
|
||||||
|
|
||||||
|
# 3. compiler hash
|
||||||
|
compiler_hash = self.compiler_manager.compute_hash(vllm_config)
|
||||||
|
factors.append(compiler_hash)
|
||||||
|
|
||||||
|
# combine all factors to generate the cache dir
|
||||||
|
hash_key = hashlib.md5(str(factors).encode(),
|
||||||
|
usedforsecurity=False).hexdigest()[:10]
|
||||||
|
|
||||||
|
cache_dir = os.path.join(
|
||||||
|
envs.VLLM_CACHE_ROOT,
|
||||||
|
"torch_compile_cache",
|
||||||
|
hash_key,
|
||||||
|
)
|
||||||
|
self.compilation_config.cache_dir = cache_dir
|
||||||
|
|
||||||
|
cache_dir = self.compilation_config.cache_dir
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
rank = vllm_config.parallel_config.rank
|
||||||
|
dp_rank = vllm_config.parallel_config.data_parallel_rank
|
||||||
|
local_cache_dir = os.path.join(cache_dir, f"rank_{rank}_{dp_rank}")
|
||||||
|
os.makedirs(local_cache_dir, exist_ok=True)
|
||||||
|
self.compilation_config.local_cache_dir = local_cache_dir
|
||||||
|
|
||||||
|
disable_cache = envs.VLLM_DISABLE_COMPILE_CACHE
|
||||||
|
|
||||||
|
if disable_cache:
|
||||||
|
logger.info("vLLM's torch.compile cache is disabled.")
|
||||||
|
else:
|
||||||
|
logger.info("Using cache directory: %s for vLLM's torch.compile",
|
||||||
|
local_cache_dir)
|
||||||
|
|
||||||
|
self.compiler_manager.initialize_cache(local_cache_dir, disable_cache)
|
||||||
|
|
||||||
|
# when dynamo calls the backend, it means the bytecode
|
||||||
|
# transform and analysis are done
|
||||||
|
compilation_counter.num_graphs_seen += 1
|
||||||
|
from .monitor import torch_compile_start_time
|
||||||
|
dynamo_time = time.time() - torch_compile_start_time
|
||||||
|
logger.info("Dynamo bytecode transform time: %.2f s", dynamo_time)
|
||||||
|
self.compilation_config.compilation_time += dynamo_time
|
||||||
|
|
||||||
|
# we control the compilation process, each instance can only be
|
||||||
|
# called once
|
||||||
|
assert not self._called, "VllmBackend can only be called once"
|
||||||
|
|
||||||
|
self.graph = graph
|
||||||
|
self.configure_post_pass()
|
||||||
|
|
||||||
|
self.split_gm, self.piecewise_graphs = split_graph(
|
||||||
|
graph, self.compilation_config.splitting_ops)
|
||||||
|
|
||||||
|
from torch._dynamo.utils import lazy_format_graph_code
|
||||||
|
|
||||||
|
# depyf will hook lazy_format_graph_code and dump the graph
|
||||||
|
# for debugging, no need to print the graph here
|
||||||
|
lazy_format_graph_code("before split", self.graph)
|
||||||
|
lazy_format_graph_code("after split", self.split_gm)
|
||||||
|
|
||||||
|
compilation_counter.num_piecewise_graphs_seen += len(
|
||||||
|
self.piecewise_graphs)
|
||||||
|
submod_names_to_compile = [
|
||||||
|
item.submod_name for item in self.piecewise_graphs
|
||||||
|
if not item.is_splitting_graph
|
||||||
|
]
|
||||||
|
|
||||||
|
# propagate the split graph to the piecewise backend,
|
||||||
|
# compile submodules with symbolic shapes
|
||||||
|
PiecewiseCompileInterpreter(self.split_gm, submod_names_to_compile,
|
||||||
|
self.vllm_config, self.graph_pool,
|
||||||
|
self).run(*example_inputs)
|
||||||
|
|
||||||
|
graph_path = os.path.join(local_cache_dir, "computation_graph.py")
|
||||||
|
if not os.path.exists(graph_path):
|
||||||
|
# code adapted from https://github.com/thuml/depyf/blob/dab831108a752d1facc00acdd6d4243891845c37/depyf/explain/patched_lazy_format_graph_code.py#L30 # noqa
|
||||||
|
# use `print_readable` because it can include submodules
|
||||||
|
src = "from __future__ import annotations\nimport torch\n" + \
|
||||||
|
self.split_gm.print_readable(print_output=False)
|
||||||
|
src = src.replace("<lambda>", "GraphModule")
|
||||||
|
with open(graph_path, "w") as f:
|
||||||
|
f.write(src)
|
||||||
|
|
||||||
|
logger.debug("Computation graph saved to %s", graph_path)
|
||||||
|
|
||||||
|
self._called = True
|
||||||
|
|
||||||
|
if not self.compilation_config.use_cudagraph or \
|
||||||
|
not self.compilation_config.cudagraph_copy_inputs:
|
||||||
|
return self.split_gm
|
||||||
|
|
||||||
|
# if we need to copy input buffers for cudagraph
|
||||||
|
from torch._guards import detect_fake_mode
|
||||||
|
fake_mode = detect_fake_mode()
|
||||||
|
fake_args = [
|
||||||
|
fake_mode.from_tensor(t) if isinstance(t, torch.Tensor) else t
|
||||||
|
for t in example_inputs
|
||||||
|
]
|
||||||
|
|
||||||
|
# index of tensors that have symbolic shapes (batch size)
|
||||||
|
# for weights and static buffers, they will have concrete shapes.
|
||||||
|
# symbolic shape only happens for input tensors.
|
||||||
|
from torch.fx.experimental.symbolic_shapes import is_symbolic
|
||||||
|
self.sym_tensor_indices = [
|
||||||
|
i for i, x in enumerate(fake_args)
|
||||||
|
if isinstance(x, torch._subclasses.fake_tensor.FakeTensor) and \
|
||||||
|
any(is_symbolic(d) for d in x.size())
|
||||||
|
]
|
||||||
|
|
||||||
|
# compiler managed cudagraph input buffers
|
||||||
|
# we assume the first run with symbolic shapes
|
||||||
|
# has the maximum size among all the tensors
|
||||||
|
self.input_buffers = [
|
||||||
|
example_inputs[x].clone() for x in self.sym_tensor_indices
|
||||||
|
]
|
||||||
|
|
||||||
|
# this is the callable we return to Dynamo to run
|
||||||
|
def copy_and_call(*args):
|
||||||
|
list_args = list(args)
|
||||||
|
for i, index in enumerate(self.sym_tensor_indices):
|
||||||
|
runtime_tensor = list_args[index]
|
||||||
|
runtime_shape = runtime_tensor.shape[0]
|
||||||
|
static_tensor = self.input_buffers[i][:runtime_shape]
|
||||||
|
|
||||||
|
# copy the tensor to the static buffer
|
||||||
|
static_tensor.copy_(runtime_tensor)
|
||||||
|
|
||||||
|
# replace the tensor in the list_args to the static buffer
|
||||||
|
list_args[index] = static_tensor
|
||||||
|
return self.split_gm(*list_args)
|
||||||
|
|
||||||
|
return copy_and_call
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ConcreteSizeEntry:
|
||||||
|
runtime_shape: int
|
||||||
|
need_to_compile: bool # the size is in compile_sizes
|
||||||
|
use_cudagraph: bool # the size is in cudagraph_capture_sizes
|
||||||
|
|
||||||
|
compiled: bool = False
|
||||||
|
runnable: Callable = None # type: ignore
|
||||||
|
num_finished_warmup: int = 0
|
||||||
|
cudagraph: Optional[torch.cuda.CUDAGraph] = None
|
||||||
|
output: Optional[Any] = None
|
||||||
|
|
||||||
|
# for cudagraph debugging, track the input addresses
|
||||||
|
# during capture, and check if they are the same during replay
|
||||||
|
input_addresses: Optional[List[int]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PiecewiseBackend:
|
||||||
|
|
||||||
|
def __init__(self, graph: fx.GraphModule, vllm_config: VllmConfig,
|
||||||
|
graph_pool: Any, piecewise_compile_index: int,
|
||||||
|
total_piecewise_compiles: int, sym_shape_indices: List[int],
|
||||||
|
compiled_graph_for_general_shape: Callable,
|
||||||
|
vllm_backend: VllmBackend):
|
||||||
|
"""
|
||||||
|
The backend for piecewise compilation.
|
||||||
|
It mainly handles the compilation and cudagraph capturing.
|
||||||
|
|
||||||
|
We will compile `self.graph` once for the general shape,
|
||||||
|
and then compile for different shapes specified in
|
||||||
|
`compilation_config.compile_sizes`.
|
||||||
|
|
||||||
|
Independently, we will capture cudagraph for different shapes.
|
||||||
|
|
||||||
|
If a shape needs both compilation and cudagraph, we will
|
||||||
|
compile it first, and then capture cudagraph.
|
||||||
|
"""
|
||||||
|
self.graph = graph
|
||||||
|
self.vllm_config = vllm_config
|
||||||
|
self.compilation_config = vllm_config.compilation_config
|
||||||
|
self.graph_pool = graph_pool
|
||||||
|
self.piecewise_compile_index = piecewise_compile_index
|
||||||
|
self.total_piecewise_compiles = total_piecewise_compiles
|
||||||
|
self.vllm_backend = vllm_backend
|
||||||
|
|
||||||
|
self.is_first_graph = piecewise_compile_index == 0
|
||||||
|
self.is_last_graph = (
|
||||||
|
piecewise_compile_index == total_piecewise_compiles - 1)
|
||||||
|
|
||||||
|
self.compile_sizes: Set[int] = set(
|
||||||
|
self.compilation_config.compile_sizes)
|
||||||
|
self.cudagraph_capture_sizes: Set[int] = set(
|
||||||
|
self.compilation_config.cudagraph_capture_sizes
|
||||||
|
) if self.compilation_config.use_cudagraph else set()
|
||||||
|
|
||||||
|
self.first_run_finished = False
|
||||||
|
|
||||||
|
self.compiled_graph_for_general_shape = compiled_graph_for_general_shape # noqa
|
||||||
|
|
||||||
|
self.sym_shape_indices = sym_shape_indices
|
||||||
|
|
||||||
|
self.is_debugging_mode = envs.VLLM_LOGGING_LEVEL == "DEBUG"
|
||||||
|
|
||||||
|
# the entries for different shapes that we need to either
|
||||||
|
# compile or capture cudagraph
|
||||||
|
self.concrete_size_entries: Dict[int, ConcreteSizeEntry] = {}
|
||||||
|
|
||||||
|
# to_be_compiled_sizes tracks the remaining sizes to compile,
|
||||||
|
# and updates during the compilation process, so we need to copy it
|
||||||
|
self.to_be_compiled_sizes: Set[int] = self.compile_sizes.copy()
|
||||||
|
for shape in self.compile_sizes.union(self.cudagraph_capture_sizes):
|
||||||
|
self.concrete_size_entries[shape] = ConcreteSizeEntry(
|
||||||
|
runtime_shape=shape,
|
||||||
|
need_to_compile=shape in self.compile_sizes,
|
||||||
|
use_cudagraph=shape in self.cudagraph_capture_sizes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_for_ending_compilation(self):
|
||||||
|
if self.is_last_graph and not self.to_be_compiled_sizes:
|
||||||
|
# no specific sizes to compile
|
||||||
|
# save the hash of the inductor graph for the next run
|
||||||
|
self.vllm_backend.compiler_manager.save_to_file()
|
||||||
|
end_monitoring_torch_compile(self.vllm_config)
|
||||||
|
|
||||||
|
def __call__(self, *args) -> Any:
|
||||||
|
if not self.first_run_finished:
|
||||||
|
self.first_run_finished = True
|
||||||
|
self.check_for_ending_compilation()
|
||||||
|
return self.compiled_graph_for_general_shape(*args)
|
||||||
|
|
||||||
|
runtime_shape = args[self.sym_shape_indices[0]]
|
||||||
|
if runtime_shape not in self.concrete_size_entries:
|
||||||
|
# we don't need to do anything for this shape
|
||||||
|
return self.compiled_graph_for_general_shape(*args)
|
||||||
|
|
||||||
|
entry = self.concrete_size_entries[runtime_shape]
|
||||||
|
|
||||||
|
if entry.runnable is None:
|
||||||
|
entry.runnable = self.compiled_graph_for_general_shape
|
||||||
|
|
||||||
|
if entry.need_to_compile and not entry.compiled:
|
||||||
|
entry.compiled = True
|
||||||
|
self.to_be_compiled_sizes.remove(runtime_shape)
|
||||||
|
# args are real arguments
|
||||||
|
entry.runnable = self.vllm_backend.compiler_manager.compile(
|
||||||
|
self.graph,
|
||||||
|
args,
|
||||||
|
self.compilation_config.inductor_compile_config,
|
||||||
|
self.compilation_config,
|
||||||
|
graph_index=self.piecewise_compile_index,
|
||||||
|
num_graphs=self.total_piecewise_compiles,
|
||||||
|
runtime_shape=runtime_shape)
|
||||||
|
|
||||||
|
# finished compilations for all required shapes
|
||||||
|
if self.is_last_graph and not self.to_be_compiled_sizes:
|
||||||
|
self.check_for_ending_compilation()
|
||||||
|
|
||||||
|
if not entry.use_cudagraph:
|
||||||
|
return entry.runnable(*args)
|
||||||
|
|
||||||
|
if entry.cudagraph is None:
|
||||||
|
if entry.num_finished_warmup < self.compilation_config.cudagraph_num_of_warmups: # noqa
|
||||||
|
entry.num_finished_warmup += 1
|
||||||
|
if self.is_first_graph:
|
||||||
|
logger.debug(
|
||||||
|
"Warming up %s/%s for shape %s",
|
||||||
|
entry.num_finished_warmup,
|
||||||
|
self.compilation_config.cudagraph_num_of_warmups,
|
||||||
|
runtime_shape)
|
||||||
|
return entry.runnable(*args)
|
||||||
|
|
||||||
|
if self.is_first_graph:
|
||||||
|
# Since we capture cudagraph for many different shapes and
|
||||||
|
# capturing is fast, we don't need to log it for every shape.
|
||||||
|
# We only log it in the debug mode.
|
||||||
|
logger.debug("Capturing a cudagraph for shape %s",
|
||||||
|
runtime_shape)
|
||||||
|
|
||||||
|
input_addresses = [
|
||||||
|
x.data_ptr() for x in args if isinstance(x, torch.Tensor)
|
||||||
|
]
|
||||||
|
entry.input_addresses = input_addresses
|
||||||
|
cudagraph = torch.cuda.CUDAGraph()
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
if not self.is_first_graph:
|
||||||
|
# during every model forward, we will capture
|
||||||
|
# many pieces of cudagraphs (roughly one per layer).
|
||||||
|
# running gc again and again across layers will
|
||||||
|
# make the cudagraph capture very slow.
|
||||||
|
# therefore, we only run gc for the first graph,
|
||||||
|
# and disable gc for the rest of the graphs.
|
||||||
|
stack.enter_context(patch("gc.collect", lambda: None))
|
||||||
|
stack.enter_context(
|
||||||
|
patch("torch.cuda.empty_cache", lambda: None))
|
||||||
|
|
||||||
|
# mind-exploding: carefully manage the reference and memory.
|
||||||
|
with torch.cuda.graph(cudagraph, pool=self.graph_pool):
|
||||||
|
# `output` is managed by pytorch's cudagraph pool
|
||||||
|
output = entry.runnable(*args)
|
||||||
|
if self.is_last_graph:
|
||||||
|
# by converting it to weak ref,
|
||||||
|
# the original `output` will immediately be released
|
||||||
|
# to save memory. It is only safe to do this for
|
||||||
|
# the last graph, because the output of the last graph
|
||||||
|
# will not be used by any other cuda graph.
|
||||||
|
output = weak_ref_tensors(output)
|
||||||
|
|
||||||
|
# here we always use weak ref for the output
|
||||||
|
# to save memory
|
||||||
|
entry.output = weak_ref_tensors(output)
|
||||||
|
entry.cudagraph = cudagraph
|
||||||
|
|
||||||
|
compilation_counter.num_cudagraph_caputured += 1
|
||||||
|
|
||||||
|
# important: we need to return the output, rather than
|
||||||
|
# the weak ref of the output, so that pytorch can correctly
|
||||||
|
# manage the memory during cuda graph capture
|
||||||
|
return output
|
||||||
|
|
||||||
|
if self.is_debugging_mode:
|
||||||
|
# check if the input addresses are the same
|
||||||
|
new_input_addresses = [
|
||||||
|
x.data_ptr() for x in args if isinstance(x, torch.Tensor)
|
||||||
|
]
|
||||||
|
assert new_input_addresses == entry.input_addresses, (
|
||||||
|
"Input addresses for cudagraphs are different during replay."
|
||||||
|
f" Expected {entry.input_addresses}, got {new_input_addresses}"
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.cudagraph.replay()
|
||||||
|
return entry.output
|
||||||
401
vllm/compilation/compiler_interface.py
Normal file
401
vllm/compilation/compiler_interface.py
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
import contextlib
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import importlib.metadata
|
||||||
|
import os
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch._inductor.compile_fx
|
||||||
|
import torch.fx as fx
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
|
from vllm.config import VllmConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CompilerInterface:
|
||||||
|
"""
|
||||||
|
The interface for a compiler that can be used by vLLM.
|
||||||
|
"""
|
||||||
|
# The name of the compiler, e.g. inductor.
|
||||||
|
# This is a class-level attribute.
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def initialize_cache(self, cache_dir: str, disable_cache: bool = False):
|
||||||
|
"""
|
||||||
|
when the vLLM process uses `cache_dir` as the cache directory,
|
||||||
|
the compiler should initialize itself with the cache directory,
|
||||||
|
e.g. by re-directing its own cache directory to a sub-directory.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def compute_hash(self, vllm_config: VllmConfig) -> str:
|
||||||
|
"""
|
||||||
|
Gather all the relevant information from the vLLM config,
|
||||||
|
to compute a hash so that we can cache the compiled model.
|
||||||
|
|
||||||
|
See :meth:`VllmConfig.compute_hash` to check what information
|
||||||
|
is already considered by default. This function should only
|
||||||
|
consider the information that is specific to the compiler.
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
compiler_config: Dict[str, Any],
|
||||||
|
runtime_shape: Optional[int] = None
|
||||||
|
) -> Tuple[Optional[Callable], Optional[Any]]:
|
||||||
|
"""
|
||||||
|
Compile the graph with the given example inputs and compiler config,
|
||||||
|
with a runtime shape. If the `runtime_shape` is None, it means
|
||||||
|
the `example_inputs` have a dynamic shape. Otherwise, the
|
||||||
|
`runtime_shape` specifies the shape of the inputs. Right now we only
|
||||||
|
support one variable shape for all inputs, which is the batchsize
|
||||||
|
(number of tokens) during inference.
|
||||||
|
|
||||||
|
Dynamo will make sure `graph(*example_inputs)` is valid.
|
||||||
|
|
||||||
|
The function should return a compiled callable function, as well as
|
||||||
|
a handle that can be used to directly load the compiled function.
|
||||||
|
|
||||||
|
The handle should be a plain Python object, preferably a string or a
|
||||||
|
file path for readability.
|
||||||
|
|
||||||
|
If the compiler doesn't support caching, it should return None for the
|
||||||
|
handle. If the compiler fails to compile the graph, it should return
|
||||||
|
None for the compiled function as well.
|
||||||
|
"""
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def load(self,
|
||||||
|
handle: Any,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
graph_index: int,
|
||||||
|
runtime_shape: Optional[int] = None) -> Callable:
|
||||||
|
"""
|
||||||
|
Load the compiled function from the handle.
|
||||||
|
Raises an error if the handle is invalid.
|
||||||
|
|
||||||
|
The handle is the second return value of the `compile` function.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("caching is not supported")
|
||||||
|
|
||||||
|
|
||||||
|
class AlwaysHitShapeEnv:
|
||||||
|
"""
|
||||||
|
Why do we need this class:
|
||||||
|
|
||||||
|
For normal `torch.compile` usage, every compilation will have
|
||||||
|
one Dynamo bytecode compilation and one Inductor compilation.
|
||||||
|
The Inductor compilation happens under the context of the
|
||||||
|
Dynamo bytecode compilation, and that context is used to
|
||||||
|
determine the dynamic shape information, etc.
|
||||||
|
|
||||||
|
For our use case, we only run Dynamo bytecode compilation once,
|
||||||
|
and run Inductor compilation multiple times with different shapes
|
||||||
|
plus a general shape. The compilation for specific shapes happens
|
||||||
|
outside of the context of the Dynamo bytecode compilation. At that
|
||||||
|
time, we don't have shape environment to provide to Inductor, and
|
||||||
|
it will fail the Inductor code cache lookup.
|
||||||
|
|
||||||
|
By providing a dummy shape environment that always hits, we can
|
||||||
|
make the Inductor code cache lookup always hit, and we can
|
||||||
|
compile the graph for different shapes as needed.
|
||||||
|
|
||||||
|
The following dummy methods are obtained by trial-and-error
|
||||||
|
until it works.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.guards: List[Any] = []
|
||||||
|
|
||||||
|
def evaluate_guards_expression(self, *args, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_pruned_guards(self, *args, **kwargs):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def produce_guards_expression(self, *args, **kwargs):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class InductorAdaptor(CompilerInterface):
|
||||||
|
"""
|
||||||
|
The adaptor for the Inductor compiler, version 2.5 and 2.6.
|
||||||
|
"""
|
||||||
|
name = "inductor"
|
||||||
|
|
||||||
|
def compute_hash(self, vllm_config: VllmConfig) -> str:
|
||||||
|
factors: List[Any] = []
|
||||||
|
# summarize system state
|
||||||
|
from torch._inductor.codecache import CacheBase
|
||||||
|
system_factors = CacheBase.get_system()
|
||||||
|
factors.append(system_factors)
|
||||||
|
|
||||||
|
# summarize pytorch state
|
||||||
|
from torch._inductor.codecache import torch_key
|
||||||
|
torch_factors = torch_key()
|
||||||
|
factors.append(torch_factors)
|
||||||
|
hash_str = hashlib.md5(str(factors).encode(),
|
||||||
|
usedforsecurity=False).hexdigest()[:10]
|
||||||
|
return hash_str
|
||||||
|
|
||||||
|
def initialize_cache(self, cache_dir: str, disable_cache: bool = False):
|
||||||
|
self.cache_dir = cache_dir
|
||||||
|
if disable_cache:
|
||||||
|
return
|
||||||
|
# redirect the cache directory to a sub-directory
|
||||||
|
# set flags so that Inductor and Triton store their cache
|
||||||
|
# in the cache_dir, then users only need to copy the cache_dir
|
||||||
|
# to another machine to reuse the cache.
|
||||||
|
inductor_cache = os.path.join(cache_dir, "inductor_cache")
|
||||||
|
os.makedirs(inductor_cache, exist_ok=True)
|
||||||
|
os.environ["TORCHINDUCTOR_CACHE_DIR"] = inductor_cache
|
||||||
|
triton_cache = os.path.join(cache_dir, "triton_cache")
|
||||||
|
os.makedirs(triton_cache, exist_ok=True)
|
||||||
|
os.environ["TRITON_CACHE_DIR"] = triton_cache
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
compiler_config: Dict[str, Any],
|
||||||
|
runtime_shape: Optional[int] = None
|
||||||
|
) -> Tuple[Optional[Callable], Optional[Any]]:
|
||||||
|
from torch._inductor import config
|
||||||
|
current_config = config.get_config_copy()
|
||||||
|
from torch._inductor.compile_fx import compile_fx
|
||||||
|
|
||||||
|
# disable remote cache
|
||||||
|
current_config["fx_graph_cache"] = True
|
||||||
|
current_config["fx_graph_remote_cache"] = False
|
||||||
|
|
||||||
|
if compiler_config is not None:
|
||||||
|
current_config.update(compiler_config)
|
||||||
|
|
||||||
|
if isinstance(runtime_shape, int):
|
||||||
|
# for a specific batchsize, tuning triton kernel parameters
|
||||||
|
# can be beneficial
|
||||||
|
current_config["max_autotune"] = True
|
||||||
|
current_config["coordinate_descent_tuning"] = True
|
||||||
|
|
||||||
|
# inductor can inplace modify the graph, so we need to copy it
|
||||||
|
# see https://github.com/pytorch/pytorch/issues/138980
|
||||||
|
graph = copy.deepcopy(graph)
|
||||||
|
|
||||||
|
# it's the first time we compile this graph
|
||||||
|
# the assumption is that we don't have nested Inductor compilation.
|
||||||
|
# compiled_fx_graph_hash will only be called once, and we can hook
|
||||||
|
# it to get the hash of the compiled graph directly.
|
||||||
|
|
||||||
|
hash_str, file_path = None, None
|
||||||
|
from torch._inductor.codecache import (FxGraphCache,
|
||||||
|
compiled_fx_graph_hash)
|
||||||
|
|
||||||
|
if torch.__version__.startswith("2.5"):
|
||||||
|
original_load = FxGraphCache.load
|
||||||
|
original_load_name = "torch._inductor.codecache.FxGraphCache.load"
|
||||||
|
|
||||||
|
def hijack_load(*args, **kwargs):
|
||||||
|
inductor_compiled_graph = original_load(*args, **kwargs)
|
||||||
|
nonlocal file_path
|
||||||
|
compiled_fn = inductor_compiled_graph.current_callable
|
||||||
|
file_path = compiled_fn.__code__.co_filename # noqa
|
||||||
|
if not file_path.startswith(self.cache_dir):
|
||||||
|
# hooked in the align_inputs_from_check_idxs function
|
||||||
|
# in torch/_inductor/utils.py
|
||||||
|
for cell in compiled_fn.__closure__:
|
||||||
|
if not callable(cell.cell_contents):
|
||||||
|
continue
|
||||||
|
if cell.cell_contents.__code__.co_filename.startswith(
|
||||||
|
self.cache_dir):
|
||||||
|
# this is the real file path compiled from Inductor
|
||||||
|
file_path = cell.cell_contents.__code__.co_filename
|
||||||
|
break
|
||||||
|
return inductor_compiled_graph
|
||||||
|
|
||||||
|
hijacked_compile_fx_inner = torch._inductor.compile_fx.compile_fx_inner # noqa
|
||||||
|
elif torch.__version__ >= "2.6":
|
||||||
|
# function renamed in 2.6
|
||||||
|
original_load_name = None
|
||||||
|
|
||||||
|
def hijacked_compile_fx_inner(*args, **kwargs):
|
||||||
|
output = torch._inductor.compile_fx.compile_fx_inner(
|
||||||
|
*args, **kwargs)
|
||||||
|
nonlocal hash_str
|
||||||
|
inductor_compiled_graph = output
|
||||||
|
if inductor_compiled_graph is not None:
|
||||||
|
nonlocal file_path
|
||||||
|
compiled_fn = inductor_compiled_graph.current_callable
|
||||||
|
file_path = compiled_fn.__code__.co_filename # noqa
|
||||||
|
if not file_path.startswith(self.cache_dir):
|
||||||
|
# hooked in the align_inputs_from_check_idxs function
|
||||||
|
# in torch/_inductor/utils.py
|
||||||
|
for cell in compiled_fn.__closure__:
|
||||||
|
if not callable(cell.cell_contents):
|
||||||
|
continue
|
||||||
|
code = cell.cell_contents.__code__
|
||||||
|
if code.co_filename.startswith(self.cache_dir):
|
||||||
|
# this is the real file path
|
||||||
|
# compiled from Inductor
|
||||||
|
file_path = code.co_filename
|
||||||
|
break
|
||||||
|
hash_str = inductor_compiled_graph._fx_graph_cache_key
|
||||||
|
return output
|
||||||
|
|
||||||
|
def hijack_compiled_fx_graph_hash(*args, **kwargs):
|
||||||
|
out = compiled_fx_graph_hash(*args, **kwargs)
|
||||||
|
nonlocal hash_str
|
||||||
|
hash_str = out[0]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _check_can_cache(*args, **kwargs):
|
||||||
|
# no error means it can be cached.
|
||||||
|
# Inductor refuses to cache the graph outside of Dynamo
|
||||||
|
# tracing context, and also disables caching for graphs
|
||||||
|
# with high-order ops.
|
||||||
|
# For vLLM, in either case, we want to cache the graph.
|
||||||
|
# see https://github.com/pytorch/pytorch/blob/9f5ebf3fc609105a74eab4ccc24932d6353ff566/torch/_inductor/codecache.py#L1221 # noqa
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_shape_env() -> AlwaysHitShapeEnv:
|
||||||
|
return AlwaysHitShapeEnv()
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
# hijack to get the compiled graph itself
|
||||||
|
if original_load_name is not None:
|
||||||
|
stack.enter_context(patch(original_load_name, hijack_load))
|
||||||
|
|
||||||
|
# for hijacking the hash of the compiled graph
|
||||||
|
stack.enter_context(
|
||||||
|
patch("torch._inductor.codecache.compiled_fx_graph_hash",
|
||||||
|
hijack_compiled_fx_graph_hash))
|
||||||
|
|
||||||
|
# for providing a dummy shape environment
|
||||||
|
stack.enter_context(
|
||||||
|
patch("torch._inductor.codecache.FxGraphCache._get_shape_env",
|
||||||
|
_get_shape_env))
|
||||||
|
|
||||||
|
# for forcing the graph to be cached
|
||||||
|
stack.enter_context(
|
||||||
|
patch(
|
||||||
|
"torch._inductor.codecache.FxGraphCache._check_can_cache",
|
||||||
|
_check_can_cache))
|
||||||
|
|
||||||
|
# Dynamo metrics context, see method for more details.
|
||||||
|
stack.enter_context(self.metrics_context())
|
||||||
|
|
||||||
|
compiled_graph = compile_fx(
|
||||||
|
graph,
|
||||||
|
example_inputs,
|
||||||
|
inner_compile=hijacked_compile_fx_inner,
|
||||||
|
config_patches=current_config)
|
||||||
|
|
||||||
|
assert hash_str is not None, (
|
||||||
|
"failed to get the hash of the compiled graph")
|
||||||
|
assert file_path is not None, (
|
||||||
|
"failed to get the file path of the compiled graph")
|
||||||
|
return compiled_graph, (hash_str, file_path)
|
||||||
|
|
||||||
|
def load(self,
|
||||||
|
handle: Any,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
graph_index: int,
|
||||||
|
runtime_shape: Optional[int] = None) -> Callable:
|
||||||
|
assert isinstance(handle, tuple)
|
||||||
|
assert isinstance(handle[0], str)
|
||||||
|
assert isinstance(handle[1], str)
|
||||||
|
hash_str = handle[0]
|
||||||
|
|
||||||
|
from torch._inductor.codecache import FxGraphCache
|
||||||
|
with ExitStack() as exit_stack:
|
||||||
|
exit_stack.enter_context(
|
||||||
|
patch("torch._inductor.codecache.FxGraphCache._get_shape_env",
|
||||||
|
lambda *args, **kwargs: AlwaysHitShapeEnv()))
|
||||||
|
|
||||||
|
# Dynamo metrics context, see method for more details.
|
||||||
|
exit_stack.enter_context(self.metrics_context())
|
||||||
|
|
||||||
|
if torch.__version__.startswith("2.5"):
|
||||||
|
inductor_compiled_graph = FxGraphCache._lookup_graph(
|
||||||
|
hash_str, example_inputs, True, False)
|
||||||
|
assert inductor_compiled_graph is not None, (
|
||||||
|
"Inductor cache lookup failed. Please remove"
|
||||||
|
f"the cache directory and try again." # noqa
|
||||||
|
)
|
||||||
|
elif torch.__version__ >= "2.6":
|
||||||
|
from torch._inductor.output_code import (
|
||||||
|
CompiledFxGraphConstantsWithGm)
|
||||||
|
constants = CompiledFxGraphConstantsWithGm(graph)
|
||||||
|
inductor_compiled_graph, _ = FxGraphCache._lookup_graph(
|
||||||
|
hash_str, example_inputs, True, None, constants)
|
||||||
|
assert inductor_compiled_graph is not None, (
|
||||||
|
"Inductor cache lookup failed. Please remove"
|
||||||
|
f"the cache directory and try again." # noqa
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inductor calling convention (function signature):
|
||||||
|
# f(list) -> tuple
|
||||||
|
# Dynamo calling convention (function signature):
|
||||||
|
# f(*args) -> Any
|
||||||
|
|
||||||
|
# need to know if the graph returns a tuple
|
||||||
|
from torch._inductor.compile_fx import graph_returns_tuple
|
||||||
|
returns_tuple = graph_returns_tuple(graph)
|
||||||
|
|
||||||
|
# this is the callable we return to Dynamo to run
|
||||||
|
def compiled_graph(*args):
|
||||||
|
# convert args to list
|
||||||
|
list_args = list(args)
|
||||||
|
graph_output = inductor_compiled_graph(list_args)
|
||||||
|
# unpack the tuple if needed
|
||||||
|
if returns_tuple:
|
||||||
|
return graph_output
|
||||||
|
else:
|
||||||
|
return graph_output[0]
|
||||||
|
|
||||||
|
return compiled_graph
|
||||||
|
|
||||||
|
def metrics_context(self) -> contextlib.AbstractContextManager:
|
||||||
|
"""
|
||||||
|
This method returns the Dynamo metrics context (if it exists,
|
||||||
|
otherwise a null context). It is used by various compile components.
|
||||||
|
Present in torch>=2.6, it's used inside FxGraphCache in
|
||||||
|
torch==2.6 (but not after). It might also be used in various other
|
||||||
|
torch.compile internal functions.
|
||||||
|
|
||||||
|
Because it is re-entrant, we always set it (even if entering via Dynamo
|
||||||
|
and the context was already entered). We might want to revisit if it
|
||||||
|
should be set at a different level of compilation.
|
||||||
|
|
||||||
|
This is likely a bug in PyTorch: public APIs should not rely on
|
||||||
|
manually setting up internal contexts. But we also rely on non-public
|
||||||
|
APIs which might not provide these guarantees.
|
||||||
|
"""
|
||||||
|
if Version(importlib.metadata.version('torch')) >= Version("2.6"):
|
||||||
|
import torch._dynamo.utils
|
||||||
|
return torch._dynamo.utils.get_metrics_context()
|
||||||
|
else:
|
||||||
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
|
|
||||||
|
class EagerAdaptor(CompilerInterface):
|
||||||
|
name = "eager"
|
||||||
|
|
||||||
|
def compile(
|
||||||
|
self,
|
||||||
|
graph: fx.GraphModule,
|
||||||
|
example_inputs: List[Any],
|
||||||
|
compiler_config: Dict[str, Any],
|
||||||
|
runtime_shape: Optional[int] = None
|
||||||
|
) -> Tuple[Optional[Callable], Optional[Any]]:
|
||||||
|
# we don't need to compile the graph, just return the graph itself.
|
||||||
|
# It does not support caching, return None for the handle.
|
||||||
|
return graph, None
|
||||||
33
vllm/compilation/counter.py
Normal file
33
vllm/compilation/counter.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import dataclasses
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CompilationCounter:
|
||||||
|
num_models_seen: int = 0
|
||||||
|
num_graphs_seen: int = 0
|
||||||
|
# including the splitting ops
|
||||||
|
num_piecewise_graphs_seen: int = 0
|
||||||
|
# not including the splitting ops
|
||||||
|
num_piecewise_capturable_graphs_seen: int = 0
|
||||||
|
num_backend_compilations: int = 0
|
||||||
|
num_cudagraph_caputured: int = 0
|
||||||
|
|
||||||
|
def clone(self) -> "CompilationCounter":
|
||||||
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def expect(self, **kwargs):
|
||||||
|
old = self.clone()
|
||||||
|
yield
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
assert getattr(self, k) - getattr(old, k) == v, (
|
||||||
|
f"{k} not as expected, before it is {getattr(old, k)}"
|
||||||
|
f", after it is {getattr(self, k)}, "
|
||||||
|
f"expected diff is {v}")
|
||||||
|
|
||||||
|
|
||||||
|
compilation_counter = CompilationCounter()
|
||||||
249
vllm/compilation/decorators.py
Normal file
249
vllm/compilation/decorators.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from typing import Callable, Dict, List, Optional, TypeVar, Union, overload
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from torch._dynamo.symbolic_convert import InliningInstructionTranslator
|
||||||
|
|
||||||
|
from vllm.compilation.counter import compilation_counter
|
||||||
|
from vllm.compilation.wrapper import TorchCompileWrapperWithCustomDispatcher
|
||||||
|
from vllm.config import CompilationLevel, VllmConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.sequence import IntermediateTensors
|
||||||
|
from vllm.utils import supports_dynamo
|
||||||
|
|
||||||
|
from .monitor import start_monitoring_torch_compile
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
_T = TypeVar("_T", bound=type[nn.Module])
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def support_torch_compile(
|
||||||
|
*,
|
||||||
|
dynamic_arg_dims: Optional[Dict[str, Union[int, List[int]]]],
|
||||||
|
) -> Callable[[_T], _T]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def support_torch_compile(cls: _T) -> _T:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def support_torch_compile(
|
||||||
|
cls: Optional[_T] = None,
|
||||||
|
*,
|
||||||
|
dynamic_arg_dims: Optional[Dict[str, Union[int, List[int]]]] = None,
|
||||||
|
) -> Union[Callable[[_T], _T], _T]:
|
||||||
|
"""
|
||||||
|
A decorator to add support for compiling the forward method of a class.
|
||||||
|
|
||||||
|
Usage 1: use directly as a decorator without arguments:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@support_torch_compile
|
||||||
|
class MyModel(nn.Module):
|
||||||
|
def forward(self, x: torch.Tensor, y: Optional[torch.Tensor]):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage 2: use as a decorator with arguments:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@support_torch_compile(dynamic_arg_dims={"x": 0, "y": 0})
|
||||||
|
class MyModel(nn.Module):
|
||||||
|
def forward(self, x: torch.Tensor, y: Optional[torch.Tensor]):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
`dynamic_arg_dims` is a dictionary that maps argument names to the dynamic
|
||||||
|
dimensions of the argument. The dynamic dimensions can be either a single
|
||||||
|
integer or a list of integers.
|
||||||
|
|
||||||
|
if `dynamic_arg_dims` is `None`, it is inferred from the type annotation
|
||||||
|
of the `forward` method, based on the following default rules:
|
||||||
|
|
||||||
|
- if the argument is annotated as `torch.Tensor` or
|
||||||
|
`Optional[torch.Tensor]`, the first dimension will be
|
||||||
|
marked as dynamic.
|
||||||
|
- if the argument is annotated as `IntermediateTensors`, the first
|
||||||
|
dimension of all the tensors in the intermediate tensors
|
||||||
|
will be marked as dynamic.
|
||||||
|
|
||||||
|
During runtime, when we actually mark dimensions of tensors,
|
||||||
|
it depends on the value of arguments:
|
||||||
|
|
||||||
|
- if it is a single integer (can be negative), the corresponding dimension
|
||||||
|
of the argument will be marked as dynamic.
|
||||||
|
- if it is `None`, ignored.
|
||||||
|
- if it is `IntermediateTensors`, all the tensors in the intermediate
|
||||||
|
tensors will be marked as dynamic.
|
||||||
|
- otherwise, it will raise an error.
|
||||||
|
|
||||||
|
NOTE: if an argument is `None`, it should always be passed as `None` during
|
||||||
|
the lifetime of the model, otherwise, it cannot be captured as a single
|
||||||
|
computation graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def cls_decorator_helper(cls: _T) -> _T:
|
||||||
|
# helper to pass `dynamic_arg_dims`` to `_support_torch_compile``
|
||||||
|
# to avoid too much indentation for `_support_torch_compile``
|
||||||
|
if not hasattr(cls, 'forward'):
|
||||||
|
raise TypeError("decorated class should have a forward method.")
|
||||||
|
sig = inspect.signature(cls.forward)
|
||||||
|
inferred_dynamic_arg_dims = dynamic_arg_dims
|
||||||
|
if inferred_dynamic_arg_dims is None:
|
||||||
|
inferred_dynamic_arg_dims = {}
|
||||||
|
for k, v in sig.parameters.items():
|
||||||
|
if v.annotation in [
|
||||||
|
torch.Tensor, Optional[torch.Tensor],
|
||||||
|
IntermediateTensors, Optional[IntermediateTensors]
|
||||||
|
]:
|
||||||
|
inferred_dynamic_arg_dims[k] = 0
|
||||||
|
|
||||||
|
logger.debug(("Inferred dynamic dimensions for "
|
||||||
|
"forward method of %s: %s"), cls,
|
||||||
|
list(inferred_dynamic_arg_dims.keys()))
|
||||||
|
|
||||||
|
if len(inferred_dynamic_arg_dims) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"No dynamic dimensions found in the forward method of "
|
||||||
|
f"{cls}. Please provide dynamic_arg_dims explicitly.")
|
||||||
|
|
||||||
|
for k in inferred_dynamic_arg_dims:
|
||||||
|
if k not in sig.parameters:
|
||||||
|
raise ValueError(
|
||||||
|
f"Argument {k} not found in the forward method of {cls}")
|
||||||
|
return _support_torch_compile(cls, inferred_dynamic_arg_dims)
|
||||||
|
|
||||||
|
if cls is not None:
|
||||||
|
# use `support_torch_compile` as a decorator without arguments
|
||||||
|
assert isinstance(cls, type)
|
||||||
|
return cls_decorator_helper(cls)
|
||||||
|
|
||||||
|
return cls_decorator_helper
|
||||||
|
|
||||||
|
|
||||||
|
def _support_torch_compile(
|
||||||
|
cls: _T,
|
||||||
|
dynamic_arg_dims: Dict[str, Union[int, List[int]]],
|
||||||
|
) -> _T:
|
||||||
|
"""
|
||||||
|
A decorator to add support for compiling the forward method of a class.
|
||||||
|
"""
|
||||||
|
if TorchCompileWrapperWithCustomDispatcher in cls.__bases__:
|
||||||
|
# support decorating multiple times
|
||||||
|
return cls
|
||||||
|
|
||||||
|
# take care of method resolution order
|
||||||
|
# make sure super().__init__ is called on the base class
|
||||||
|
# other than TorchCompileWrapperWithCustomDispatcher
|
||||||
|
cls.__bases__ = cls.__bases__ + (TorchCompileWrapperWithCustomDispatcher, )
|
||||||
|
|
||||||
|
old_init = cls.__init__
|
||||||
|
|
||||||
|
def __init__(self, *, vllm_config: VllmConfig, prefix: str = '', **kwargs):
|
||||||
|
old_init(self, vllm_config=vllm_config, prefix=prefix, **kwargs)
|
||||||
|
self.vllm_config = vllm_config
|
||||||
|
# for CompilationLevel.DYNAMO_AS_IS , the upper level model runner
|
||||||
|
# will handle the compilation, so we don't need to do anything here.
|
||||||
|
self.do_not_compile = \
|
||||||
|
vllm_config.compilation_config.level in [
|
||||||
|
CompilationLevel.NO_COMPILATION, CompilationLevel.DYNAMO_AS_IS
|
||||||
|
] or not supports_dynamo()
|
||||||
|
if self.do_not_compile:
|
||||||
|
return
|
||||||
|
compilation_counter.num_models_seen += 1
|
||||||
|
TorchCompileWrapperWithCustomDispatcher.__init__(
|
||||||
|
self, compilation_level=vllm_config.compilation_config.level)
|
||||||
|
|
||||||
|
cls.__init__ = __init__
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
# torch.compiler.is_compiling() means we are inside the compilation
|
||||||
|
# e.g. TPU has the compilation logic in model runner, so we don't
|
||||||
|
# need to compile the model inside.
|
||||||
|
if self.do_not_compile or torch.compiler.is_compiling():
|
||||||
|
return self.forward(*args, **kwargs)
|
||||||
|
|
||||||
|
# the first compilation needs to have dynamic shapes marked
|
||||||
|
if len(self.compiled_codes) < 1:
|
||||||
|
sig = inspect.signature(self.__class__.forward)
|
||||||
|
bound_args = sig.bind(self, *args, **kwargs)
|
||||||
|
bound_args.apply_defaults()
|
||||||
|
for k, dims in dynamic_arg_dims.items():
|
||||||
|
arg = bound_args.arguments.get(k)
|
||||||
|
if arg is not None:
|
||||||
|
dims = [dims] if isinstance(dims, int) else dims
|
||||||
|
if isinstance(arg, torch.Tensor):
|
||||||
|
# In case dims is specified with negative indexing
|
||||||
|
dims = [
|
||||||
|
arg.ndim + dim if dim < 0 else dim for dim in dims
|
||||||
|
]
|
||||||
|
torch._dynamo.mark_dynamic(arg, dims)
|
||||||
|
elif isinstance(arg, IntermediateTensors):
|
||||||
|
for tensor in arg.tensors.values():
|
||||||
|
# In case dims is specified with negative indexing
|
||||||
|
dims = [
|
||||||
|
tensor.ndim + dim if dim < 0 else dim
|
||||||
|
for dim in dims
|
||||||
|
]
|
||||||
|
torch._dynamo.mark_dynamic(tensor, dims)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"Unsupported dynamic dimensions"
|
||||||
|
f" {dims} for argument {k} with type {type(arg)}.")
|
||||||
|
# here, it is the starting point of the `torch.compile` process
|
||||||
|
start_monitoring_torch_compile(self.vllm_config)
|
||||||
|
logger.debug("Start compiling function %s",
|
||||||
|
self.original_code_object)
|
||||||
|
|
||||||
|
# if we don't use custom dispatcher, we can directly call the
|
||||||
|
# compiled function and let torch.compile handle the dispatching,
|
||||||
|
# with the overhead of guard evaluation and recompilation.
|
||||||
|
if len(self.compiled_codes) < 1 or not self.use_custom_dispatcher:
|
||||||
|
# it seems Dynamo reuse the compilation across instances,
|
||||||
|
# while we need to make sure the compiled code is not reused.
|
||||||
|
# we need to control all the compilation of the model.
|
||||||
|
torch._dynamo.eval_frame.remove_from_cache(
|
||||||
|
self.original_code_object)
|
||||||
|
|
||||||
|
# collect all relevant files traced by Dynamo,
|
||||||
|
# so that the compilation cache can trigger re-compilation
|
||||||
|
# properly when any of these files change.
|
||||||
|
|
||||||
|
# 1. the file containing the top-level forward function
|
||||||
|
self.vllm_config.compilation_config.traced_files.add(
|
||||||
|
self.original_code_object.co_filename)
|
||||||
|
|
||||||
|
# 2. every time Dynamo sees a function call, it will inline
|
||||||
|
# the function by calling InliningInstructionTranslator.inline_call
|
||||||
|
# we hijack this function to know all the functions called
|
||||||
|
# during Dynamo tracing, and their corresponding files
|
||||||
|
inline_call = InliningInstructionTranslator.inline_call
|
||||||
|
|
||||||
|
def patched_inline_call(parent, func, args, kwargs):
|
||||||
|
code = func.get_code()
|
||||||
|
self.vllm_config.compilation_config.traced_files.add(
|
||||||
|
code.co_filename)
|
||||||
|
return inline_call(parent, func, args, kwargs)
|
||||||
|
|
||||||
|
with patch.object(InliningInstructionTranslator, 'inline_call',
|
||||||
|
patched_inline_call):
|
||||||
|
output = self.compiled_callable(*args, **kwargs)
|
||||||
|
return output
|
||||||
|
|
||||||
|
# usually, capturing the model once is enough, and then we can
|
||||||
|
# dispatch to the compiled code directly, without going through
|
||||||
|
# the Dynamo guard mechanism.
|
||||||
|
with self.dispatch_to_code(0):
|
||||||
|
model_output = self.forward(*args, **kwargs)
|
||||||
|
return model_output
|
||||||
|
|
||||||
|
cls.__call__ = __call__
|
||||||
|
return cls
|
||||||
182
vllm/compilation/fix_functionalization.py
Normal file
182
vllm/compilation/fix_functionalization.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import operator
|
||||||
|
from typing import Dict, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch._higher_order_ops.auto_functionalize import auto_functionalized
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
from .fx_utils import is_func
|
||||||
|
from .vllm_inductor_pass import VllmInductorPass
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FixFunctionalizationPass(VllmInductorPass):
|
||||||
|
"""
|
||||||
|
This pass defunctionalizes certain nodes to avoid redundant tensor copies.
|
||||||
|
After this pass, DCE (dead-code elimination) should never be run,
|
||||||
|
as de-functionalized nodes may appear as dead code.
|
||||||
|
|
||||||
|
To add new nodes to defunctionalize, add to the if-elif chain in __call__.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, graph: torch.fx.Graph):
|
||||||
|
self.begin()
|
||||||
|
self.dump_graph(graph, "before_fix_functionalization")
|
||||||
|
|
||||||
|
self.nodes_to_remove: List[torch.fx.Node] = []
|
||||||
|
count = 0
|
||||||
|
for node in graph.nodes:
|
||||||
|
if not is_func(node, auto_functionalized):
|
||||||
|
continue # Avoid deep if-elif nesting
|
||||||
|
|
||||||
|
kwargs = node.kwargs
|
||||||
|
at_target = node.args[0]
|
||||||
|
|
||||||
|
if at_target == torch.ops._C.rotary_embedding.default:
|
||||||
|
query = kwargs['query']
|
||||||
|
mm_node = query.args[0].args[0]
|
||||||
|
|
||||||
|
# rotary_embedding is a special case: the two mutating inputs
|
||||||
|
# are query and key, which are slices of mm_node.
|
||||||
|
# While functionalized, results at[1] and at[2] are scattered
|
||||||
|
# back into mm_node. After de-functionalization, we can just
|
||||||
|
# use mm_node directly.
|
||||||
|
for idx, user in self.getitem_users(node).items():
|
||||||
|
for user_of_getitem in user.users:
|
||||||
|
if is_func(user_of_getitem,
|
||||||
|
torch.ops.aten.slice_scatter.default):
|
||||||
|
user_of_getitem.replace_all_uses_with(mm_node)
|
||||||
|
self._remove(user_of_getitem)
|
||||||
|
self._remove(user)
|
||||||
|
|
||||||
|
self.insert_defunctionalized(graph, node)
|
||||||
|
self._remove(node)
|
||||||
|
|
||||||
|
# rms_norm replacements avoid the most copies for LLaMa.
|
||||||
|
elif at_target == torch.ops._C.fused_add_rms_norm.default:
|
||||||
|
mutated_args = {1: 'input', 2: 'residual'}
|
||||||
|
self.defunctionalize(graph, node, mutated_args)
|
||||||
|
elif at_target == torch.ops._C.fused_add_rms_norm_static_fp8_quant.default: # noqa: E501
|
||||||
|
mutated_args = {1: 'result', 2: 'residual'}
|
||||||
|
self.defunctionalize(graph, node, mutated_args)
|
||||||
|
elif at_target == torch.ops._C.rms_norm_dynamic_per_token_quant.default: # noqa: E501
|
||||||
|
mutated_args = {1: 'result', 2: 'scale', 3: 'residual'}
|
||||||
|
self.defunctionalize(graph, node, mutated_args)
|
||||||
|
elif at_target in [
|
||||||
|
torch.ops._C.rms_norm.default,
|
||||||
|
torch.ops._C.rms_norm_static_fp8_quant.default
|
||||||
|
]:
|
||||||
|
mutated_args = {1: 'result'}
|
||||||
|
self.defunctionalize(graph, node, mutated_args)
|
||||||
|
|
||||||
|
elif at_target == torch.ops._C.silu_and_mul.default:
|
||||||
|
mutated_args = {1: 'out'}
|
||||||
|
# Because we have an 'out', need to specify args directly
|
||||||
|
self.defunctionalize(graph,
|
||||||
|
node,
|
||||||
|
mutated_args,
|
||||||
|
args=('out', 'input'))
|
||||||
|
else:
|
||||||
|
continue # skip the count
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
self.dump_graph(graph, "before_fix_functionalization_cleanup")
|
||||||
|
|
||||||
|
# Remove the nodes all at once
|
||||||
|
count_removed = len(self.nodes_to_remove)
|
||||||
|
for node in self.nodes_to_remove:
|
||||||
|
graph.erase_node(node)
|
||||||
|
|
||||||
|
logger.debug("De-functionalized %s nodes, removed %s nodes", count,
|
||||||
|
count_removed)
|
||||||
|
self.dump_graph(graph, "after_fix_functionalization")
|
||||||
|
self.end_and_log()
|
||||||
|
|
||||||
|
def _remove(self, node_or_nodes: Union[torch.fx.Node,
|
||||||
|
Iterable[torch.fx.Node]]):
|
||||||
|
"""
|
||||||
|
Stage a node (or nodes) for removal at the end of the pass.
|
||||||
|
"""
|
||||||
|
if isinstance(node_or_nodes, torch.fx.Node):
|
||||||
|
self.nodes_to_remove.append(node_or_nodes)
|
||||||
|
else:
|
||||||
|
self.nodes_to_remove.extend(node_or_nodes)
|
||||||
|
|
||||||
|
def defunctionalize(self,
|
||||||
|
graph: torch.fx.Graph,
|
||||||
|
node: torch.fx.Node,
|
||||||
|
mutated_args: Dict[int, Union[torch.fx.Node, str]],
|
||||||
|
args: Optional[Tuple[Union[torch.fx.Node, str],
|
||||||
|
...]] = None):
|
||||||
|
"""
|
||||||
|
De-functionalize a node by replacing it with a call to the original.
|
||||||
|
It also replaces the getitem users with the mutated arguments.
|
||||||
|
See replace_users_with_mutated_args and insert_defunctionalized.
|
||||||
|
"""
|
||||||
|
self.replace_users_with_mutated_args(node, mutated_args)
|
||||||
|
self.insert_defunctionalized(graph, node, args=args)
|
||||||
|
self._remove(node)
|
||||||
|
|
||||||
|
def replace_users_with_mutated_args(self, node: torch.fx.Node,
|
||||||
|
mutated_args: Dict[int,
|
||||||
|
Union[torch.fx.Node,
|
||||||
|
str]]):
|
||||||
|
"""
|
||||||
|
Replace all getitem users of the auto-functionalized node with the
|
||||||
|
mutated arguments.
|
||||||
|
:param node: The auto-functionalized node
|
||||||
|
:param mutated_args: The mutated arguments, indexed by getitem index.
|
||||||
|
If the value of an arg is a string, `node.kwargs[arg]` is used.
|
||||||
|
"""
|
||||||
|
for idx, user in self.getitem_users(node).items():
|
||||||
|
arg = mutated_args[idx]
|
||||||
|
arg = node.kwargs[arg] if isinstance(arg, str) else arg
|
||||||
|
user.replace_all_uses_with(arg)
|
||||||
|
self._remove(user)
|
||||||
|
|
||||||
|
def getitem_users(self, node: torch.fx.Node) -> Dict[int, torch.fx.Node]:
|
||||||
|
"""
|
||||||
|
Returns the operator.getitem users of the auto-functionalized node,
|
||||||
|
indexed by the index they are getting.
|
||||||
|
"""
|
||||||
|
users = {}
|
||||||
|
for user in node.users:
|
||||||
|
if is_func(user, operator.getitem):
|
||||||
|
idx = user.args[1]
|
||||||
|
users[idx] = user
|
||||||
|
return users
|
||||||
|
|
||||||
|
def insert_defunctionalized(self,
|
||||||
|
graph: torch.fx.Graph,
|
||||||
|
node: torch.fx.Node,
|
||||||
|
args: Optional[Tuple[Union[torch.fx.Node, str],
|
||||||
|
...]] = None):
|
||||||
|
"""
|
||||||
|
Insert a new defunctionalized node into the graph before node.
|
||||||
|
If one of the kwargs is 'out', provide args directly,
|
||||||
|
as node.kwargs cannot be used.
|
||||||
|
See https://github.com/pytorch/pytorch/blob/a00faf440888ffb724bad413f329a49e2b6388e7/torch/_inductor/lowering.py#L351
|
||||||
|
|
||||||
|
:param graph: Graph to insert the defunctionalized node into
|
||||||
|
:param node: The auto-functionalized node to defunctionalize
|
||||||
|
:param args: If we cannot use kwargs, specify args directly.
|
||||||
|
If an arg is a string, `node.kwargs[arg]` is used.
|
||||||
|
""" # noqa: E501
|
||||||
|
assert is_func(node, auto_functionalized), \
|
||||||
|
f"node must be auto-functionalized, is {node} instead"
|
||||||
|
|
||||||
|
# Create a new call to the original function
|
||||||
|
with graph.inserting_before(node):
|
||||||
|
function = node.args[0]
|
||||||
|
if args is None:
|
||||||
|
graph.call_function(function, kwargs=node.kwargs)
|
||||||
|
else:
|
||||||
|
# Args passed as strings refer to items in node.kwargs
|
||||||
|
args = tuple(node.kwargs[arg] if isinstance(arg, str) else arg
|
||||||
|
for arg in args)
|
||||||
|
graph.call_function(function, args=args)
|
||||||
617
vllm/compilation/fusion.py
Normal file
617
vllm/compilation/fusion.py
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Callable, Dict, List, NamedTuple, Optional, Tuple
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch._inductor.pattern_matcher as pm
|
||||||
|
from torch import fx
|
||||||
|
from torch._higher_order_ops.auto_functionalize import auto_functionalized
|
||||||
|
from torch._inductor.pattern_matcher import PatternMatcherPass
|
||||||
|
from torch._ops import OpOverload
|
||||||
|
|
||||||
|
from vllm.config import CompilationConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
|
||||||
|
from .fx_utils import find_getitem_maybe
|
||||||
|
from .multi_output_match import MultiOutputMatch
|
||||||
|
from .vllm_inductor_pass import VllmInductorPass
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
FP8_DTYPE = current_platform.fp8_dtype()
|
||||||
|
|
||||||
|
|
||||||
|
def empty_bf16(*args, **kwargs):
|
||||||
|
return torch.empty(*args, **kwargs, dtype=torch.bfloat16, device="cuda")
|
||||||
|
|
||||||
|
|
||||||
|
def empty_fp32(*args, **kwargs):
|
||||||
|
return torch.empty(*args, **kwargs, dtype=torch.float32, device="cuda")
|
||||||
|
|
||||||
|
|
||||||
|
RMS_OP = torch.ops._C.rms_norm.default
|
||||||
|
RMS_ADD_OP = torch.ops._C.fused_add_rms_norm.default
|
||||||
|
|
||||||
|
|
||||||
|
class QuantKey(NamedTuple):
|
||||||
|
"""
|
||||||
|
Named tuple for identifying the type of quantization.
|
||||||
|
dtype: quantized data type
|
||||||
|
static: static quantization if True, dynamic if False
|
||||||
|
per_tensor: per-tensor quantization if True, per-token if False
|
||||||
|
symmetric: symmetric if True, asymmetric if False
|
||||||
|
"""
|
||||||
|
dtype: torch.dtype
|
||||||
|
static: bool
|
||||||
|
per_tensor: bool = True
|
||||||
|
symmetric: bool = True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (f"QuantKey({'static' if self.static else 'dynamic'},"
|
||||||
|
f"{fx.graph.dtype_abbrs[self.dtype]},"
|
||||||
|
f"{'per_tensor' if self.per_tensor else 'per_token'},"
|
||||||
|
f"{'a' if not self.symmetric else ''}symmetric)")
|
||||||
|
|
||||||
|
|
||||||
|
kFp8StaticTensorSym = QuantKey(FP8_DTYPE, True, True, True)
|
||||||
|
kFp8DynamicTensorSym = QuantKey(FP8_DTYPE, False, True, True)
|
||||||
|
kFp8DynamicTokenSym = QuantKey(FP8_DTYPE, False, False, True)
|
||||||
|
|
||||||
|
QUANT_OPS: Dict[QuantKey, OpOverload] = {
|
||||||
|
kFp8StaticTensorSym: torch.ops._C.static_scaled_fp8_quant.default, # noqa
|
||||||
|
kFp8DynamicTensorSym:
|
||||||
|
torch.ops._C.dynamic_scaled_fp8_quant.default, # noqa
|
||||||
|
kFp8DynamicTokenSym:
|
||||||
|
torch.ops._C.dynamic_per_token_scaled_fp8_quant.default, # noqa
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FusedRMSQuantKey(NamedTuple):
|
||||||
|
"""
|
||||||
|
Named tuple for identifying the type of RMSNorm + quant fusion.
|
||||||
|
quant: type of quantization
|
||||||
|
fused_add: does the op also perform the residual add
|
||||||
|
"""
|
||||||
|
quant: QuantKey
|
||||||
|
fused_add: bool
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (f"FusedQuantKey({self.quant}, with"
|
||||||
|
f"{'' if self.fused_add else 'out'} residual)")
|
||||||
|
|
||||||
|
|
||||||
|
FUSED_OPS: Dict[FusedRMSQuantKey, OpOverload] = {
|
||||||
|
FusedRMSQuantKey(kFp8StaticTensorSym, False):
|
||||||
|
torch.ops._C.rms_norm_static_fp8_quant.default, # noqa
|
||||||
|
FusedRMSQuantKey(kFp8StaticTensorSym, True):
|
||||||
|
torch.ops._C.fused_add_rms_norm_static_fp8_quant.default, # noqa
|
||||||
|
FusedRMSQuantKey(kFp8DynamicTokenSym, False):
|
||||||
|
torch.ops._C.rms_norm_dynamic_per_token_quant.default, # noqa
|
||||||
|
FusedRMSQuantKey(kFp8DynamicTokenSym, True):
|
||||||
|
torch.ops._C.rms_norm_dynamic_per_token_quant.default, # noqa
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QuantMultiOutputMatch(MultiOutputMatch):
|
||||||
|
|
||||||
|
def __init__(self, match: pm.Match, quant_op, fused_op):
|
||||||
|
super().__init__(match)
|
||||||
|
assert isinstance(quant_op, OpOverload)
|
||||||
|
assert isinstance(fused_op, OpOverload)
|
||||||
|
self.QUANT_OP = quant_op # in-place quant op
|
||||||
|
self.FUSED_OP = fused_op # in-place fused quant op
|
||||||
|
|
||||||
|
def insert_fused_node(self, fused_return_mapping: Dict[int, Tuple[fx.Node,
|
||||||
|
int]],
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
This utility function inserts an auto-functionalized node for FUSED_OP.
|
||||||
|
It also correctly sets its meta value and rebinds the users of the
|
||||||
|
unfused nodes to use the fused node instead.
|
||||||
|
|
||||||
|
:param fused_return_mapping: A dictionary, mapping from getitem indices
|
||||||
|
of the fused node result to a tuple of the old node and a getitem index.
|
||||||
|
:param kwargs: kwargs that get directly forwarded to the auto_fn node
|
||||||
|
|
||||||
|
Example:
|
||||||
|
If we want to replace this graph:
|
||||||
|
_, x1, x2 = auto_fn(op1)
|
||||||
|
_, y1, y2 = auto_fn(op2)
|
||||||
|
|
||||||
|
with
|
||||||
|
_, x1, y2, x2 = auto_fn(FUSED_OP)
|
||||||
|
|
||||||
|
we would call:
|
||||||
|
insert_fused_node({1: (op1_node, 1), 2: (op2_node, 2), 3: (op1_node, 2)}
|
||||||
|
|
||||||
|
Note that the 0th element is None for auto-functionalized in-place ops.
|
||||||
|
Hence, others appear 1-indexed.
|
||||||
|
"""
|
||||||
|
fused_node = self.insert_auto_fn(self.FUSED_OP, kwargs)
|
||||||
|
indices = fused_return_mapping.keys()
|
||||||
|
getitem_nodes = self.insert_getitems(fused_node, indices)
|
||||||
|
|
||||||
|
# Prepare the meta value, use a list so it's mutable
|
||||||
|
meta_val = [None] * (max(indices) + 1)
|
||||||
|
|
||||||
|
# Iterate through elements of the tuple produced by fused_node
|
||||||
|
for idx, getitem_node in zip(indices, getitem_nodes):
|
||||||
|
old_node, old_idx = fused_return_mapping[idx]
|
||||||
|
|
||||||
|
# If the old value was never used, the old_getitem might not exist
|
||||||
|
old_getitem = find_getitem_maybe(old_node, old_idx)
|
||||||
|
if old_getitem is not None:
|
||||||
|
# Rebind the users of match getitem nodes to use the new nodes.
|
||||||
|
# The old nodes will be removed by DCE at the end of the pass.
|
||||||
|
old_getitem.replace_all_uses_with(getitem_node)
|
||||||
|
getitem_node.meta["val"] = old_getitem.meta["val"]
|
||||||
|
|
||||||
|
# Extract the appropriate meta value
|
||||||
|
# It is present even if the getitem node does not exist
|
||||||
|
meta_val[idx] = old_node.meta["val"][old_idx]
|
||||||
|
|
||||||
|
# Fix the meta value on the new fused node
|
||||||
|
fused_node.meta["val"] = tuple(meta_val)
|
||||||
|
|
||||||
|
|
||||||
|
class RMSNormQuantPattern:
|
||||||
|
|
||||||
|
def __init__(self, epsilon: float, key: FusedRMSQuantKey):
|
||||||
|
self.epsilon = epsilon
|
||||||
|
self.quant_dtype = key.quant.dtype
|
||||||
|
|
||||||
|
assert key.quant in QUANT_OPS, \
|
||||||
|
f"unsupported quantization scheme {key.quant}"
|
||||||
|
self.QUANT_OP = QUANT_OPS[key.quant]
|
||||||
|
|
||||||
|
assert key in FUSED_OPS, \
|
||||||
|
f"unsupported fused rmsnorm+quant op for {key}"
|
||||||
|
self.FUSED_OP = FUSED_OPS[key]
|
||||||
|
|
||||||
|
|
||||||
|
class RMSNormStaticQuantPattern(RMSNormQuantPattern):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
epsilon: float,
|
||||||
|
quant_dtype: torch.dtype,
|
||||||
|
symmetric=True):
|
||||||
|
fused_key = FusedRMSQuantKey(fused_add=False,
|
||||||
|
quant=QuantKey(dtype=quant_dtype,
|
||||||
|
static=True,
|
||||||
|
per_tensor=True,
|
||||||
|
symmetric=symmetric))
|
||||||
|
super().__init__(epsilon, fused_key)
|
||||||
|
|
||||||
|
def register(self, pm_pass: PatternMatcherPass):
|
||||||
|
# Cannot use methods, as the self argument affects tracing
|
||||||
|
def pattern(result: torch.Tensor, result_rms: torch.Tensor,
|
||||||
|
input: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at1 = auto_functionalized(RMS_OP,
|
||||||
|
result=result_rms,
|
||||||
|
input=input,
|
||||||
|
weight=weight,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
at2 = auto_functionalized(self.QUANT_OP,
|
||||||
|
result=result,
|
||||||
|
input=at1[1],
|
||||||
|
scale=scale)
|
||||||
|
|
||||||
|
# result
|
||||||
|
return at2[1]
|
||||||
|
|
||||||
|
def replacement(result: torch.Tensor, result_rms: torch.Tensor,
|
||||||
|
input: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(self.FUSED_OP,
|
||||||
|
result=result,
|
||||||
|
input=input,
|
||||||
|
weight=weight,
|
||||||
|
scale=scale,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
|
||||||
|
# result
|
||||||
|
return at[1]
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
|
||||||
|
empty_bf16(5, 4), # result_rms
|
||||||
|
empty_bf16(5, 4), # input
|
||||||
|
empty_bf16(1, 5), # weight
|
||||||
|
empty_fp32(1, 1) # scale
|
||||||
|
]
|
||||||
|
|
||||||
|
pm.register_replacement(pattern, replacement, inputs, pm.fwd_only,
|
||||||
|
pm_pass)
|
||||||
|
|
||||||
|
|
||||||
|
class FusedAddRMSNormStaticQuantPattern(RMSNormQuantPattern):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
epsilon: float,
|
||||||
|
quant_dtype: torch.dtype,
|
||||||
|
symmetric=True):
|
||||||
|
key = FusedRMSQuantKey(fused_add=True,
|
||||||
|
quant=QuantKey(dtype=quant_dtype,
|
||||||
|
static=True,
|
||||||
|
per_tensor=True,
|
||||||
|
symmetric=symmetric))
|
||||||
|
super().__init__(epsilon, key)
|
||||||
|
|
||||||
|
def register(self, pm_pass: PatternMatcherPass,
|
||||||
|
record_match: Callable[[MultiOutputMatch], bool]):
|
||||||
|
|
||||||
|
def pattern(result: torch.Tensor, input: torch.Tensor,
|
||||||
|
residual: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(RMS_ADD_OP,
|
||||||
|
input=input,
|
||||||
|
residual=residual,
|
||||||
|
weight=weight,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
at1 = auto_functionalized(self.QUANT_OP,
|
||||||
|
result=result,
|
||||||
|
input=at[1],
|
||||||
|
scale=scale)
|
||||||
|
|
||||||
|
# result, residual
|
||||||
|
return at1[1], at[2]
|
||||||
|
|
||||||
|
def replacement(result: torch.Tensor, input: torch.Tensor,
|
||||||
|
residual: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(self.FUSED_OP,
|
||||||
|
result=result,
|
||||||
|
input=input,
|
||||||
|
residual=residual,
|
||||||
|
weight=weight,
|
||||||
|
scale=scale,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
|
||||||
|
# result, residual
|
||||||
|
return at[1], at[2]
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
|
||||||
|
empty_bf16(5, 4), # input
|
||||||
|
empty_bf16(5, 4), # residual
|
||||||
|
empty_bf16(1, 5), # weight
|
||||||
|
empty_fp32(1, 1) # scale
|
||||||
|
]
|
||||||
|
|
||||||
|
pm.register_replacement(
|
||||||
|
pattern,
|
||||||
|
replacement,
|
||||||
|
inputs,
|
||||||
|
pm.fwd_only,
|
||||||
|
pm_pass,
|
||||||
|
extra_check=lambda m: record_match(
|
||||||
|
self.Match(m, self.QUANT_OP, self.FUSED_OP)))
|
||||||
|
|
||||||
|
class Match(QuantMultiOutputMatch):
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
# Find the nodes in the match that we need to rebind
|
||||||
|
rms_node = self.find_auto_fn(RMS_ADD_OP)
|
||||||
|
quant_node = self.find_auto_fn(self.QUANT_OP)
|
||||||
|
|
||||||
|
assert len(rms_node.users) == 2
|
||||||
|
assert len(quant_node.users) == 1
|
||||||
|
|
||||||
|
# First, insert a new auto_functionalized node for the fused op,
|
||||||
|
# as well as getitem nodes to extract the result and residual.
|
||||||
|
# The auto_fn node returns a tuple of (None, result, residual).
|
||||||
|
#
|
||||||
|
# The resulting graph looks like this:
|
||||||
|
# at = auto_functionalized(torch.ops._C.fused_add_rms_norm_static_fp8_quant.default, ...) # noqa
|
||||||
|
# result_node_new = at[1]
|
||||||
|
# residual_node_new = at[2]
|
||||||
|
with self.inserting_after_match():
|
||||||
|
# Missing epsilon, scalars cannot be inputs to the pattern
|
||||||
|
kwargs = self.match.kwargs.copy()
|
||||||
|
|
||||||
|
# 0 is always None
|
||||||
|
fused_return_mapping = {1: (quant_node, 1), 2: (rms_node, 2)}
|
||||||
|
self.insert_fused_node(fused_return_mapping,
|
||||||
|
epsilon=rms_node.kwargs["epsilon"],
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RMSNormDynamicQuantPattern(RMSNormQuantPattern):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
epsilon: float,
|
||||||
|
quant_dtype: torch.dtype,
|
||||||
|
per_tensor: bool,
|
||||||
|
symmetric=True):
|
||||||
|
key = FusedRMSQuantKey(fused_add=False,
|
||||||
|
quant=QuantKey(dtype=quant_dtype,
|
||||||
|
static=False,
|
||||||
|
per_tensor=per_tensor,
|
||||||
|
symmetric=symmetric))
|
||||||
|
super().__init__(epsilon, key)
|
||||||
|
|
||||||
|
def register(self, pm_pass: PatternMatcherPass,
|
||||||
|
record_match: Callable[[MultiOutputMatch], bool]):
|
||||||
|
|
||||||
|
def pattern(result: torch.Tensor, result_rms: torch.Tensor,
|
||||||
|
input: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at1 = auto_functionalized(RMS_OP,
|
||||||
|
result=result_rms,
|
||||||
|
input=input,
|
||||||
|
weight=weight,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
at2 = auto_functionalized(self.QUANT_OP,
|
||||||
|
result=result,
|
||||||
|
input=at1[1],
|
||||||
|
scale=scale,
|
||||||
|
scale_ub=None)
|
||||||
|
|
||||||
|
# result, scale
|
||||||
|
return at2[1], at2[2]
|
||||||
|
|
||||||
|
def replacement(result: torch.Tensor, result_rms: torch.Tensor,
|
||||||
|
input: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(self.FUSED_OP,
|
||||||
|
result=result,
|
||||||
|
input=input,
|
||||||
|
weight=weight,
|
||||||
|
scale=scale,
|
||||||
|
epsilon=self.epsilon,
|
||||||
|
scale_ub=None,
|
||||||
|
residual=None)
|
||||||
|
|
||||||
|
# result, scale
|
||||||
|
return at[1], at[2]
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
|
||||||
|
empty_bf16(5, 4), # result_rms
|
||||||
|
empty_bf16(5, 4), # input
|
||||||
|
empty_bf16(1, 5), # weight
|
||||||
|
empty_fp32(1, 1) # scale
|
||||||
|
]
|
||||||
|
|
||||||
|
pm.register_replacement(
|
||||||
|
pattern,
|
||||||
|
replacement,
|
||||||
|
inputs,
|
||||||
|
pm.fwd_only,
|
||||||
|
pm_pass,
|
||||||
|
extra_check=lambda m: record_match(
|
||||||
|
self.Match(m, self.QUANT_OP, self.FUSED_OP)))
|
||||||
|
|
||||||
|
class Match(QuantMultiOutputMatch):
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
# Find the nodes in the match that we need to rebind
|
||||||
|
rms_node = self.find_auto_fn(RMS_OP)
|
||||||
|
quant_node = self.find_auto_fn(self.QUANT_OP)
|
||||||
|
|
||||||
|
assert len(rms_node.users) == 1
|
||||||
|
assert len(quant_node.users) == 2
|
||||||
|
|
||||||
|
# First, insert a new auto_functionalized node for the fused op,
|
||||||
|
# as well as getitem nodes to extract the result and scale.
|
||||||
|
# The auto_fn node returns a tuple of (None, result, scale).
|
||||||
|
#
|
||||||
|
# The resulting graph looks like this:
|
||||||
|
# at = auto_functionalized(torch.ops._C.rms_norm_dynamic_per_token_quant.default, ...) # noqa
|
||||||
|
# result_node_new = at[1]
|
||||||
|
# scale_node_new = at[2]
|
||||||
|
with self.inserting_after_match():
|
||||||
|
# Missing epsilon, scalars cannot be inputs to the pattern
|
||||||
|
kwargs = self.match.kwargs.copy()
|
||||||
|
del kwargs["result_rms"] # not used in the fused op
|
||||||
|
|
||||||
|
fused_return_mapping = {1: (quant_node, 1), 2: (quant_node, 2)}
|
||||||
|
self.insert_fused_node(
|
||||||
|
fused_return_mapping,
|
||||||
|
epsilon=rms_node.kwargs["epsilon"],
|
||||||
|
scale_ub=None, # not used but required
|
||||||
|
residual=None, # not used but required
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FusedAddRMSNormDynamicQuantPattern(RMSNormQuantPattern):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
epsilon: float,
|
||||||
|
quant_dtype: torch.dtype,
|
||||||
|
per_tensor: bool = True,
|
||||||
|
symmetric=True):
|
||||||
|
key = FusedRMSQuantKey(fused_add=True,
|
||||||
|
quant=QuantKey(dtype=quant_dtype,
|
||||||
|
static=False,
|
||||||
|
per_tensor=per_tensor,
|
||||||
|
symmetric=symmetric))
|
||||||
|
super().__init__(epsilon, key)
|
||||||
|
|
||||||
|
def register(self, pm_pass: PatternMatcherPass,
|
||||||
|
record_match: Callable[[MultiOutputMatch], bool]):
|
||||||
|
|
||||||
|
def pattern(result: torch.Tensor, input: torch.Tensor,
|
||||||
|
residual: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(RMS_ADD_OP,
|
||||||
|
input=input,
|
||||||
|
residual=residual,
|
||||||
|
weight=weight,
|
||||||
|
epsilon=self.epsilon)
|
||||||
|
at1 = auto_functionalized(self.QUANT_OP,
|
||||||
|
result=result,
|
||||||
|
input=at[1],
|
||||||
|
scale=scale,
|
||||||
|
scale_ub=None)
|
||||||
|
|
||||||
|
# result, residual, scale
|
||||||
|
return at1[1], at[2], at1[2]
|
||||||
|
|
||||||
|
def replacement(result: torch.Tensor, input: torch.Tensor,
|
||||||
|
residual: torch.Tensor, weight: torch.Tensor,
|
||||||
|
scale: torch.Tensor):
|
||||||
|
at = auto_functionalized(self.FUSED_OP,
|
||||||
|
result=result,
|
||||||
|
input=input,
|
||||||
|
weight=weight,
|
||||||
|
scale=scale,
|
||||||
|
epsilon=self.epsilon,
|
||||||
|
scale_ub=None,
|
||||||
|
residual=residual)
|
||||||
|
|
||||||
|
# result, residual, scale
|
||||||
|
return at[1], at[3], at[2]
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
torch.empty(5, 4, device="cuda", dtype=self.quant_dtype), # result
|
||||||
|
empty_bf16(5, 4), # input
|
||||||
|
empty_bf16(5, 4), # residual
|
||||||
|
empty_bf16(1, 5), # weight
|
||||||
|
empty_fp32(1, 1) # scale
|
||||||
|
]
|
||||||
|
|
||||||
|
pm.register_replacement(
|
||||||
|
pattern,
|
||||||
|
replacement,
|
||||||
|
inputs,
|
||||||
|
pm.fwd_only,
|
||||||
|
pm_pass,
|
||||||
|
extra_check=lambda m: record_match(
|
||||||
|
self.Match(m, self.QUANT_OP, self.FUSED_OP)))
|
||||||
|
|
||||||
|
class Match(QuantMultiOutputMatch):
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
# Find the nodes in the match that we need to rebind
|
||||||
|
rms_node = self.find_auto_fn(RMS_ADD_OP)
|
||||||
|
quant_node = self.find_auto_fn(self.QUANT_OP)
|
||||||
|
|
||||||
|
assert len(rms_node.users) == 2
|
||||||
|
assert len(quant_node.users) == 2
|
||||||
|
|
||||||
|
# First, insert a new auto_functionalized node for the fused op,
|
||||||
|
# as well as getitem nodes to extract result, scale, and residual.
|
||||||
|
# The auto_fn node returns a tuple (None, result, scale, residual).
|
||||||
|
#
|
||||||
|
# The resulting graph looks like this:
|
||||||
|
# at = auto_functionalized(torch.ops._C.rms_norm_dynamic_per_token_quant.default, ...) # noqa
|
||||||
|
# result_node_new = at[1]
|
||||||
|
# scale_node_new = at[2]
|
||||||
|
# residual_node_new = at[3]
|
||||||
|
with self.inserting_after_match():
|
||||||
|
# Missing epsilon, scalars cannot be inputs to the pattern
|
||||||
|
kwargs = self.match.kwargs.copy()
|
||||||
|
|
||||||
|
fused_return_mapping = {
|
||||||
|
1: (quant_node, 1), # result
|
||||||
|
2: (quant_node, 2), # scale
|
||||||
|
3: (rms_node, 2), # residual
|
||||||
|
}
|
||||||
|
self.insert_fused_node(
|
||||||
|
fused_return_mapping,
|
||||||
|
epsilon=rms_node.kwargs["epsilon"],
|
||||||
|
scale_ub=None, # not used but required
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FusionPass(VllmInductorPass):
|
||||||
|
"""
|
||||||
|
This pass fuses a pre-defined set of custom ops into fused ops.
|
||||||
|
It uses the torch pattern matcher to find the patterns and replace them.
|
||||||
|
It also manually processes multi-output matches, as those are broken in
|
||||||
|
the torch pattern matcher.
|
||||||
|
|
||||||
|
Because patterns can only be registered once, the pass is a singleton.
|
||||||
|
This will be addressed in a future version of PyTorch:
|
||||||
|
https://github.com/pytorch/pytorch/pull/139321#issuecomment-2452354980
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance: 'Optional[FusionPass]' = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def instance(cls, config: CompilationConfig.PassConfig):
|
||||||
|
"""
|
||||||
|
Get the singleton instance of the FusionPass.
|
||||||
|
If the instance exists, the config is updated but
|
||||||
|
initialization is not repeated.
|
||||||
|
"""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = FusionPass(config)
|
||||||
|
else:
|
||||||
|
cls._instance.config = config
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self, config: CompilationConfig.PassConfig):
|
||||||
|
assert self.__class__._instance is None, \
|
||||||
|
"FusionPass singleton instance already exists"
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
self.matches: List[MultiOutputMatch] = []
|
||||||
|
self.patterns: PatternMatcherPass = PatternMatcherPass(
|
||||||
|
pass_name="fusion_pass")
|
||||||
|
|
||||||
|
for epsilon in [1e-5, 1e-6]:
|
||||||
|
# Fuse rms_norm + static fp8 quant
|
||||||
|
RMSNormStaticQuantPattern(epsilon,
|
||||||
|
FP8_DTYPE).register(self.patterns)
|
||||||
|
|
||||||
|
# Matches for patterns below have 2 or more outputs,
|
||||||
|
# so we need to process them manually (see process_matches)
|
||||||
|
|
||||||
|
# Fuse rms_norm + static fp8 quant
|
||||||
|
FusedAddRMSNormStaticQuantPattern(epsilon, FP8_DTYPE).register(
|
||||||
|
self.patterns, self.record_match)
|
||||||
|
|
||||||
|
# Fuse rms_norm + dynamic per-token fp8 quant
|
||||||
|
RMSNormDynamicQuantPattern(epsilon, FP8_DTYPE,
|
||||||
|
per_tensor=False).register(
|
||||||
|
self.patterns, self.record_match)
|
||||||
|
|
||||||
|
# Fuse fused_add_rms_norm + dynamic per-token fp8 quant
|
||||||
|
FusedAddRMSNormDynamicQuantPattern(epsilon,
|
||||||
|
FP8_DTYPE,
|
||||||
|
per_tensor=False).register(
|
||||||
|
self.patterns,
|
||||||
|
self.record_match)
|
||||||
|
|
||||||
|
# WARNING: This is a hack to clear the pattern matcher cache
|
||||||
|
# and allow multiple values of epsilon.
|
||||||
|
torch._inductor.pattern_matcher._seen_patterns.clear()
|
||||||
|
|
||||||
|
def record_match(self, match: MultiOutputMatch) -> bool:
|
||||||
|
# Hijack the extra_check to record the match and
|
||||||
|
# save it for post-processing.
|
||||||
|
self.matches.append(match)
|
||||||
|
|
||||||
|
# Return False to prevent automatic replacement.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_matches(self, graph: fx.Graph):
|
||||||
|
"""
|
||||||
|
Manually process multi-output matches and replace them with fused nodes.
|
||||||
|
See MultiOutputMatch for more details.
|
||||||
|
"""
|
||||||
|
for match in self.matches:
|
||||||
|
match.process()
|
||||||
|
|
||||||
|
# Finally, remove matched nodes
|
||||||
|
graph.eliminate_dead_code()
|
||||||
|
assert all(node not in graph.nodes for match in self.matches
|
||||||
|
for node in match.match.nodes)
|
||||||
|
|
||||||
|
def __call__(self, graph: fx.Graph):
|
||||||
|
self.begin()
|
||||||
|
self.dump_graph(graph, "before_fusion")
|
||||||
|
|
||||||
|
count = self.patterns.apply(graph)
|
||||||
|
logger.debug("Replaced %s patterns", count)
|
||||||
|
self.dump_graph(graph, "after_pattern_match")
|
||||||
|
|
||||||
|
# Manually process multi-output matches (and run DCE)
|
||||||
|
self.process_matches(graph)
|
||||||
|
logger.debug("Post-processed %s matches", len(self.matches))
|
||||||
|
self.dump_graph(graph, "after_fusion")
|
||||||
|
self.matches.clear()
|
||||||
|
self.end_and_log()
|
||||||
44
vllm/compilation/fx_utils.py
Normal file
44
vllm/compilation/fx_utils.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import operator
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from torch import fx
|
||||||
|
from torch._higher_order_ops.auto_functionalize import auto_functionalized
|
||||||
|
from torch._ops import OpOverload
|
||||||
|
|
||||||
|
|
||||||
|
def is_func(node: fx.Node, target) -> bool:
|
||||||
|
return node.op == "call_function" and node.target == target
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the first auto_functionalized node with the given op (if it exists)
|
||||||
|
def find_auto_fn_maybe(nodes: Iterable[fx.Node],
|
||||||
|
op: OpOverload) -> Optional[fx.Node]:
|
||||||
|
for node in nodes:
|
||||||
|
if is_func(node, auto_functionalized) and node.args[0] == op: # noqa
|
||||||
|
return node
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the first auto_functionalized node with the given op
|
||||||
|
def find_auto_fn(nodes: Iterable[fx.Node], op: OpOverload) -> fx.Node:
|
||||||
|
node = find_auto_fn_maybe(nodes, op)
|
||||||
|
assert node is not None, f"Could not find {op} in nodes {nodes}"
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the getitem node that extracts the idx-th element from node
|
||||||
|
# (if it exists)
|
||||||
|
def find_getitem_maybe(node: fx.Node, idx: int) -> Optional[fx.Node]:
|
||||||
|
for user in node.users:
|
||||||
|
if is_func(user, operator.getitem) and user.args[1] == idx:
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the getitem node that extracts the idx-th element from node
|
||||||
|
def find_getitem(node: fx.Node, idx: int) -> fx.Node:
|
||||||
|
ret = find_getitem_maybe(node, idx)
|
||||||
|
assert ret is not None, f"Could not find getitem {idx} in node {node}"
|
||||||
|
return ret
|
||||||
82
vllm/compilation/inductor_pass.py
Normal file
82
vllm/compilation/inductor_pass.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import importlib.metadata
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import types
|
||||||
|
from typing import Any, Callable, Dict, Optional, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from packaging.version import Version
|
||||||
|
from torch import fx
|
||||||
|
|
||||||
|
if Version(importlib.metadata.version('torch')) >= Version("2.6"):
|
||||||
|
from torch._inductor.custom_graph_pass import CustomGraphPass
|
||||||
|
else:
|
||||||
|
# CustomGraphPass is not present in 2.5 or lower, import our version
|
||||||
|
from .torch25_custom_graph_pass import ( # noqa: yapf
|
||||||
|
Torch25CustomGraphPass as CustomGraphPass)
|
||||||
|
|
||||||
|
|
||||||
|
class InductorPass(CustomGraphPass):
|
||||||
|
"""
|
||||||
|
A custom graph pass that uses a hash of its source as the UUID.
|
||||||
|
This is defined as a convenience and should work in most cases.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def uuid(self) -> Any:
|
||||||
|
"""
|
||||||
|
Provide a unique identifier for the pass, used in Inductor code cache.
|
||||||
|
This should depend on the pass implementation, so that changes to the
|
||||||
|
pass result in recompilation.
|
||||||
|
By default, the object source is hashed.
|
||||||
|
"""
|
||||||
|
return InductorPass.hash_source(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hash_source(*srcs: Union[str, Any]):
|
||||||
|
"""
|
||||||
|
Utility method to hash the sources of functions or objects.
|
||||||
|
:param srcs: strings or objects to add to the hash.
|
||||||
|
Objects and functions have their source inspected.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for src in srcs:
|
||||||
|
if isinstance(src, str):
|
||||||
|
src_str = src
|
||||||
|
elif isinstance(src, types.FunctionType):
|
||||||
|
src_str = inspect.getsource(src)
|
||||||
|
else:
|
||||||
|
src_str = inspect.getsource(src.__class__)
|
||||||
|
hasher.update(src_str.encode("utf-8"))
|
||||||
|
return hasher.hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hash_dict(dict_: Dict[Any, Any]):
|
||||||
|
"""
|
||||||
|
Utility method to hash a dictionary, can alternatively be used for uuid.
|
||||||
|
:return: A sha256 hash of the json rep of the dictionary.
|
||||||
|
"""
|
||||||
|
encoded = json.dumps(dict_, sort_keys=True).encode("utf-8")
|
||||||
|
return hashlib.sha256(encoded).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class CallableInductorPass(InductorPass):
|
||||||
|
"""
|
||||||
|
This class is a wrapper for a callable that automatically provides an
|
||||||
|
implementation of the UUID.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
callable: Callable[[fx.Graph], None],
|
||||||
|
uuid: Optional[Any] = None):
|
||||||
|
self.callable = callable
|
||||||
|
self._uuid = self.hash_source(callable) if uuid is None else uuid
|
||||||
|
|
||||||
|
def __call__(self, graph: torch.fx.Graph):
|
||||||
|
self.callable(graph)
|
||||||
|
|
||||||
|
def uuid(self) -> Any:
|
||||||
|
return self._uuid
|
||||||
38
vllm/compilation/monitor.py
Normal file
38
vllm/compilation/monitor.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from vllm.config import CompilationConfig, CompilationLevel, VllmConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
context_manager = None
|
||||||
|
torch_compile_start_time: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def start_monitoring_torch_compile(vllm_config: VllmConfig):
|
||||||
|
global torch_compile_start_time
|
||||||
|
torch_compile_start_time = time.time()
|
||||||
|
|
||||||
|
compilation_config: CompilationConfig = vllm_config.compilation_config
|
||||||
|
if compilation_config.level == CompilationLevel.PIECEWISE and \
|
||||||
|
compilation_config.debug_dump_path:
|
||||||
|
import depyf
|
||||||
|
path = os.path.join(compilation_config.debug_dump_path,
|
||||||
|
f"rank_{vllm_config.parallel_config.rank}")
|
||||||
|
global context_manager
|
||||||
|
context_manager = depyf.prepare_debug(path)
|
||||||
|
context_manager.__enter__()
|
||||||
|
|
||||||
|
|
||||||
|
def end_monitoring_torch_compile(vllm_config: VllmConfig):
|
||||||
|
compilation_config: CompilationConfig = vllm_config.compilation_config
|
||||||
|
if compilation_config.level == CompilationLevel.PIECEWISE:
|
||||||
|
logger.info("torch.compile takes %.2f s in total",
|
||||||
|
compilation_config.compilation_time)
|
||||||
|
global context_manager
|
||||||
|
if context_manager is not None:
|
||||||
|
context_manager.__exit__(None, None, None)
|
||||||
|
context_manager = None
|
||||||
108
vllm/compilation/multi_output_match.py
Normal file
108
vllm/compilation/multi_output_match.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import operator
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Iterable, List, Tuple
|
||||||
|
|
||||||
|
from torch import fx
|
||||||
|
from torch._higher_order_ops.auto_functionalize import auto_functionalized
|
||||||
|
from torch._inductor import pattern_matcher as pm
|
||||||
|
from torch._ops import OpOverload
|
||||||
|
from torch.fx import Node
|
||||||
|
|
||||||
|
from vllm.compilation.fx_utils import find_auto_fn
|
||||||
|
|
||||||
|
|
||||||
|
class MultiOutputMatch(abc.ABC):
|
||||||
|
"""
|
||||||
|
This class provides utilities to process multi-output matches and
|
||||||
|
manually insert replacements.
|
||||||
|
|
||||||
|
This is necessary because the automatic replacement for multi-output
|
||||||
|
matches is broken: https://github.com/pytorch/pytorch/issues/137280
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, match: pm.Match):
|
||||||
|
self.match = match
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process(self):
|
||||||
|
"""
|
||||||
|
Process a multi-output match and manually insert the replacement.
|
||||||
|
|
||||||
|
This method should:
|
||||||
|
1. Insert the replacement nodes after the last node in the match.
|
||||||
|
2. Rebind the users of nodes in the match to use the new nodes.
|
||||||
|
3. Set meta["val"] for de-functionalization.
|
||||||
|
|
||||||
|
The result of an auto-functionalized node is a tuple of tensors.
|
||||||
|
The first element is the return value of the function, usually None.
|
||||||
|
The remaining elements are the mutated args of the function.
|
||||||
|
|
||||||
|
All auto-functionalized nodes must contain a proper meta["val"],
|
||||||
|
as it is used by de-functionalization. meta["val"] has to contain the
|
||||||
|
value of the node (tuple of tensors) that would be returned by the
|
||||||
|
functionalized node during tracing.
|
||||||
|
|
||||||
|
Existing nodes in the graph all have this property set, but we have
|
||||||
|
to set it manually for new nodes we insert.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
# op schema: foo(a: Tensor!, b: Tensor, c: Tensor!) -> None
|
||||||
|
at = auto_functionalized(torch.ops._C.foo.default, a, b, c)
|
||||||
|
# at.meta["val"] = (None, a, c)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nodes(self) -> List[fx.Node]:
|
||||||
|
return self.match.nodes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def graph(self) -> fx.Graph:
|
||||||
|
return self.match.graph
|
||||||
|
|
||||||
|
def find_auto_fn(self, op) -> fx.Node:
|
||||||
|
"""
|
||||||
|
Find the first auto_functionalized node with the given op in the match.
|
||||||
|
"""
|
||||||
|
return find_auto_fn(self.nodes, op)
|
||||||
|
|
||||||
|
def inserting_after_match(self):
|
||||||
|
"""
|
||||||
|
Insert nodes after the last node in the match.
|
||||||
|
This is done to avoid use-before-definition errors after inserting
|
||||||
|
replacement nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# match.nodes is not guaranteed to be sorted.
|
||||||
|
# Find the last node in the match.
|
||||||
|
for last_node_in_match in reversed(self.graph.nodes):
|
||||||
|
if last_node_in_match in self.match.nodes:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("No nodes in graph")
|
||||||
|
|
||||||
|
return self.graph.inserting_after(last_node_in_match)
|
||||||
|
|
||||||
|
def insert_getitems(self, tuple_node: fx.Node,
|
||||||
|
indices: Iterable[int]) -> Tuple[fx.Node, ...]:
|
||||||
|
"""
|
||||||
|
Insert operator.getitem nodes to extract elements from a tuple node.
|
||||||
|
|
||||||
|
:param tuple_node: The tuple node to extract elements from.
|
||||||
|
:param indices: The indices of the elements to extract.
|
||||||
|
:return: Tuple of the new getitem nodes, corresponding to the indices.
|
||||||
|
"""
|
||||||
|
with self.graph.inserting_after(tuple_node):
|
||||||
|
return tuple(
|
||||||
|
self.graph.call_function(operator.getitem, (tuple_node, idx))
|
||||||
|
for idx in indices)
|
||||||
|
|
||||||
|
def insert_auto_fn(self, op: OpOverload, kwargs) -> Node:
|
||||||
|
"""
|
||||||
|
Insert an auto_functionalized node with the given op and kwargs.
|
||||||
|
"""
|
||||||
|
return self.graph.call_function(auto_functionalized, (op, ),
|
||||||
|
kwargs=kwargs)
|
||||||
135
vllm/compilation/noop_elimination.py
Normal file
135
vllm/compilation/noop_elimination.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Iterable, Union
|
||||||
|
|
||||||
|
import torch.fx
|
||||||
|
from torch import SymInt
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
from .fx_utils import is_func
|
||||||
|
from .vllm_inductor_pass import VllmInductorPass
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoOpEliminationPass(VllmInductorPass):
|
||||||
|
"""
|
||||||
|
This is an inductor pass that removes redundant reshape/slice operations.
|
||||||
|
It is required for RMSNorm-quant fusion to work properly.
|
||||||
|
That's because apply_fp8_linear adds a reshape, which is redundant
|
||||||
|
in the 2D-case. Additionally, torch internal no-op elimination pass does
|
||||||
|
not handle certain slice variants.
|
||||||
|
|
||||||
|
Example graph 1:
|
||||||
|
getitem_1: "f16[s0, 4096]" = ...
|
||||||
|
view_1: "f16[s0, 4096]" = torch.reshape(getitem_1, [-1, 4096])
|
||||||
|
at = auto_functionalized(static_scaled_fp8_quant, input = view_1, ...)
|
||||||
|
out: "f8e4m3fn[s0, 4096]" = at[1]
|
||||||
|
|
||||||
|
Can be replaced with:
|
||||||
|
getitem_1: "f16[s0, 4096]" = ...
|
||||||
|
at = auto_functionalized(static_scaled_fp8_quant, input = getitem_1, ...)
|
||||||
|
out: "f8e4m3fn[s0, 4096]" = at[1]
|
||||||
|
|
||||||
|
Example graph 2:
|
||||||
|
arg0: "s0" = SymInt(s0)
|
||||||
|
scaled_mm: "f16[s0, 4096]" = ...
|
||||||
|
slice_1: "f16[s0, 4096]" = torch.slice(scaled_mm, -1, 0, arg0)
|
||||||
|
at = auto_functionalized(fused_add_rms_norm, input = slice_1, ...)
|
||||||
|
out: "f16[s0, 4096]" = torch.slice_scatter(scaled_mm, at[1], 0, 0, arg0)
|
||||||
|
|
||||||
|
Can be replaced with:
|
||||||
|
arg0: "s0" = SymInt(s0)
|
||||||
|
scaled_mm: "f16[s0, 4096]" = ...
|
||||||
|
at = auto_functionalized(fused_add_rms_norm, input = scaled_mm, ...)
|
||||||
|
out: "f16[s0, 4096]" = at[1]
|
||||||
|
|
||||||
|
TODO(luka): This is currently tested in test_fusion,
|
||||||
|
but separate tests could be good.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, graph: torch.fx.Graph):
|
||||||
|
self.begin()
|
||||||
|
self.dump_graph(graph, "before_noop_elimination")
|
||||||
|
count = 0
|
||||||
|
# Remove no-op reshapes/views:
|
||||||
|
for node in graph.nodes:
|
||||||
|
if is_func(node, torch.ops.aten.reshape.default):
|
||||||
|
input, shape = node.args[:2]
|
||||||
|
input_shape = input.meta["val"].shape
|
||||||
|
if len(shape) != len(input_shape):
|
||||||
|
# Reshape changing rank, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
if shape.count(-1) > 1:
|
||||||
|
# Invalid reshape args, skip
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.all_dims_equivalent(shape, input_shape):
|
||||||
|
node.replace_all_uses_with(input)
|
||||||
|
graph.erase_node(node)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
elif is_func(node, torch.ops.aten.slice.Tensor):
|
||||||
|
input, dim_index, start, end = node.args[:4]
|
||||||
|
input_shape = input.meta["val"].shape
|
||||||
|
i_dim = input_shape[dim_index]
|
||||||
|
|
||||||
|
if start == 0 and self.dims_equivalent(end, i_dim):
|
||||||
|
node.replace_all_uses_with(input)
|
||||||
|
graph.erase_node(node)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
elif is_func(node, torch.ops.aten.slice_scatter.default):
|
||||||
|
base, view, dim_index, start, end = node.args[:5]
|
||||||
|
base_shape = base.meta["val"].shape
|
||||||
|
view_shape = view.meta["val"].shape
|
||||||
|
|
||||||
|
view_dim = view_shape[dim_index]
|
||||||
|
|
||||||
|
# Check that view fully covers base and the full view is used
|
||||||
|
# (if the view fully covered the base after slicing but was not
|
||||||
|
# fully used, we could replace slice_scatter with a simple slice
|
||||||
|
# but that's a niche case).
|
||||||
|
if (base_shape == view_shape and start == 0
|
||||||
|
and self.dims_equivalent(end, view_dim)):
|
||||||
|
node.replace_all_uses_with(view)
|
||||||
|
graph.erase_node(node)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
logger.debug("Removed %s no-op reshapes and slices", count)
|
||||||
|
self.dump_graph(graph, "after_noop_elimination")
|
||||||
|
self.end_and_log()
|
||||||
|
|
||||||
|
def all_dims_equivalent(self, dims: Iterable[Union[int, torch.fx.Node]],
|
||||||
|
i_dims: Iterable[Union[int, SymInt]]):
|
||||||
|
return all(
|
||||||
|
self.dims_equivalent(s, i_s) for s, i_s in zip(dims, i_dims))
|
||||||
|
|
||||||
|
def dims_equivalent(self, dim: Union[int, torch.fx.Node],
|
||||||
|
i_dim: Union[int, SymInt]) -> bool:
|
||||||
|
"""
|
||||||
|
This function checks if two dimensions are equivalent.
|
||||||
|
:param dim: The dimension arg to reshape/slice
|
||||||
|
:param i_dim: The corresponding dimension in the input tensor
|
||||||
|
:return: Are the dimensions equivalent?
|
||||||
|
|
||||||
|
There are three cases in which the dimensions are equivalent:
|
||||||
|
1. The dimensions are equal (both integers)
|
||||||
|
2. The reshape dimension is -1 (i.e. inferred)
|
||||||
|
3. The dimensions both correspond to the same SymInt
|
||||||
|
|
||||||
|
While case 2 does not guarantee the dimensions are equal,
|
||||||
|
they are equal if all other dimensions are equal.
|
||||||
|
|
||||||
|
In case 3, the reshape dimension is a torch.fx.Node,
|
||||||
|
and its value is a SymInt. That value is equal to the
|
||||||
|
input dimension.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Case 1 and 2
|
||||||
|
if dim == i_dim or dim == -1:
|
||||||
|
return True
|
||||||
|
# Case 3
|
||||||
|
return isinstance(dim, torch.fx.Node) and dim.meta["val"] == i_dim
|
||||||
67
vllm/compilation/pass_manager.py
Normal file
67
vllm/compilation/pass_manager.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from torch import fx as fx
|
||||||
|
|
||||||
|
from vllm.config import CompilationConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
from .fix_functionalization import FixFunctionalizationPass
|
||||||
|
from .fusion import FusionPass
|
||||||
|
from .inductor_pass import CustomGraphPass, InductorPass
|
||||||
|
from .noop_elimination import NoOpEliminationPass
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PostGradPassManager(CustomGraphPass):
|
||||||
|
"""
|
||||||
|
The pass manager for post-grad passes.
|
||||||
|
It handles configuration, adding custom passes, and running passes.
|
||||||
|
It supports uuid for the Inductor code cache. That includes torch<2.6
|
||||||
|
support using pickling (in .inductor_pass.CustomGraphPass).
|
||||||
|
|
||||||
|
The order of the post-grad post-passes is:
|
||||||
|
1. passes (constructor parameter)
|
||||||
|
2. default passes (NoopEliminationPass, FusionPass)
|
||||||
|
3. config["post_grad_custom_post_pass"] (if it exists)
|
||||||
|
4. fix_functionalization
|
||||||
|
This way, all passes operate on a functionalized graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.passes: List[InductorPass] = []
|
||||||
|
|
||||||
|
def __call__(self, graph: fx.Graph):
|
||||||
|
for pass_ in self.passes:
|
||||||
|
pass_(graph)
|
||||||
|
|
||||||
|
# always run fix_functionalization last
|
||||||
|
self.fix_functionalization(graph)
|
||||||
|
|
||||||
|
def configure(self, pass_config: CompilationConfig.PassConfig):
|
||||||
|
self.pass_config = pass_config
|
||||||
|
if pass_config.enable_noop:
|
||||||
|
self.passes += [NoOpEliminationPass(pass_config)]
|
||||||
|
|
||||||
|
if pass_config.enable_fusion:
|
||||||
|
self.passes += [FusionPass.instance(pass_config)]
|
||||||
|
|
||||||
|
self.fix_functionalization = FixFunctionalizationPass(pass_config)
|
||||||
|
|
||||||
|
def add(self, pass_: InductorPass):
|
||||||
|
assert isinstance(pass_, InductorPass)
|
||||||
|
self.passes.append(pass_)
|
||||||
|
|
||||||
|
def uuid(self):
|
||||||
|
"""
|
||||||
|
The PostGradPassManager is set as a custom pass in the Inductor and
|
||||||
|
affects compilation caching. Its uuid depends on the UUIDs of all
|
||||||
|
dependent passes and the pass config. See InductorPass for more info.
|
||||||
|
"""
|
||||||
|
state = {"pass_config": self.pass_config.uuid(), "passes": []}
|
||||||
|
for pass_ in self.passes:
|
||||||
|
state["passes"].append(pass_.uuid())
|
||||||
|
state["passes"].append(self.fix_functionalization.uuid())
|
||||||
|
return InductorPass.hash_dict(state)
|
||||||
41
vllm/compilation/torch25_custom_graph_pass.py
Normal file
41
vllm/compilation/torch25_custom_graph_pass.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
|
||||||
|
class Torch25CustomGraphPass(ABC): # noqa (redefinition)
|
||||||
|
"""
|
||||||
|
This class replaces CustomGraphPass from torch==2.6 when using torch<2.6.
|
||||||
|
It conforms to the 2.6 interface but also supports pickling, as that's what
|
||||||
|
the inductor code cache uses to determine the cache key before 2.6.
|
||||||
|
(in 2.6 and above, uuid() is used.)
|
||||||
|
|
||||||
|
Subclasses can just "pretend" that uuid is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __call__(self, graph: torch.fx.graph.Graph) -> None:
|
||||||
|
"""
|
||||||
|
Implementation of the custom pass.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def uuid(self) -> Optional[Any]:
|
||||||
|
"""
|
||||||
|
Return an ID to uniquely identify your custom pass implementation.
|
||||||
|
Return None to skip inductor code caching entirely.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
"""
|
||||||
|
Pickling is used instead of uuid() in torch<2.6. Just return uuid()
|
||||||
|
to enable subclasses to only have to implement uuid.
|
||||||
|
"""
|
||||||
|
return self.uuid()
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
raise ValueError("Cannot unpickle CustomGraphPass because pickling"
|
||||||
|
" is used for cache key uuid. Use torch>=2.6 with"
|
||||||
|
" native uuid support for custom passes.")
|
||||||
65
vllm/compilation/vllm_inductor_pass.py
Normal file
65
vllm/compilation/vllm_inductor_pass.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.config import CompilationConfig
|
||||||
|
# yapf: disable
|
||||||
|
from vllm.distributed import get_tensor_model_parallel_rank as get_tp_rank
|
||||||
|
from vllm.distributed import (
|
||||||
|
get_tensor_model_parallel_world_size as get_tp_world_size)
|
||||||
|
from vllm.distributed import model_parallel_is_initialized as p_is_init
|
||||||
|
# yapf: enable
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
from .inductor_pass import InductorPass
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class VllmInductorPass(InductorPass):
|
||||||
|
"""
|
||||||
|
An inductor pass with access to vLLM PassConfig.
|
||||||
|
It provides timing, logging, and dumping utilities.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: CompilationConfig.PassConfig):
|
||||||
|
self.config = config
|
||||||
|
self.pass_name = self.__class__.__name__
|
||||||
|
|
||||||
|
def dump_graph(self, graph: torch.fx.Graph, stage: str, always=False):
|
||||||
|
if stage in self.config.dump_graph_stages or always:
|
||||||
|
# Make sure filename includes rank in the distributed setting
|
||||||
|
parallel = p_is_init() and get_tp_world_size() > 1
|
||||||
|
rank = f"-{get_tp_rank()}" if parallel else ""
|
||||||
|
filepath = self.config.dump_graph_dir / f"{stage}{rank}.py"
|
||||||
|
|
||||||
|
logger.info("%s printing graph to %s", self.pass_name, filepath)
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
src = graph.python_code(root_module="self", verbose=True).src
|
||||||
|
# Add imports so it's not full of errors
|
||||||
|
print("import torch; from torch import device", file=f)
|
||||||
|
print(src, file=f)
|
||||||
|
|
||||||
|
def begin(self):
|
||||||
|
self._start_time = time.perf_counter_ns()
|
||||||
|
|
||||||
|
def end_and_log(self):
|
||||||
|
self._end_time = time.perf_counter_ns()
|
||||||
|
duration_ms = float(self._end_time - self._start_time) / 1.0e6
|
||||||
|
logger.debug("%s completed in %.1f ms", self.pass_name, duration_ms)
|
||||||
|
|
||||||
|
|
||||||
|
class PrinterInductorPass(VllmInductorPass):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
name: str,
|
||||||
|
config: CompilationConfig.PassConfig,
|
||||||
|
always=False):
|
||||||
|
super().__init__(config)
|
||||||
|
self.name = name
|
||||||
|
self.always = always
|
||||||
|
|
||||||
|
def __call__(self, graph: torch.fx.Graph):
|
||||||
|
self.dump_graph(graph, self.name, always=self.always)
|
||||||
129
vllm/compilation/wrapper.py
Normal file
129
vllm/compilation/wrapper.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from abc import abstractmethod
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from types import CodeType
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.config import CompilationLevel, get_current_vllm_config
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TorchCompileWrapperWithCustomDispatcher:
|
||||||
|
"""
|
||||||
|
A wrapper class for torch.compile, with a custom dispatch logic.
|
||||||
|
Subclasses should:
|
||||||
|
1. Implement the forward method
|
||||||
|
2. Implement the dispatch logic in the __call__ method
|
||||||
|
It can use `self.compiled_codes` to access the compiled bytecode,
|
||||||
|
and `with self.dispatch_to_code(index):` to dispatch to
|
||||||
|
the compiled code.
|
||||||
|
3. Implement the `__init__` method to determine how to call
|
||||||
|
`torch.compile` over the forward method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
compiled_callable: Optional[Callable] = None,
|
||||||
|
compilation_level: int = 0):
|
||||||
|
|
||||||
|
vllm_config = get_current_vllm_config()
|
||||||
|
self.vllm_config = vllm_config
|
||||||
|
if compiled_callable is None:
|
||||||
|
# default compilation settings
|
||||||
|
# compiling the forward method
|
||||||
|
|
||||||
|
backend = vllm_config.compilation_config.init_backend(vllm_config)
|
||||||
|
|
||||||
|
compiled_callable = torch.compile(
|
||||||
|
self.forward,
|
||||||
|
fullgraph=envs.VLLM_TEST_DYNAMO_FULLGRAPH_CAPTURE,
|
||||||
|
backend=backend)
|
||||||
|
|
||||||
|
self.compiled_callable = compiled_callable
|
||||||
|
self.original_code_object = self.__class__.forward.__code__
|
||||||
|
self.compiled_codes: List[CodeType] = []
|
||||||
|
torch._dynamo.convert_frame.register_bytecode_hook(self.bytecode_hook)
|
||||||
|
|
||||||
|
# read the env var to determine whether to use the custom dispatcher
|
||||||
|
# subclasses can use this to switch between the custom dispatcher
|
||||||
|
# and the default Dynamo guard mechanism.
|
||||||
|
self.use_custom_dispatcher: bool = \
|
||||||
|
compilation_level >= CompilationLevel.DYNAMO_ONCE
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
"""Implement the dispatch logic here, beyond the torch.compile level.
|
||||||
|
NOTE: this function can have additional arguments beyond the forward
|
||||||
|
method, for directly dispatching to the compiled code.
|
||||||
|
"""
|
||||||
|
return self.compiled_callable(*args, **kwargs)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def forward(self, *args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
def bytecode_hook(self, old_code: CodeType, new_code: CodeType):
|
||||||
|
"""Hook to save the compiled bytecode for direct execution."""
|
||||||
|
if old_code is not self.original_code_object:
|
||||||
|
return
|
||||||
|
# code borrowed from https://github.com/thuml/depyf/blob/f4ad79fadee27ea113b4c75202db1eb1a11c0dbc/depyf/explain/enable_debugging.py#L25
|
||||||
|
frame = sys._getframe()
|
||||||
|
while frame and frame.f_back:
|
||||||
|
frame = frame.f_back
|
||||||
|
code_name = frame.f_code.co_name
|
||||||
|
file_name = frame.f_code.co_filename.split(os.path.sep)[-1]
|
||||||
|
if code_name == "_compile" and file_name == "convert_frame.py":
|
||||||
|
break
|
||||||
|
frame = frame.f_locals["frame"]
|
||||||
|
assert frame.f_code == old_code
|
||||||
|
|
||||||
|
if frame.f_locals["self"] is not self:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.compiled_codes.append(new_code)
|
||||||
|
local_cache_dir = self.vllm_config.compilation_config.local_cache_dir
|
||||||
|
if isinstance(local_cache_dir, str):
|
||||||
|
decompiled_file = os.path.join(local_cache_dir,
|
||||||
|
"transformed_code.py")
|
||||||
|
if not os.path.exists(decompiled_file):
|
||||||
|
try:
|
||||||
|
# usually the decompilation will succeed for most models,
|
||||||
|
# as we guarantee a full-graph compilation in Dynamo.
|
||||||
|
# but there's no 100% guarantee, since decompliation is
|
||||||
|
# not a reversible process.
|
||||||
|
import depyf
|
||||||
|
src = depyf.decompile(new_code)
|
||||||
|
with open(decompiled_file, "w") as f:
|
||||||
|
f.write(src)
|
||||||
|
|
||||||
|
logger.debug("Dynamo transformed code saved to %s",
|
||||||
|
decompiled_file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.vllm_config.compilation_config.use_cudagraph and \
|
||||||
|
"update" in new_code.co_names:
|
||||||
|
import depyf
|
||||||
|
src = depyf.decompile(new_code)
|
||||||
|
msg = "Assigning / modifying buffers of nn.Module during forward pass is not allowed when using cudagraph inside the compiler because it will cause silent errors. Please use eager mode or fix the code. The following code contains clues about which buffer is being modified (please search for the usage of the function `update`):\n" + src # noqa
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def dispatch_to_code(self, index: int):
|
||||||
|
"""Context manager to dispatch to the compiled code.
|
||||||
|
Why does this work? Because Dynamo guarantees that the compiled
|
||||||
|
bytecode has exactly the same arguments, cell variables, and free
|
||||||
|
variables as the original code. Therefore we can directly switch
|
||||||
|
the code object in the function and call it.
|
||||||
|
|
||||||
|
See https://dev-discuss.pytorch.org/t/what-is-the-relationship-requirement-among-original-bytecode-transformed-bytecode-and-bytecode-returned-by-hooks-in-dynamo/1693/7 for more details.
|
||||||
|
""" # noqa
|
||||||
|
self.__class__.forward.__code__ = self.compiled_codes[index]
|
||||||
|
yield
|
||||||
|
self.__class__.forward.__code__ = self.original_code_object
|
||||||
3862
vllm/config.py
Normal file
3862
vllm/config.py
Normal file
File diff suppressed because it is too large
Load Diff
170
vllm/connections.py
Normal file
170
vllm/connections.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from collections.abc import Mapping, MutableMapping
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from vllm.version import __version__ as VLLM_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPConnection:
|
||||||
|
"""Helper class to send HTTP requests."""
|
||||||
|
|
||||||
|
def __init__(self, *, reuse_client: bool = True) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.reuse_client = reuse_client
|
||||||
|
|
||||||
|
self._sync_client: Optional[requests.Session] = None
|
||||||
|
self._async_client: Optional[aiohttp.ClientSession] = None
|
||||||
|
|
||||||
|
def get_sync_client(self) -> requests.Session:
|
||||||
|
if self._sync_client is None or not self.reuse_client:
|
||||||
|
self._sync_client = requests.Session()
|
||||||
|
|
||||||
|
return self._sync_client
|
||||||
|
|
||||||
|
# NOTE: We intentionally use an async function even though it is not
|
||||||
|
# required, so that the client is only accessible inside async event loop
|
||||||
|
async def get_async_client(self) -> aiohttp.ClientSession:
|
||||||
|
if self._async_client is None or not self.reuse_client:
|
||||||
|
self._async_client = aiohttp.ClientSession(trust_env=True)
|
||||||
|
|
||||||
|
return self._async_client
|
||||||
|
|
||||||
|
def _validate_http_url(self, url: str):
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
|
||||||
|
if parsed_url.scheme not in ("http", "https"):
|
||||||
|
raise ValueError("Invalid HTTP URL: A valid HTTP URL "
|
||||||
|
"must have scheme 'http' or 'https'.")
|
||||||
|
|
||||||
|
def _headers(self, **extras: str) -> MutableMapping[str, str]:
|
||||||
|
return {"User-Agent": f"vLLM/{VLLM_VERSION}", **extras}
|
||||||
|
|
||||||
|
def get_response(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
stream: bool = False,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
extra_headers: Optional[Mapping[str, str]] = None,
|
||||||
|
):
|
||||||
|
self._validate_http_url(url)
|
||||||
|
|
||||||
|
client = self.get_sync_client()
|
||||||
|
extra_headers = extra_headers or {}
|
||||||
|
|
||||||
|
return client.get(url,
|
||||||
|
headers=self._headers(**extra_headers),
|
||||||
|
stream=stream,
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
async def get_async_response(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
extra_headers: Optional[Mapping[str, str]] = None,
|
||||||
|
):
|
||||||
|
self._validate_http_url(url)
|
||||||
|
|
||||||
|
client = await self.get_async_client()
|
||||||
|
extra_headers = extra_headers or {}
|
||||||
|
|
||||||
|
return client.get(url,
|
||||||
|
headers=self._headers(**extra_headers),
|
||||||
|
timeout=timeout)
|
||||||
|
|
||||||
|
def get_bytes(self, url: str, *, timeout: Optional[float] = None) -> bytes:
|
||||||
|
with self.get_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return r.content
|
||||||
|
|
||||||
|
async def async_get_bytes(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
) -> bytes:
|
||||||
|
async with await self.get_async_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return await r.read()
|
||||||
|
|
||||||
|
def get_text(self, url: str, *, timeout: Optional[float] = None) -> str:
|
||||||
|
with self.get_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
async def async_get_text(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
) -> str:
|
||||||
|
async with await self.get_async_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return await r.text()
|
||||||
|
|
||||||
|
def get_json(self, url: str, *, timeout: Optional[float] = None) -> str:
|
||||||
|
with self.get_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
async def async_get_json(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
) -> str:
|
||||||
|
async with await self.get_async_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
return await r.json()
|
||||||
|
|
||||||
|
def download_file(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
save_path: Path,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
chunk_size: int = 128,
|
||||||
|
) -> Path:
|
||||||
|
with self.get_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
with save_path.open("wb") as f:
|
||||||
|
for chunk in r.iter_content(chunk_size):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
async def async_download_file(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
save_path: Path,
|
||||||
|
*,
|
||||||
|
timeout: Optional[float] = None,
|
||||||
|
chunk_size: int = 128,
|
||||||
|
) -> Path:
|
||||||
|
async with await self.get_async_response(url, timeout=timeout) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
with save_path.open("wb") as f:
|
||||||
|
async for chunk in r.content.iter_chunked(chunk_size):
|
||||||
|
f.write(chunk)
|
||||||
|
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
|
||||||
|
global_http_connection = HTTPConnection()
|
||||||
|
"""The global :class:`HTTPConnection` instance used by vLLM."""
|
||||||
0
vllm/core/__init__.py
Normal file
0
vllm/core/__init__.py
Normal file
0
vllm/core/block/__init__.py
Normal file
0
vllm/core/block/__init__.py
Normal file
398
vllm/core/block/block_table.py
Normal file
398
vllm/core/block/block_table.py
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from vllm.core.block.common import BlockList
|
||||||
|
from vllm.core.block.interfaces import Block, DeviceAwareBlockAllocator
|
||||||
|
from vllm.utils import Device, cdiv, chunk_list
|
||||||
|
|
||||||
|
|
||||||
|
class BlockTable:
|
||||||
|
"""A class to manage blocks for a specific sequence.
|
||||||
|
|
||||||
|
The BlockTable maps a sequence of tokens to a list of blocks, where each
|
||||||
|
block represents a contiguous memory allocation for a portion of the
|
||||||
|
sequence. The blocks are managed by a DeviceAwareBlockAllocator, which is
|
||||||
|
responsible for allocating and freeing memory for the blocks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block_size (int): The maximum number of tokens that can be stored in a
|
||||||
|
single block.
|
||||||
|
block_allocator (DeviceAwareBlockAllocator): The block allocator used to
|
||||||
|
manage memory for the blocks.
|
||||||
|
_blocks (Optional[List[Block]], optional): An optional list of existing
|
||||||
|
blocks to initialize the BlockTable with. If not provided, an empty
|
||||||
|
BlockTable is created.
|
||||||
|
max_block_sliding_window (Optional[int], optional): The number of
|
||||||
|
blocks to keep around for each sequence. If None, all blocks
|
||||||
|
are kept (eg., when sliding window is not used).
|
||||||
|
It should at least fit the sliding window size of the model.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_block_size (int): The maximum number of tokens that can be stored in a
|
||||||
|
single block.
|
||||||
|
_allocator (DeviceAwareBlockAllocator): The block allocator used to
|
||||||
|
manage memory for the blocks.
|
||||||
|
_blocks (Optional[List[Block]]): The list of blocks managed by this
|
||||||
|
BlockTable.
|
||||||
|
_num_full_slots (int): The number of tokens currently stored in the
|
||||||
|
blocks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
block_size: int,
|
||||||
|
block_allocator: DeviceAwareBlockAllocator,
|
||||||
|
_blocks: Optional[List[Block]] = None,
|
||||||
|
max_block_sliding_window: Optional[int] = None,
|
||||||
|
):
|
||||||
|
self._block_size = block_size
|
||||||
|
self._allocator = block_allocator
|
||||||
|
if _blocks is None:
|
||||||
|
_blocks = []
|
||||||
|
self._blocks: BlockList = BlockList(_blocks)
|
||||||
|
|
||||||
|
self._max_block_sliding_window = max_block_sliding_window
|
||||||
|
self._num_full_slots = self._get_num_token_ids()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_num_required_blocks(token_ids: List[int],
|
||||||
|
block_size: int,
|
||||||
|
num_lookahead_slots: int = 0) -> int:
|
||||||
|
"""Calculates the minimum number of blocks required to store a given
|
||||||
|
sequence of token IDs along with any look-ahead slots that may be
|
||||||
|
required (like in multi-step + chunked-prefill).
|
||||||
|
|
||||||
|
This assumes worst-case scenario, where every block requires a new
|
||||||
|
allocation (e.g. ignoring prefix caching).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids (List[int]): The sequence of token IDs to be stored.
|
||||||
|
block_size (int): The maximum number of tokens that can be stored in
|
||||||
|
a single block.
|
||||||
|
num_lookahead_slots (int): look-ahead slots that the sequence may
|
||||||
|
require.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The minimum number of blocks required to store the given
|
||||||
|
sequence of token IDs along with any required look-ahead slots.
|
||||||
|
"""
|
||||||
|
return cdiv(len(token_ids) + num_lookahead_slots, block_size)
|
||||||
|
|
||||||
|
def allocate(self,
|
||||||
|
token_ids: List[int],
|
||||||
|
device: Device = Device.GPU,
|
||||||
|
extra_hash: Optional[int] = None) -> None:
|
||||||
|
"""Allocates memory blocks for storing the given sequence of token IDs.
|
||||||
|
|
||||||
|
This method allocates the required number of blocks to store the given
|
||||||
|
sequence of token IDs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids (List[int]): The sequence of token IDs to be stored.
|
||||||
|
device (Device, optional): The device on which the blocks should be
|
||||||
|
allocated. Defaults to Device.GPU.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors, such as adapters, that influence the block hash
|
||||||
|
in the prefixcaching block.
|
||||||
|
"""
|
||||||
|
assert not self._is_allocated
|
||||||
|
assert token_ids
|
||||||
|
blocks = self._allocate_blocks_for_token_ids(prev_block=None,
|
||||||
|
token_ids=token_ids,
|
||||||
|
device=device,
|
||||||
|
extra_hash=extra_hash)
|
||||||
|
self.update(blocks)
|
||||||
|
self._num_full_slots = len(token_ids)
|
||||||
|
|
||||||
|
def update(self, blocks: List[Block]) -> None:
|
||||||
|
"""Resets the table to the newly provided blocks
|
||||||
|
(with their corresponding block ids)
|
||||||
|
"""
|
||||||
|
self._blocks.update(blocks)
|
||||||
|
|
||||||
|
def append_token_ids(self,
|
||||||
|
token_ids: List[int],
|
||||||
|
num_lookahead_slots: int = 0,
|
||||||
|
num_computed_slots: Optional[int] = None,
|
||||||
|
extra_hash: Optional[int] = None) -> None:
|
||||||
|
"""Appends a sequence of token IDs to the existing blocks in the
|
||||||
|
BlockTable.
|
||||||
|
|
||||||
|
This method appends the given sequence of token IDs to the existing
|
||||||
|
blocks in the BlockTable. If there is not enough space in the existing
|
||||||
|
blocks, new blocks are allocated using the `ensure_num_empty_slots`
|
||||||
|
method to accommodate the additional tokens.
|
||||||
|
|
||||||
|
The token IDs are divided into chunks of size `block_size` (except for
|
||||||
|
the first chunk, which may be smaller), and each chunk is appended to a
|
||||||
|
separate block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids (List[int]): The sequence of token IDs to be appended.
|
||||||
|
num_computed_slots (Optional[int]): The number of KV cache slots
|
||||||
|
that are already filled (computed).
|
||||||
|
When sliding window is enabled, this is used to compute how many
|
||||||
|
blocks to drop at the front of the sequence.
|
||||||
|
Without sliding window, None can be passed.
|
||||||
|
Without chunked prefill, it should be the same as
|
||||||
|
_num_full_slots.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors such as adapters that influence the block, apart
|
||||||
|
from the token_ids.
|
||||||
|
"""
|
||||||
|
assert self._is_allocated, "no blocks have been allocated"
|
||||||
|
assert len(self._blocks) > 0
|
||||||
|
|
||||||
|
# Drop blocks that are no longer needed due to sliding window
|
||||||
|
if self._max_block_sliding_window is not None:
|
||||||
|
null_block = self._allocator.allocate_or_get_null_block()
|
||||||
|
assert num_computed_slots is not None
|
||||||
|
end_block_idx = (num_computed_slots //
|
||||||
|
self._block_size) - self._max_block_sliding_window
|
||||||
|
for idx in range(0, end_block_idx):
|
||||||
|
b = self._blocks[idx]
|
||||||
|
if b is not null_block:
|
||||||
|
self._allocator.free(b)
|
||||||
|
self._blocks[idx] = null_block
|
||||||
|
|
||||||
|
# Ensure there are enough empty slots for the new tokens plus
|
||||||
|
# lookahead slots
|
||||||
|
self.ensure_num_empty_slots(num_empty_slots=len(token_ids) +
|
||||||
|
num_lookahead_slots,
|
||||||
|
extra_hash=extra_hash)
|
||||||
|
|
||||||
|
# Update the blocks with the new tokens
|
||||||
|
first_block_idx = self._num_full_slots // self._block_size
|
||||||
|
token_blocks = self._chunk_token_blocks_for_append(token_ids)
|
||||||
|
|
||||||
|
for i, token_block in enumerate(token_blocks):
|
||||||
|
self._blocks.append_token_ids(first_block_idx + i, token_block)
|
||||||
|
|
||||||
|
self._num_full_slots += len(token_ids)
|
||||||
|
|
||||||
|
def ensure_num_empty_slots(self,
|
||||||
|
num_empty_slots: int,
|
||||||
|
extra_hash: Optional[int] = None) -> None:
|
||||||
|
"""Ensures that the BlockTable has at least the specified number of
|
||||||
|
empty slots available.
|
||||||
|
|
||||||
|
This method checks if the BlockTable has enough empty slots (i.e.,
|
||||||
|
available space) to accommodate the requested number of tokens. If not,
|
||||||
|
it allocates additional blocks on the GPU to ensure that the required
|
||||||
|
number of empty slots is available.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_empty_slots (int): The minimum number of empty slots required.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors such as adapters that influence the block, apart
|
||||||
|
from the token_ids.
|
||||||
|
"""
|
||||||
|
# Currently the block table only supports
|
||||||
|
# appending tokens to GPU blocks.
|
||||||
|
device = Device.GPU
|
||||||
|
assert self._is_allocated
|
||||||
|
|
||||||
|
if self._num_empty_slots >= num_empty_slots:
|
||||||
|
return
|
||||||
|
|
||||||
|
slots_to_allocate = num_empty_slots - self._num_empty_slots
|
||||||
|
blocks_to_allocate = cdiv(slots_to_allocate, self._block_size)
|
||||||
|
|
||||||
|
for _ in range(blocks_to_allocate):
|
||||||
|
assert len(self._blocks) > 0
|
||||||
|
self._blocks.append(
|
||||||
|
self._allocator.allocate_mutable_block(
|
||||||
|
prev_block=self._blocks[-1],
|
||||||
|
device=device,
|
||||||
|
extra_hash=extra_hash))
|
||||||
|
|
||||||
|
def fork(self) -> "BlockTable":
|
||||||
|
"""Creates a new BlockTable instance with a copy of the blocks from the
|
||||||
|
current instance.
|
||||||
|
|
||||||
|
This method creates a new BlockTable instance with the same block size,
|
||||||
|
block allocator, and a copy of the blocks from the current instance. The
|
||||||
|
new BlockTable has its own independent set of blocks, but shares the
|
||||||
|
same underlying memory allocation with the original BlockTable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BlockTable: A new BlockTable instance with a copy of the blocks from
|
||||||
|
the current instance.
|
||||||
|
"""
|
||||||
|
assert self._is_allocated
|
||||||
|
assert len(self._blocks) > 0
|
||||||
|
forked_blocks = self._allocator.fork(self._blocks[-1])
|
||||||
|
return BlockTable(
|
||||||
|
block_size=self._block_size,
|
||||||
|
block_allocator=self._allocator,
|
||||||
|
_blocks=forked_blocks,
|
||||||
|
max_block_sliding_window=self._max_block_sliding_window,
|
||||||
|
)
|
||||||
|
|
||||||
|
def free(self) -> None:
|
||||||
|
"""Frees the memory occupied by the blocks in the BlockTable.
|
||||||
|
|
||||||
|
This method iterates over all the blocks in the `_blocks` list and calls
|
||||||
|
the `free` method of the `_allocator` object to release the memory
|
||||||
|
occupied by each block. After freeing all the blocks, the `_blocks` list
|
||||||
|
is set to `None`.
|
||||||
|
"""
|
||||||
|
for block in self.blocks:
|
||||||
|
self._allocator.free(block)
|
||||||
|
self._blocks.reset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def physical_block_ids(self) -> List[int]:
|
||||||
|
"""Returns a list of physical block indices for the blocks in the
|
||||||
|
BlockTable.
|
||||||
|
|
||||||
|
This property returns a list of integers, where each integer represents
|
||||||
|
the physical block index of a corresponding block in the `_blocks` list.
|
||||||
|
The physical block index is a unique identifier for the memory location
|
||||||
|
occupied by the block.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[int]: A list of physical block indices for the blocks in the
|
||||||
|
BlockTable.
|
||||||
|
"""
|
||||||
|
return self._blocks.ids()
|
||||||
|
|
||||||
|
def get_unseen_token_ids(self, sequence_token_ids: List[int]) -> List[int]:
|
||||||
|
"""Get the number of "unseen" tokens in the sequence.
|
||||||
|
|
||||||
|
Unseen tokens are tokens in the sequence corresponding to this block
|
||||||
|
table, but are not yet appended to this block table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence_token_ids (List[int]): The list of token ids in the
|
||||||
|
sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[int]: The postfix of sequence_token_ids that has not yet been
|
||||||
|
appended to the block table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Since the block table is append-only, the unseen token ids are the
|
||||||
|
# ones after the appended ones.
|
||||||
|
return sequence_token_ids[self.num_full_slots:]
|
||||||
|
|
||||||
|
def _allocate_blocks_for_token_ids(
|
||||||
|
self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> List[Block]:
|
||||||
|
blocks: List[Block] = []
|
||||||
|
|
||||||
|
block_token_ids = []
|
||||||
|
tail_token_ids = []
|
||||||
|
for cur_token_ids in chunk_list(token_ids, self._block_size):
|
||||||
|
if len(cur_token_ids) == self._block_size:
|
||||||
|
block_token_ids.append(cur_token_ids)
|
||||||
|
else:
|
||||||
|
tail_token_ids.append(cur_token_ids)
|
||||||
|
|
||||||
|
if block_token_ids:
|
||||||
|
blocks.extend(
|
||||||
|
self._allocator.allocate_immutable_blocks(
|
||||||
|
prev_block,
|
||||||
|
block_token_ids=block_token_ids,
|
||||||
|
device=device,
|
||||||
|
extra_hash=extra_hash))
|
||||||
|
prev_block = blocks[-1]
|
||||||
|
|
||||||
|
if tail_token_ids:
|
||||||
|
assert len(tail_token_ids) == 1
|
||||||
|
cur_token_ids = tail_token_ids[0]
|
||||||
|
|
||||||
|
block = self._allocator.allocate_mutable_block(
|
||||||
|
prev_block=prev_block, device=device, extra_hash=extra_hash)
|
||||||
|
block.append_token_ids(cur_token_ids)
|
||||||
|
|
||||||
|
blocks.append(block)
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def _get_all_token_ids(self) -> List[int]:
|
||||||
|
# NOTE: This function is O(seq_len); use sparingly.
|
||||||
|
token_ids: List[int] = []
|
||||||
|
|
||||||
|
if not self._is_allocated:
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
for block in self.blocks:
|
||||||
|
token_ids.extend(block.token_ids)
|
||||||
|
|
||||||
|
return token_ids
|
||||||
|
|
||||||
|
def _get_num_token_ids(self) -> int:
|
||||||
|
res = 0
|
||||||
|
for block in self.blocks:
|
||||||
|
res += len(block.token_ids)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _is_allocated(self) -> bool:
|
||||||
|
return len(self._blocks) > 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blocks(self) -> List[Block]:
|
||||||
|
return self._blocks.list()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _num_empty_slots(self) -> int:
|
||||||
|
assert self._is_allocated
|
||||||
|
return len(self._blocks) * self._block_size - self._num_full_slots
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_full_slots(self) -> int:
|
||||||
|
"""Returns the total number of tokens currently stored in the
|
||||||
|
BlockTable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The total number of tokens currently stored in the BlockTable.
|
||||||
|
"""
|
||||||
|
return self._num_full_slots
|
||||||
|
|
||||||
|
def get_num_blocks_touched_by_append_slots(
|
||||||
|
self, token_ids: List[int], num_lookahead_slots: int) -> int:
|
||||||
|
"""Determine how many blocks will be "touched" by appending the token
|
||||||
|
ids.
|
||||||
|
|
||||||
|
This is required for the scheduler to determine whether a sequence can
|
||||||
|
continue generation, or if it must be preempted.
|
||||||
|
"""
|
||||||
|
# Math below is equivalent to:
|
||||||
|
# all_token_ids = token_ids + [-1] * num_lookahead_slots
|
||||||
|
# token_blocks = self._chunk_token_blocks_for_append(all_token_ids)
|
||||||
|
# return len(token_blocks)
|
||||||
|
|
||||||
|
num_token_ids = len(token_ids) + num_lookahead_slots
|
||||||
|
first_chunk_size = self._block_size - (self._num_full_slots %
|
||||||
|
self._block_size)
|
||||||
|
num_token_blocks = (1 + math.ceil(
|
||||||
|
(num_token_ids - first_chunk_size) / self._block_size))
|
||||||
|
return num_token_blocks
|
||||||
|
|
||||||
|
def _chunk_token_blocks_for_append(
|
||||||
|
self, token_ids: List[int]) -> List[List[int]]:
|
||||||
|
"""Split the token ids into block-sized chunks so they can be easily
|
||||||
|
appended to blocks. The first such "token block" may have less token ids
|
||||||
|
than the block size, since the last allocated block may be partially
|
||||||
|
full.
|
||||||
|
|
||||||
|
If no token ids are provided, then no chunks are returned.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not token_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
first_chunk_size = self._block_size - (self._num_full_slots %
|
||||||
|
self._block_size)
|
||||||
|
token_blocks = [token_ids[:first_chunk_size]]
|
||||||
|
token_blocks.extend(
|
||||||
|
chunk_list(token_ids[first_chunk_size:], self._block_size))
|
||||||
|
return token_blocks
|
||||||
370
vllm/core/block/common.py
Normal file
370
vllm/core/block/common.py
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Deque, Dict, Iterable, List, Optional, Protocol, Tuple
|
||||||
|
|
||||||
|
from vllm.core.block.interfaces import Block, BlockAllocator
|
||||||
|
|
||||||
|
BlockId = int
|
||||||
|
RefCount = int
|
||||||
|
|
||||||
|
|
||||||
|
class RefCounterProtocol(Protocol):
|
||||||
|
|
||||||
|
def incr(self, block_id: BlockId) -> RefCount:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def decr(self, block_id: BlockId) -> RefCount:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get(self, block_id: BlockId) -> RefCount:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class RefCounter(RefCounterProtocol):
|
||||||
|
"""A class for managing reference counts for a set of block indices.
|
||||||
|
|
||||||
|
The RefCounter class maintains a dictionary that maps block indices to their
|
||||||
|
corresponding reference counts. It provides methods to increment, decrement,
|
||||||
|
and retrieve the reference count for a given block index.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_block_indices (Iterable[BlockId]): An iterable of block indices
|
||||||
|
to initialize the reference counter with.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, all_block_indices: Iterable[BlockId]):
|
||||||
|
deduped = set(all_block_indices)
|
||||||
|
self._refcounts: Dict[BlockId, RefCount] = {
|
||||||
|
index: 0
|
||||||
|
for index in deduped
|
||||||
|
}
|
||||||
|
|
||||||
|
def incr(self, block_id: BlockId) -> RefCount:
|
||||||
|
assert block_id in self._refcounts
|
||||||
|
pre_incr_refcount = self._refcounts[block_id]
|
||||||
|
|
||||||
|
assert pre_incr_refcount >= 0
|
||||||
|
|
||||||
|
post_incr_refcount = pre_incr_refcount + 1
|
||||||
|
self._refcounts[block_id] = post_incr_refcount
|
||||||
|
return post_incr_refcount
|
||||||
|
|
||||||
|
def decr(self, block_id: BlockId) -> RefCount:
|
||||||
|
assert block_id in self._refcounts
|
||||||
|
refcount = self._refcounts[block_id]
|
||||||
|
|
||||||
|
assert refcount > 0
|
||||||
|
refcount -= 1
|
||||||
|
|
||||||
|
self._refcounts[block_id] = refcount
|
||||||
|
|
||||||
|
return refcount
|
||||||
|
|
||||||
|
def get(self, block_id: BlockId) -> RefCount:
|
||||||
|
assert block_id in self._refcounts
|
||||||
|
return self._refcounts[block_id]
|
||||||
|
|
||||||
|
def as_readonly(self) -> "ReadOnlyRefCounter":
|
||||||
|
return ReadOnlyRefCounter(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyRefCounter(RefCounterProtocol):
|
||||||
|
"""A read-only view of the RefCounter class.
|
||||||
|
|
||||||
|
The ReadOnlyRefCounter class provides a read-only interface to access the
|
||||||
|
reference counts maintained by a RefCounter instance. It does not allow
|
||||||
|
modifications to the reference counts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
refcounter (RefCounter): The RefCounter instance to create a read-only
|
||||||
|
view for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, refcounter: RefCounter):
|
||||||
|
self._refcounter = refcounter
|
||||||
|
|
||||||
|
def incr(self, block_id: BlockId) -> RefCount:
|
||||||
|
raise ValueError("Incr not allowed")
|
||||||
|
|
||||||
|
def decr(self, block_id: BlockId) -> RefCount:
|
||||||
|
raise ValueError("Decr not allowed")
|
||||||
|
|
||||||
|
def get(self, block_id: BlockId) -> RefCount:
|
||||||
|
return self._refcounter.get(block_id)
|
||||||
|
|
||||||
|
|
||||||
|
class CopyOnWriteTracker:
|
||||||
|
"""A class for tracking and managing copy-on-write operations for blocks.
|
||||||
|
|
||||||
|
The CopyOnWriteTracker class maintains a mapping of source block indices to
|
||||||
|
their corresponding copy-on-write destination block indices. It works in
|
||||||
|
conjunction with a RefCounter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
refcounter (RefCounter): The reference counter used to track block
|
||||||
|
reference counts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, refcounter: RefCounterProtocol):
|
||||||
|
self._copy_on_writes: List[Tuple[BlockId, BlockId]] = []
|
||||||
|
self._refcounter = refcounter
|
||||||
|
|
||||||
|
def is_appendable(self, block: Block) -> bool:
|
||||||
|
"""Checks if the block is shared or not. If shared, then it cannot
|
||||||
|
be appended and needs to be duplicated via copy-on-write
|
||||||
|
"""
|
||||||
|
block_id = block.block_id
|
||||||
|
if block_id is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
refcount = self._refcounter.get(block_id)
|
||||||
|
return refcount <= 1
|
||||||
|
|
||||||
|
def record_cow(self, src_block_id: Optional[BlockId],
|
||||||
|
trg_block_id: Optional[BlockId]) -> None:
|
||||||
|
"""Records a copy-on-write operation from source to target block id
|
||||||
|
Args:
|
||||||
|
src_block_id (BlockId): The source block id from which to copy
|
||||||
|
the data
|
||||||
|
trg_block_id (BlockId): The target block id to which the data
|
||||||
|
is copied
|
||||||
|
"""
|
||||||
|
assert src_block_id is not None
|
||||||
|
assert trg_block_id is not None
|
||||||
|
self._copy_on_writes.append((src_block_id, trg_block_id))
|
||||||
|
|
||||||
|
def clear_cows(self) -> List[Tuple[BlockId, BlockId]]:
|
||||||
|
"""Clears the copy-on-write tracking information and returns the current
|
||||||
|
state.
|
||||||
|
|
||||||
|
This method returns a list mapping source block indices to
|
||||||
|
destination block indices for the current copy-on-write operations.
|
||||||
|
It then clears the internal tracking information.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[BlockId, BlockId]]: A list mapping source
|
||||||
|
block indices to destination block indices for the
|
||||||
|
current copy-on-write operations.
|
||||||
|
"""
|
||||||
|
cows = self._copy_on_writes
|
||||||
|
self._copy_on_writes = []
|
||||||
|
return cows
|
||||||
|
|
||||||
|
|
||||||
|
class BlockPool:
|
||||||
|
"""Used to pre-allocate block objects, in order to avoid excessive python
|
||||||
|
object allocations/deallocations.
|
||||||
|
The pool starts from "pool_size" objects and will increase to more objects
|
||||||
|
if necessary
|
||||||
|
|
||||||
|
Note that multiple block objects may point to the same physical block id,
|
||||||
|
which is why this pool is needed, so that it will be easier to support
|
||||||
|
prefix caching and more complicated sharing of physical blocks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, block_size: int, create_block: Block.Factory,
|
||||||
|
allocator: BlockAllocator, pool_size: int):
|
||||||
|
self._block_size = block_size
|
||||||
|
self._create_block = create_block
|
||||||
|
self._allocator = allocator
|
||||||
|
self._pool_size = pool_size
|
||||||
|
assert self._pool_size >= 0
|
||||||
|
|
||||||
|
self._free_ids: Deque[int] = deque(range(self._pool_size))
|
||||||
|
self._pool = []
|
||||||
|
for i in range(self._pool_size):
|
||||||
|
self._pool.append(
|
||||||
|
self._create_block(prev_block=None,
|
||||||
|
token_ids=[],
|
||||||
|
block_size=self._block_size,
|
||||||
|
allocator=self._allocator,
|
||||||
|
block_id=None,
|
||||||
|
extra_hash=None))
|
||||||
|
|
||||||
|
def increase_pool(self):
|
||||||
|
"""Doubles the internal pool size
|
||||||
|
"""
|
||||||
|
cur_pool_size = self._pool_size
|
||||||
|
new_pool_size = cur_pool_size * 2
|
||||||
|
self._pool_size = new_pool_size
|
||||||
|
|
||||||
|
self._free_ids += deque(range(cur_pool_size, new_pool_size))
|
||||||
|
|
||||||
|
for i in range(cur_pool_size, new_pool_size):
|
||||||
|
self._pool.append(
|
||||||
|
self._create_block(prev_block=None,
|
||||||
|
token_ids=[],
|
||||||
|
block_size=self._block_size,
|
||||||
|
allocator=self._allocator,
|
||||||
|
block_id=None,
|
||||||
|
extra_hash=None))
|
||||||
|
|
||||||
|
def init_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
block_size: int,
|
||||||
|
physical_block_id: Optional[int],
|
||||||
|
extra_hash: Optional[int] = None) -> Block:
|
||||||
|
if len(self._free_ids) == 0:
|
||||||
|
self.increase_pool()
|
||||||
|
assert len(self._free_ids) > 0
|
||||||
|
|
||||||
|
pool_id = self._free_ids.popleft()
|
||||||
|
|
||||||
|
block = self._pool[pool_id]
|
||||||
|
block.__init__( # type: ignore[misc]
|
||||||
|
prev_block=prev_block,
|
||||||
|
token_ids=token_ids,
|
||||||
|
block_size=block_size,
|
||||||
|
allocator=block._allocator, # type: ignore[attr-defined]
|
||||||
|
block_id=physical_block_id,
|
||||||
|
extra_hash=extra_hash)
|
||||||
|
block.pool_id = pool_id # type: ignore[attr-defined]
|
||||||
|
return block
|
||||||
|
|
||||||
|
def free_block(self, block: Block) -> None:
|
||||||
|
self._free_ids.appendleft(block.pool_id) # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
|
class BlockList:
|
||||||
|
"""This class is an optimization to allow fast-access to physical
|
||||||
|
block ids. It maintains a block id list that is updated with the
|
||||||
|
block list and this avoids the need to reconstruct the block id
|
||||||
|
list on every iteration of the block manager
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, blocks: List[Block]):
|
||||||
|
self._blocks: List[Block] = []
|
||||||
|
self._block_ids: List[int] = []
|
||||||
|
|
||||||
|
self.update(blocks)
|
||||||
|
|
||||||
|
def _add_block_id(self, block_id: Optional[BlockId]) -> None:
|
||||||
|
assert block_id is not None
|
||||||
|
self._block_ids.append(block_id)
|
||||||
|
|
||||||
|
def _update_block_id(self, block_index: int,
|
||||||
|
new_block_id: Optional[BlockId]) -> None:
|
||||||
|
assert new_block_id is not None
|
||||||
|
self._block_ids[block_index] = new_block_id
|
||||||
|
|
||||||
|
def update(self, blocks: List[Block]):
|
||||||
|
self._blocks = blocks
|
||||||
|
|
||||||
|
# Cache block ids for fast query
|
||||||
|
self._block_ids = []
|
||||||
|
for block in self._blocks:
|
||||||
|
self._add_block_id(block.block_id)
|
||||||
|
|
||||||
|
def append_token_ids(self, block_index: int, token_ids: List[int]) -> None:
|
||||||
|
block = self._blocks[block_index]
|
||||||
|
prev_block_id = block.block_id
|
||||||
|
|
||||||
|
block.append_token_ids(token_ids)
|
||||||
|
|
||||||
|
# CoW or promotion may update the internal block_id
|
||||||
|
if prev_block_id != block.block_id:
|
||||||
|
self._update_block_id(block_index, block.block_id)
|
||||||
|
|
||||||
|
def append(self, new_block: Block):
|
||||||
|
self._blocks.append(new_block)
|
||||||
|
self._add_block_id(new_block.block_id)
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._blocks)
|
||||||
|
|
||||||
|
def __getitem__(self, block_index: int) -> Block:
|
||||||
|
return self._blocks[block_index]
|
||||||
|
|
||||||
|
def __setitem__(self, block_index: int, new_block: Block) -> None:
|
||||||
|
self._blocks[block_index] = new_block
|
||||||
|
self._update_block_id(block_index, new_block.block_id)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self._blocks = []
|
||||||
|
self._block_ids = []
|
||||||
|
|
||||||
|
def list(self) -> List[Block]:
|
||||||
|
return self._blocks
|
||||||
|
|
||||||
|
def ids(self) -> List[int]:
|
||||||
|
return self._block_ids
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CacheMetricData:
|
||||||
|
"""A utility dataclass to maintain cache metric.
|
||||||
|
To avoid overflow, we maintain the hit rate in block granularity, so that
|
||||||
|
we can maintain a single hit rate for n_completed_block x block_size,
|
||||||
|
and calculate the real time hit rate by the following:
|
||||||
|
BS = The number of queries per block.
|
||||||
|
nB = The number of completed blocks.
|
||||||
|
HR = hit rate of (nB x BS) queries.
|
||||||
|
Q = current number of queries (< BS).
|
||||||
|
H = current number of hits (< BS).
|
||||||
|
hit rate = ((HR x nB) + (H / Q) x (Q / BS)) / (nB + Q / BS)
|
||||||
|
"""
|
||||||
|
num_completed_blocks: int = 0
|
||||||
|
completed_block_cache_hit_rate: float = 0.0
|
||||||
|
num_incompleted_block_queries: int = 0
|
||||||
|
num_incompleted_block_hit: int = 0
|
||||||
|
block_size: int = 1000
|
||||||
|
|
||||||
|
def query(self, hit: bool):
|
||||||
|
self.num_incompleted_block_queries += 1
|
||||||
|
self.num_incompleted_block_hit += 1 if hit else 0
|
||||||
|
|
||||||
|
# When a block is completed, update the cache hit rate
|
||||||
|
# and reset the incomplete numbers.
|
||||||
|
if self.num_incompleted_block_queries == self.block_size:
|
||||||
|
hit_rate = (self.num_incompleted_block_hit /
|
||||||
|
self.num_incompleted_block_queries)
|
||||||
|
self.completed_block_cache_hit_rate = (
|
||||||
|
self.completed_block_cache_hit_rate * self.num_completed_blocks
|
||||||
|
+ hit_rate) / (self.num_completed_blocks + 1)
|
||||||
|
self.num_incompleted_block_queries = 0
|
||||||
|
self.num_incompleted_block_hit = 0
|
||||||
|
self.num_completed_blocks += 1
|
||||||
|
|
||||||
|
def get_hit_rate(self):
|
||||||
|
incomplete_ratio = self.num_incompleted_block_queries / self.block_size
|
||||||
|
total_blocks = self.num_completed_blocks + incomplete_ratio
|
||||||
|
if total_blocks == 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
completed_block_hit, incompleted_block_hit = 0.0, 0.0
|
||||||
|
if self.num_completed_blocks > 0:
|
||||||
|
completed_block_hit = (self.completed_block_cache_hit_rate *
|
||||||
|
self.num_completed_blocks)
|
||||||
|
if self.num_incompleted_block_queries > 0:
|
||||||
|
incompleted_hit_rate = (self.num_incompleted_block_hit /
|
||||||
|
self.num_incompleted_block_queries)
|
||||||
|
incompleted_block_hit = (incompleted_hit_rate * incomplete_ratio)
|
||||||
|
return (completed_block_hit + incompleted_block_hit) / total_blocks
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_blocks_recursively(last_block: Block) -> List[Block]:
|
||||||
|
"""Retrieves all the blocks in a sequence starting from the last block.
|
||||||
|
|
||||||
|
This function recursively traverses the sequence of blocks in reverse order,
|
||||||
|
starting from the given last block, and returns a list of all the blocks in
|
||||||
|
the sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_block (Block): The last block in the sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Block]: A list of all the blocks in the sequence, in the order they
|
||||||
|
appear.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def recurse(block: Block, lst: List[Block]) -> None:
|
||||||
|
if block.prev_block is not None:
|
||||||
|
recurse(block.prev_block, lst)
|
||||||
|
lst.append(block)
|
||||||
|
|
||||||
|
all_blocks: List[Block] = []
|
||||||
|
recurse(last_block, all_blocks)
|
||||||
|
return all_blocks
|
||||||
440
vllm/core/block/cpu_gpu_block_allocator.py
Normal file
440
vllm/core/block/cpu_gpu_block_allocator.py
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Dict, FrozenSet, List, Optional, Tuple
|
||||||
|
|
||||||
|
from vllm.core.block.interfaces import (Block, BlockAllocator, BlockId,
|
||||||
|
DeviceAwareBlockAllocator)
|
||||||
|
from vllm.core.block.naive_block import NaiveBlock, NaiveBlockAllocator
|
||||||
|
from vllm.core.block.prefix_caching_block import PrefixCachingBlockAllocator
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
from vllm.utils import Device
|
||||||
|
|
||||||
|
|
||||||
|
class CpuGpuBlockAllocator(DeviceAwareBlockAllocator):
|
||||||
|
"""A block allocator that can allocate blocks on both CPU and GPU memory.
|
||||||
|
|
||||||
|
This class implements the `DeviceAwareBlockAllocator` interface and provides
|
||||||
|
functionality for allocating and managing blocks of memory on both CPU and
|
||||||
|
GPU devices.
|
||||||
|
|
||||||
|
The `CpuGpuBlockAllocator` maintains separate memory pools for CPU and GPU
|
||||||
|
blocks, and allows for allocation, deallocation, forking, and swapping of
|
||||||
|
blocks across these memory pools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(
|
||||||
|
allocator_type: str,
|
||||||
|
num_gpu_blocks: int,
|
||||||
|
num_cpu_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
) -> DeviceAwareBlockAllocator:
|
||||||
|
"""Creates a CpuGpuBlockAllocator instance with the specified
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
This static method creates and returns a CpuGpuBlockAllocator instance
|
||||||
|
based on the provided parameters. It initializes the CPU and GPU block
|
||||||
|
allocators with the specified number of blocks, block size, and
|
||||||
|
allocator type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allocator_type (str): The type of block allocator to use for CPU
|
||||||
|
and GPU blocks. Currently supported values are "naive" and
|
||||||
|
"prefix_caching".
|
||||||
|
num_gpu_blocks (int): The number of blocks to allocate for GPU
|
||||||
|
memory.
|
||||||
|
num_cpu_blocks (int): The number of blocks to allocate for CPU
|
||||||
|
memory.
|
||||||
|
block_size (int): The size of each block in number of tokens.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DeviceAwareBlockAllocator: A CpuGpuBlockAllocator instance with the
|
||||||
|
specified configuration.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The block IDs are assigned contiguously, with GPU block IDs coming
|
||||||
|
before CPU block IDs.
|
||||||
|
"""
|
||||||
|
# For HPU, block id 0 is used only for padding
|
||||||
|
reserved_blocks = 1 if current_platform.is_hpu() else 0
|
||||||
|
block_ids = list(
|
||||||
|
range(reserved_blocks, num_gpu_blocks + num_cpu_blocks))
|
||||||
|
num_gpu_blocks -= reserved_blocks
|
||||||
|
gpu_block_ids = block_ids[:num_gpu_blocks]
|
||||||
|
cpu_block_ids = block_ids[num_gpu_blocks:]
|
||||||
|
|
||||||
|
if allocator_type == "naive":
|
||||||
|
gpu_allocator: BlockAllocator = NaiveBlockAllocator(
|
||||||
|
create_block=NaiveBlock, # type: ignore
|
||||||
|
num_blocks=num_gpu_blocks,
|
||||||
|
block_size=block_size,
|
||||||
|
block_ids=gpu_block_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
cpu_allocator: BlockAllocator = NaiveBlockAllocator(
|
||||||
|
create_block=NaiveBlock, # type: ignore
|
||||||
|
num_blocks=num_cpu_blocks,
|
||||||
|
block_size=block_size,
|
||||||
|
block_ids=cpu_block_ids,
|
||||||
|
)
|
||||||
|
elif allocator_type == "prefix_caching":
|
||||||
|
gpu_allocator = PrefixCachingBlockAllocator(
|
||||||
|
num_blocks=num_gpu_blocks,
|
||||||
|
block_size=block_size,
|
||||||
|
block_ids=gpu_block_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
cpu_allocator = PrefixCachingBlockAllocator(
|
||||||
|
num_blocks=num_cpu_blocks,
|
||||||
|
block_size=block_size,
|
||||||
|
block_ids=cpu_block_ids,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown allocator type {allocator_type=}")
|
||||||
|
|
||||||
|
return CpuGpuBlockAllocator(
|
||||||
|
cpu_block_allocator=cpu_allocator,
|
||||||
|
gpu_block_allocator=gpu_allocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, cpu_block_allocator: BlockAllocator,
|
||||||
|
gpu_block_allocator: BlockAllocator):
|
||||||
|
assert not (
|
||||||
|
cpu_block_allocator.all_block_ids
|
||||||
|
& gpu_block_allocator.all_block_ids
|
||||||
|
), "cpu and gpu block allocators can't have intersection of block ids"
|
||||||
|
|
||||||
|
self._allocators = {
|
||||||
|
Device.CPU: cpu_block_allocator,
|
||||||
|
Device.GPU: gpu_block_allocator,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._swap_mapping: Dict[int, int] = {}
|
||||||
|
self._null_block: Optional[Block] = None
|
||||||
|
|
||||||
|
self._block_ids_to_allocator: Dict[int, BlockAllocator] = {}
|
||||||
|
for _, allocator in self._allocators.items():
|
||||||
|
for block_id in allocator.all_block_ids:
|
||||||
|
self._block_ids_to_allocator[block_id] = allocator
|
||||||
|
|
||||||
|
def allocate_or_get_null_block(self) -> Block:
|
||||||
|
if self._null_block is None:
|
||||||
|
self._null_block = NullBlock(
|
||||||
|
self.allocate_mutable_block(None, Device.GPU))
|
||||||
|
return self._null_block
|
||||||
|
|
||||||
|
def allocate_mutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> Block:
|
||||||
|
"""Allocates a new mutable block on the specified device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Optional[Block]): The previous block to in the sequence.
|
||||||
|
Used for prefix hashing.
|
||||||
|
device (Device): The device on which to allocate the new block.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors, such as adapters, that influence the block hash
|
||||||
|
in the prefix caching block.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block: The newly allocated mutable block.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].allocate_mutable_block(
|
||||||
|
prev_block, extra_hash=extra_hash)
|
||||||
|
|
||||||
|
def allocate_immutable_blocks(
|
||||||
|
self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
block_token_ids: List[List[int]],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> List[Block]:
|
||||||
|
"""Allocates a new group of immutable blocks with the provided block
|
||||||
|
token IDs on the specified device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Optional[Block]): The previous block in the sequence.
|
||||||
|
Used for prefix hashing.
|
||||||
|
block_token_ids (List[int]): The list of block token IDs to be
|
||||||
|
stored in the new blocks.
|
||||||
|
device (Device): The device on which to allocate the new block.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors, such as adapters, that influence the block hash
|
||||||
|
in the prefix caching block.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Block]: The newly allocated list of immutable blocks
|
||||||
|
containing the provided block token IDs.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].allocate_immutable_blocks(
|
||||||
|
prev_block, block_token_ids, extra_hash=extra_hash)
|
||||||
|
|
||||||
|
def allocate_immutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> Block:
|
||||||
|
"""Allocates a new immutable block with the provided token IDs on the
|
||||||
|
specified device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Optional[Block]): The previous block in the sequence.
|
||||||
|
Used for prefix hashing.
|
||||||
|
token_ids (List[int]): The list of token IDs to be stored in the new
|
||||||
|
block.
|
||||||
|
device (Device): The device on which to allocate the new block.
|
||||||
|
extra_hash (Optional[int]): The hash value of additional
|
||||||
|
factors, such as adapters, that influence the block hash
|
||||||
|
in the prefix caching block.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block: The newly allocated immutable block containing the provided
|
||||||
|
token IDs.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].allocate_immutable_block(
|
||||||
|
prev_block, token_ids, extra_hash=extra_hash)
|
||||||
|
|
||||||
|
def free(self, block: Block) -> None:
|
||||||
|
"""Frees the memory occupied by the given block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block (Block): The block to be freed.
|
||||||
|
"""
|
||||||
|
# Null block should never be freed
|
||||||
|
if isinstance(block, NullBlock):
|
||||||
|
return
|
||||||
|
block_id = block.block_id
|
||||||
|
assert block_id is not None
|
||||||
|
allocator = self._block_ids_to_allocator[block_id]
|
||||||
|
allocator.free(block)
|
||||||
|
|
||||||
|
def fork(self, last_block: Block) -> List[Block]:
|
||||||
|
"""Creates a new sequence of blocks that shares the same underlying
|
||||||
|
memory as the original sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_block (Block): The last block in the original sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Block]: A new list of blocks that shares the same memory as the
|
||||||
|
original sequence.
|
||||||
|
"""
|
||||||
|
# do not attempt to fork the null block
|
||||||
|
assert not isinstance(last_block, NullBlock)
|
||||||
|
block_id = last_block.block_id
|
||||||
|
assert block_id is not None
|
||||||
|
allocator = self._block_ids_to_allocator[block_id]
|
||||||
|
return allocator.fork(last_block)
|
||||||
|
|
||||||
|
def get_num_free_blocks(self, device: Device) -> int:
|
||||||
|
"""Returns the number of free blocks available on the specified device.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device (Device): The device for which to query the number of free
|
||||||
|
blocks. AssertionError is raised if None is passed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of free blocks available on the specified device.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].get_num_free_blocks()
|
||||||
|
|
||||||
|
def get_num_total_blocks(self, device: Device) -> int:
|
||||||
|
return self._allocators[device].get_num_total_blocks()
|
||||||
|
|
||||||
|
def get_physical_block_id(self, device: Device, absolute_id: int) -> int:
|
||||||
|
"""Returns the zero-offset block id on certain device given the
|
||||||
|
absolute block id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device (Device): The device for which to query relative block id.
|
||||||
|
absolute_id (int): The absolute block id for the block in
|
||||||
|
whole allocator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The zero-offset block id on certain device.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].get_physical_block_id(absolute_id)
|
||||||
|
|
||||||
|
def swap(self, blocks: List[Block], src_device: Device,
|
||||||
|
dst_device: Device) -> Dict[int, int]:
|
||||||
|
"""Execute the swap for the given blocks from source_device
|
||||||
|
on to dest_device, save the current swap mapping and append
|
||||||
|
them to the accumulated `self._swap_mapping` for each
|
||||||
|
scheduling move.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocks: List of blocks to be swapped.
|
||||||
|
src_device (Device): Device to swap the 'blocks' from.
|
||||||
|
dst_device (Device): Device to swap the 'blocks' to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[int, int]: Swap mapping from source_device
|
||||||
|
on to dest_device.
|
||||||
|
"""
|
||||||
|
src_block_ids = [block.block_id for block in blocks]
|
||||||
|
self._allocators[src_device].swap_out(blocks)
|
||||||
|
self._allocators[dst_device].swap_in(blocks)
|
||||||
|
dst_block_ids = [block.block_id for block in blocks]
|
||||||
|
|
||||||
|
current_swap_mapping: Dict[int, int] = {}
|
||||||
|
for src_block_id, dst_block_id in zip(src_block_ids, dst_block_ids):
|
||||||
|
if src_block_id is not None and dst_block_id is not None:
|
||||||
|
self._swap_mapping[src_block_id] = dst_block_id
|
||||||
|
current_swap_mapping[src_block_id] = dst_block_id
|
||||||
|
return current_swap_mapping
|
||||||
|
|
||||||
|
def get_num_full_blocks_touched(self, blocks: List[Block],
|
||||||
|
device: Device) -> int:
|
||||||
|
"""Returns the number of full blocks that will be touched by
|
||||||
|
swapping in/out the given blocks on to the 'device'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocks: List of blocks to be swapped.
|
||||||
|
device (Device): Device to swap the 'blocks' on.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: the number of full blocks that will be touched by
|
||||||
|
swapping in/out the given blocks on to the 'device'.
|
||||||
|
Non full blocks are ignored when deciding the number
|
||||||
|
of blocks to touch.
|
||||||
|
"""
|
||||||
|
return self._allocators[device].get_num_full_blocks_touched(blocks)
|
||||||
|
|
||||||
|
def clear_copy_on_writes(self) -> List[Tuple[int, int]]:
|
||||||
|
"""Clears the copy-on-write (CoW) state and returns the mapping of
|
||||||
|
source to destination block IDs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[int, int]]: A list mapping source block IDs to
|
||||||
|
destination block IDs.
|
||||||
|
"""
|
||||||
|
# CoW only supported on GPU
|
||||||
|
device = Device.GPU
|
||||||
|
return self._allocators[device].clear_copy_on_writes()
|
||||||
|
|
||||||
|
def mark_blocks_as_accessed(self, block_ids: List[int],
|
||||||
|
now: float) -> None:
|
||||||
|
"""Mark blocks as accessed, only use for prefix caching."""
|
||||||
|
# Prefix caching only supported on GPU.
|
||||||
|
device = Device.GPU
|
||||||
|
return self._allocators[device].mark_blocks_as_accessed(block_ids, now)
|
||||||
|
|
||||||
|
def mark_blocks_as_computed(self, block_ids: List[int]) -> None:
|
||||||
|
"""Mark blocks as accessed, only use for prefix caching."""
|
||||||
|
# Prefix caching only supported on GPU.
|
||||||
|
device = Device.GPU
|
||||||
|
return self._allocators[device].mark_blocks_as_computed(block_ids)
|
||||||
|
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, computed_seq_block_ids: List[List[int]]) -> List[int]:
|
||||||
|
# Prefix caching only supported on GPU.
|
||||||
|
device = Device.GPU
|
||||||
|
return self._allocators[device].get_common_computed_block_ids(
|
||||||
|
computed_seq_block_ids)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_block_ids(self) -> FrozenSet[int]:
|
||||||
|
return frozenset(self._block_ids_to_allocator.keys())
|
||||||
|
|
||||||
|
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||||
|
"""Prefix cache hit rate. -1 means not supported or disabled."""
|
||||||
|
assert device in self._allocators
|
||||||
|
return self._allocators[device].get_prefix_cache_hit_rate()
|
||||||
|
|
||||||
|
def reset_prefix_cache(self, device: Optional[Device] = None) -> bool:
|
||||||
|
"""Reset prefix cache for specified or all devices."""
|
||||||
|
if device:
|
||||||
|
return self._allocators[device].reset_prefix_cache()
|
||||||
|
success = True
|
||||||
|
for allocator in self._allocators.values():
|
||||||
|
success = success and allocator.reset_prefix_cache()
|
||||||
|
return success
|
||||||
|
|
||||||
|
def get_and_reset_swaps(self) -> List[Tuple[int, int]]:
|
||||||
|
"""Returns and clears the mapping of source to destination block IDs.
|
||||||
|
Will be called after every swapping operations for now, and after every
|
||||||
|
schedule when BlockManagerV2 become default. Currently not useful.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[int, int]]: A mapping of source to destination block IDs.
|
||||||
|
"""
|
||||||
|
mapping = self._swap_mapping.copy()
|
||||||
|
self._swap_mapping.clear()
|
||||||
|
return list(mapping.items())
|
||||||
|
|
||||||
|
def find_cached_blocks_prefix(
|
||||||
|
self,
|
||||||
|
block_hashes: List[int],
|
||||||
|
device: Device = Device.GPU,
|
||||||
|
) -> List[int]:
|
||||||
|
return self._allocators[device].find_cached_blocks_prefix(block_hashes)
|
||||||
|
|
||||||
|
|
||||||
|
class NullBlock(Block):
|
||||||
|
"""
|
||||||
|
Null blocks are used as a placeholders for KV cache blocks that have
|
||||||
|
been dropped due to sliding window.
|
||||||
|
This implementation just wraps an ordinary block and prevents it from
|
||||||
|
being modified. It also allows for testing if a block is NullBlock
|
||||||
|
via isinstance().
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, proxy: Block):
|
||||||
|
super().__init__()
|
||||||
|
self._proxy = proxy
|
||||||
|
|
||||||
|
def append_token_ids(self, token_ids: List[BlockId]):
|
||||||
|
raise ValueError("null block should not be modified")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block_id(self):
|
||||||
|
return self._proxy.block_id
|
||||||
|
|
||||||
|
@block_id.setter
|
||||||
|
def block_id(self, value: Optional[BlockId]):
|
||||||
|
raise ValueError("null block should not be modified")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_ids(self) -> List[BlockId]:
|
||||||
|
return self._proxy.token_ids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_tokens_total(self) -> int:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"num_tokens_total is not used for null block")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_empty_slots(self) -> BlockId:
|
||||||
|
return self._proxy.num_empty_slots
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_full(self):
|
||||||
|
return self._proxy.is_full
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prev_block(self):
|
||||||
|
return self._proxy.prev_block
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_hash(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def computed(self):
|
||||||
|
return self._proxy.computed
|
||||||
|
|
||||||
|
@computed.setter
|
||||||
|
def computed(self, value):
|
||||||
|
self._proxy.computed = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_accessed(self) -> float:
|
||||||
|
return self._proxy.last_accessed
|
||||||
|
|
||||||
|
@last_accessed.setter
|
||||||
|
def last_accessed(self, last_accessed_ts: float):
|
||||||
|
self._proxy.last_accessed = last_accessed_ts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_hash(self):
|
||||||
|
return self._proxy.content_hash
|
||||||
318
vllm/core/block/interfaces.py
Normal file
318
vllm/core/block/interfaces.py
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, FrozenSet, List, Optional, Protocol, Tuple
|
||||||
|
|
||||||
|
from vllm.utils import Device
|
||||||
|
|
||||||
|
BlockId = int
|
||||||
|
|
||||||
|
|
||||||
|
class Block(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def append_token_ids(self, token_ids: List[int]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def block_id(self) -> Optional[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@block_id.setter
|
||||||
|
@abstractmethod
|
||||||
|
def block_id(self, value: Optional[int]) -> None:
|
||||||
|
"""NOTE: Do not use this API outside Block."""
|
||||||
|
self._block_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def token_ids(self) -> List[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def num_tokens_total(self) -> int:
|
||||||
|
"""The number of tokens till the current block (inclusive)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def num_empty_slots(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def is_full(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def prev_block(self) -> Optional["Block"]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def extra_hash(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def computed(self) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@computed.setter
|
||||||
|
@abstractmethod
|
||||||
|
def computed(self, value) -> bool:
|
||||||
|
"""Should be only used by PrefixCacingAllocator"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def last_accessed(self) -> float:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@last_accessed.setter
|
||||||
|
@abstractmethod
|
||||||
|
def last_accessed(self, last_accessed_ts: float):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class Factory(Protocol):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
prev_block: Optional["Block"],
|
||||||
|
token_ids: List[int],
|
||||||
|
block_size: int,
|
||||||
|
allocator: "BlockAllocator",
|
||||||
|
block_id: Optional[int] = None,
|
||||||
|
computed: bool = False,
|
||||||
|
extra_hash: Optional[int] = None,
|
||||||
|
) -> "Block":
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def content_hash(self) -> Optional[int]:
|
||||||
|
"""Return the content-based hash of the current block, or None if it is
|
||||||
|
not yet defined or not supported.
|
||||||
|
|
||||||
|
For the content-based hash to be defined, the current block must be
|
||||||
|
full.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class BlockAllocator(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_mutable_block(self, prev_block: Optional[Block],
|
||||||
|
extra_hash: Optional[int]) -> Block:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_immutable_block(self, prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
extra_hash: Optional[int]) -> Block:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_immutable_blocks(self, prev_block: Optional[Block],
|
||||||
|
block_token_ids: List[List[int]],
|
||||||
|
extra_hash: Optional[int]) -> List[Block]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def free(self, block: Block) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fork(self, last_block: Block) -> List[Block]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_total_blocks(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_free_blocks(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_physical_block_id(self, absolute_id: int) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def swap_out(self, blocks: List[Block]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def swap_in(self, blocks: List[Block]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def all_block_ids(self) -> FrozenSet[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear_copy_on_writes(self) -> List[Tuple[int, int]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_blocks_as_accessed(self, block_ids: List[int],
|
||||||
|
now: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_blocks_as_computed(self, block_ids: List[int]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, computed_seq_block_ids: List[List[int]]) -> List[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cow_block_if_not_appendable(self, block: Block) -> BlockId:
|
||||||
|
"""NOTE: This should not be used besides Block"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def promote_to_immutable_block(self, block: Block) -> BlockId:
|
||||||
|
"""NOTE: This should not be used besides Block"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_full_blocks_touched(self, blocks: List[Block]) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_prefix_cache_hit_rate(self) -> float:
|
||||||
|
"""Prefix cache hit rate. -1 means not supported or disabled."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def reset_prefix_cache(self) -> bool:
|
||||||
|
"""Reset prefix cache."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoFreeBlocksError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_cached_blocks_prefix(
|
||||||
|
self,
|
||||||
|
block_hashes: List[int],
|
||||||
|
) -> List[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceAwareBlockAllocator(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_mutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> Block:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_immutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None) -> Block:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_immutable_blocks(
|
||||||
|
self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
block_token_ids: List[List[int]],
|
||||||
|
device: Device,
|
||||||
|
extra_hash: Optional[int] = None,
|
||||||
|
) -> List[Block]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_free_blocks(self, device: Device) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_total_blocks(self, device: Device) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def free(self, block: Block) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fork(self, last_block: Block) -> List[Block]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def all_block_ids(self) -> FrozenSet[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def clear_copy_on_writes(self) -> List[Tuple[int, int]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_blocks_as_accessed(self, block_ids: List[int],
|
||||||
|
now: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_blocks_as_computed(self, block_ids: List[int]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, computed_seq_block_ids: List[List[int]]) -> List[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_full_blocks_touched(self, blocks: List[Block],
|
||||||
|
device: Device) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def swap(self, blocks: List[Block], src_device: Device,
|
||||||
|
dst_device: Device) -> Dict[int, int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_physical_block_id(self, device: Device, absolute_id: int) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate_or_get_null_block(self) -> Block:
|
||||||
|
"""
|
||||||
|
Null blocks are used as a placeholders for KV cache blocks that have
|
||||||
|
been dropped due to sliding window.
|
||||||
|
There is at most one null block per allocator.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||||
|
"""Prefix cache hit rate. -1 means not supported or disabled."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def reset_prefix_cache(self, device: Optional[Device] = None) -> bool:
|
||||||
|
"""Reset prefix cache."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def find_cached_blocks_prefix(
|
||||||
|
self,
|
||||||
|
block_hashes: List[int],
|
||||||
|
device: Device = Device.GPU,
|
||||||
|
) -> List[int]:
|
||||||
|
pass
|
||||||
465
vllm/core/block/naive_block.py
Normal file
465
vllm/core/block/naive_block.py
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from typing import Deque, FrozenSet, Iterable, List, Optional, Tuple, Union
|
||||||
|
|
||||||
|
from vllm.core.block.common import (BlockPool, CopyOnWriteTracker, RefCounter,
|
||||||
|
get_all_blocks_recursively)
|
||||||
|
from vllm.core.block.interfaces import Block, BlockAllocator, BlockId, Device
|
||||||
|
|
||||||
|
Refcount = int
|
||||||
|
|
||||||
|
|
||||||
|
class NaiveBlockAllocator(BlockAllocator):
|
||||||
|
"""A simple block allocator that manages blocks of memory without prefix
|
||||||
|
caching.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
create_block (Block.Factory): A factory function for creating new
|
||||||
|
blocks. This is used when a NaiveBlockAllocator is composed within
|
||||||
|
a prefix caching allocator -- the naive block allocator must
|
||||||
|
construct prefix caching blocks (but shouldn't know anything else
|
||||||
|
about them).
|
||||||
|
num_blocks (int): The total number of blocks to manage.
|
||||||
|
block_size (int): The size of each block in tokens.
|
||||||
|
block_ids (Optional[Iterable[int]], optional): An optional iterable of
|
||||||
|
block IDs. If not provided, block IDs will be assigned sequentially
|
||||||
|
from 0 to num_blocks - 1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
create_block: Block.Factory,
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
block_ids: Optional[Iterable[int]] = None,
|
||||||
|
block_pool: Optional[BlockPool] = None,
|
||||||
|
):
|
||||||
|
if block_ids is None:
|
||||||
|
block_ids = range(num_blocks)
|
||||||
|
|
||||||
|
self._free_block_indices: Deque[BlockId] = deque(block_ids)
|
||||||
|
self._all_block_indices = frozenset(block_ids)
|
||||||
|
assert len(self._all_block_indices) == num_blocks
|
||||||
|
|
||||||
|
self._refcounter = RefCounter(
|
||||||
|
all_block_indices=self._free_block_indices)
|
||||||
|
self._block_size = block_size
|
||||||
|
|
||||||
|
self._cow_tracker = CopyOnWriteTracker(
|
||||||
|
refcounter=self._refcounter.as_readonly())
|
||||||
|
|
||||||
|
if block_pool is None:
|
||||||
|
extra_factor = 4
|
||||||
|
# Pre-allocate "num_blocks * extra_factor" block objects.
|
||||||
|
# The "* extra_factor" is a buffer to allow more block objects
|
||||||
|
# than physical blocks
|
||||||
|
self._block_pool = BlockPool(self._block_size, create_block, self,
|
||||||
|
num_blocks * extra_factor)
|
||||||
|
else:
|
||||||
|
# In this case, the block pool is provided by the caller,
|
||||||
|
# which means that there is most likely a need to share
|
||||||
|
# a block pool between allocators
|
||||||
|
self._block_pool = block_pool
|
||||||
|
|
||||||
|
def allocate_immutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
extra_hash: Optional[int] = None,
|
||||||
|
device: Optional[Device] = None) -> Block:
|
||||||
|
"""Allocates a new immutable block with the given token IDs, linked to
|
||||||
|
the previous block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Optional[Block]): The previous block in the sequence. If
|
||||||
|
None, then the block to be allocated is the first block in the
|
||||||
|
sequence.
|
||||||
|
token_ids (List[int]): The token IDs to be stored in the new block.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block: The newly allocated immutable block.
|
||||||
|
"""
|
||||||
|
assert device is None
|
||||||
|
block = self.allocate_mutable_block(prev_block=prev_block)
|
||||||
|
block.append_token_ids(token_ids)
|
||||||
|
return block
|
||||||
|
|
||||||
|
def allocate_immutable_blocks(
|
||||||
|
self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
block_token_ids: List[List[int]],
|
||||||
|
extra_hash: Optional[int] = None,
|
||||||
|
device: Optional[Device] = None) -> List[Block]:
|
||||||
|
assert device is None
|
||||||
|
num_blocks = len(block_token_ids)
|
||||||
|
|
||||||
|
block_ids = []
|
||||||
|
for i in range(num_blocks):
|
||||||
|
block_ids.append(self._allocate_block_id())
|
||||||
|
|
||||||
|
blocks = []
|
||||||
|
for i in range(num_blocks):
|
||||||
|
prev_block = self._block_pool.init_block(
|
||||||
|
prev_block=prev_block,
|
||||||
|
token_ids=block_token_ids[i],
|
||||||
|
block_size=self._block_size,
|
||||||
|
physical_block_id=block_ids[i])
|
||||||
|
blocks.append(prev_block)
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def allocate_mutable_block(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
extra_hash: Optional[int] = None,
|
||||||
|
device: Optional[Device] = None) -> Block:
|
||||||
|
"""Allocates a new mutable block, linked to the previous block.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Optional[Block]): The previous block in the sequence. If
|
||||||
|
None, then the block to be allocated is the first block in the
|
||||||
|
sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Block: The newly allocated mutable block.
|
||||||
|
"""
|
||||||
|
assert device is None
|
||||||
|
block_id = self._allocate_block_id()
|
||||||
|
block = self._block_pool.init_block(prev_block=prev_block,
|
||||||
|
token_ids=[],
|
||||||
|
block_size=self._block_size,
|
||||||
|
physical_block_id=block_id)
|
||||||
|
return block
|
||||||
|
|
||||||
|
def _allocate_block_id(self) -> BlockId:
|
||||||
|
if not self._free_block_indices:
|
||||||
|
raise BlockAllocator.NoFreeBlocksError()
|
||||||
|
|
||||||
|
block_id = self._free_block_indices.popleft()
|
||||||
|
self._refcounter.incr(block_id)
|
||||||
|
return block_id
|
||||||
|
|
||||||
|
def _free_block_id(self, block: Union[Block, BlockId]) -> None:
|
||||||
|
if isinstance(block, Block):
|
||||||
|
block_id = block.block_id
|
||||||
|
block.block_id = None
|
||||||
|
else:
|
||||||
|
block_id = block
|
||||||
|
assert block_id is not None
|
||||||
|
|
||||||
|
refcount = self._refcounter.decr(block_id)
|
||||||
|
if refcount == 0:
|
||||||
|
self._free_block_indices.appendleft(block_id)
|
||||||
|
|
||||||
|
def free(self, block: Block, keep_block_object: bool = False) -> None:
|
||||||
|
# Release the physical block id
|
||||||
|
self._free_block_id(block)
|
||||||
|
|
||||||
|
# Release the block object
|
||||||
|
if not keep_block_object:
|
||||||
|
self._block_pool.free_block(block)
|
||||||
|
|
||||||
|
def free_block_id(self, block_id: BlockId) -> None:
|
||||||
|
self._free_block_id(block_id)
|
||||||
|
|
||||||
|
def fork(self, last_block: Block) -> List[Block]:
|
||||||
|
"""Creates a new sequence of blocks that shares the same underlying
|
||||||
|
memory as the original sequence.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
last_block (Block): The last block in the original sequence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Block]: The new sequence of blocks that shares the same memory
|
||||||
|
as the original sequence.
|
||||||
|
"""
|
||||||
|
source_blocks = get_all_blocks_recursively(last_block)
|
||||||
|
|
||||||
|
forked_blocks: List[Block] = []
|
||||||
|
prev_block = None
|
||||||
|
for block in source_blocks:
|
||||||
|
|
||||||
|
# Increment refcount for each block.
|
||||||
|
assert block.block_id is not None
|
||||||
|
refcount = self._refcounter.incr(block.block_id)
|
||||||
|
assert refcount != 1, "can't fork free'd block"
|
||||||
|
|
||||||
|
forked_block = self._block_pool.init_block(
|
||||||
|
prev_block=prev_block,
|
||||||
|
token_ids=block.token_ids,
|
||||||
|
block_size=self._block_size,
|
||||||
|
physical_block_id=block.block_id)
|
||||||
|
|
||||||
|
forked_blocks.append(forked_block)
|
||||||
|
prev_block = forked_blocks[-1]
|
||||||
|
|
||||||
|
return forked_blocks
|
||||||
|
|
||||||
|
def get_num_free_blocks(self) -> int:
|
||||||
|
return len(self._free_block_indices)
|
||||||
|
|
||||||
|
def get_num_total_blocks(self) -> int:
|
||||||
|
return len(self._all_block_indices)
|
||||||
|
|
||||||
|
def get_physical_block_id(self, absolute_id: int) -> int:
|
||||||
|
"""Returns the zero-offset block id on certain block allocator
|
||||||
|
given the absolute block id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
absolute_id (int): The absolute block id for the block
|
||||||
|
in whole allocator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The zero-offset block id on certain device.
|
||||||
|
"""
|
||||||
|
return sorted(self._all_block_indices).index(absolute_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def refcounter(self):
|
||||||
|
return self._refcounter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_block_ids(self) -> FrozenSet[int]:
|
||||||
|
return self._all_block_indices
|
||||||
|
|
||||||
|
def cow_block_if_not_appendable(self, block: Block) -> BlockId:
|
||||||
|
"""Performs a copy-on-write operation on the given block if it is not
|
||||||
|
appendable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block (Block): The block to check for copy-on-write.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BlockId: The block index of the new block if a copy-on-write
|
||||||
|
operation was performed, or the original block index if
|
||||||
|
no copy-on-write was necessary.
|
||||||
|
"""
|
||||||
|
src_block_id = block.block_id
|
||||||
|
assert src_block_id is not None
|
||||||
|
|
||||||
|
if self._cow_tracker.is_appendable(block):
|
||||||
|
return src_block_id
|
||||||
|
|
||||||
|
self._free_block_id(block)
|
||||||
|
trg_block_id = self._allocate_block_id()
|
||||||
|
|
||||||
|
self._cow_tracker.record_cow(src_block_id, trg_block_id)
|
||||||
|
|
||||||
|
return trg_block_id
|
||||||
|
|
||||||
|
def clear_copy_on_writes(self) -> List[Tuple[BlockId, BlockId]]:
|
||||||
|
"""Returns the copy-on-write source->destination mapping and clears it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[BlockId, BlockId]]: A list mapping source
|
||||||
|
block indices to destination block indices.
|
||||||
|
"""
|
||||||
|
return self._cow_tracker.clear_cows()
|
||||||
|
|
||||||
|
def mark_blocks_as_accessed(self, block_ids: List[int],
|
||||||
|
now: float) -> None:
|
||||||
|
"""Mark blocks as accessed, used in prefix caching.
|
||||||
|
|
||||||
|
Since the naive allocator does not implement prefix caching, we do
|
||||||
|
nothing.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mark_blocks_as_computed(self, block_ids: List[int]) -> None:
|
||||||
|
"""Mark blocks as computed, used in prefix caching.
|
||||||
|
|
||||||
|
Since the naive allocator does not implement prefix caching, we do
|
||||||
|
nothing.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, computed_seq_block_ids: List[List[int]]) -> List[int]:
|
||||||
|
"""Determine blocks that can be skipped in prefill.
|
||||||
|
|
||||||
|
Since the naive allocator does not support prefix caching, always return
|
||||||
|
an empty list.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
|
def promote_to_immutable_block(self, block: Block) -> BlockId:
|
||||||
|
raise NotImplementedError("There is no promotion for naive blocks")
|
||||||
|
|
||||||
|
def get_num_full_blocks_touched(self, blocks: List[Block]) -> int:
|
||||||
|
"""Returns the number of full blocks that will be touched by
|
||||||
|
swapping in/out.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
blocks: List of blocks to be swapped.
|
||||||
|
Returns:
|
||||||
|
int: the number of full blocks that will be touched by
|
||||||
|
swapping in/out the given blocks. Non full blocks are ignored
|
||||||
|
when deciding the number of blocks to touch.
|
||||||
|
"""
|
||||||
|
# NOTE: for naive block, we use set to eliminate common blocks among
|
||||||
|
# seqs, also we compare the empty slots in the mutable blocks with
|
||||||
|
# lookahead slots to get the number of unique new block that are
|
||||||
|
# needed.
|
||||||
|
old_block_set = set()
|
||||||
|
for block in blocks:
|
||||||
|
if block.is_full:
|
||||||
|
old_block_set.add(block)
|
||||||
|
return len(old_block_set)
|
||||||
|
|
||||||
|
def swap_out(self, blocks: List[Block]) -> None:
|
||||||
|
for block in blocks:
|
||||||
|
self._free_block_id(block)
|
||||||
|
|
||||||
|
def swap_in(self, blocks: List[Block]) -> None:
|
||||||
|
for block in blocks:
|
||||||
|
# Here we allocate either immutable or mutable block and then
|
||||||
|
# extract its block_id. Note that the block object is released
|
||||||
|
# and the block_id is assigned to "block" to allow reusing the
|
||||||
|
# existing "block" object
|
||||||
|
if block.is_full:
|
||||||
|
tmp_block = self.allocate_immutable_block(
|
||||||
|
prev_block=block.prev_block, token_ids=block.token_ids)
|
||||||
|
else:
|
||||||
|
tmp_block = self.allocate_mutable_block(
|
||||||
|
prev_block=block.prev_block)
|
||||||
|
tmp_block.append_token_ids(block.token_ids)
|
||||||
|
|
||||||
|
block_id = tmp_block.block_id
|
||||||
|
tmp_block.block_id = None
|
||||||
|
self._block_pool.free_block(tmp_block)
|
||||||
|
|
||||||
|
block.block_id = block_id # Assign block_id
|
||||||
|
|
||||||
|
def get_prefix_cache_hit_rate(self) -> float:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def reset_prefix_cache(self) -> bool:
|
||||||
|
"""No prefix cache for naive block allocator."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def find_cached_blocks_prefix(self, block_hashes: List[int]) -> List[int]:
|
||||||
|
# Not applicable for naive block allocator.
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class NaiveBlock(Block):
|
||||||
|
"""An implementation of the Block class that does not support prefix
|
||||||
|
caching.
|
||||||
|
|
||||||
|
The NaiveBlock class represents a block of token IDs with a fixed size. It
|
||||||
|
provides methods for appending token IDs to the block and manages copy-on
|
||||||
|
-write operations when necessary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prev_block (Block): The previous block in the sequence.
|
||||||
|
token_ids (List[int]): The initial token IDs to be stored in the block.
|
||||||
|
block_size (int): The maximum number of token IDs that can be stored in
|
||||||
|
the block.
|
||||||
|
allocator (BlockAllocator): The block allocator associated with this
|
||||||
|
block.
|
||||||
|
block_id (Optional[int], optional): The physical block index
|
||||||
|
of this block. Defaults to None, which means no allocation has been
|
||||||
|
made.
|
||||||
|
_cow_target (Optional[Block], optional): The copy-on-write target block.
|
||||||
|
If not provided, it defaults to self.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
prev_block: Optional[Block],
|
||||||
|
token_ids: List[int],
|
||||||
|
block_size: int,
|
||||||
|
allocator: BlockAllocator,
|
||||||
|
block_id: Optional[int] = None,
|
||||||
|
_cow_target: Optional[Block] = None,
|
||||||
|
extra_hash: Optional[int] = None):
|
||||||
|
self._token_ids: List[int] = []
|
||||||
|
self._block_size = block_size
|
||||||
|
self._prev_block = prev_block
|
||||||
|
self._block_id = block_id
|
||||||
|
self._allocator = allocator
|
||||||
|
self._cow_target = _cow_target if _cow_target is not None else self
|
||||||
|
|
||||||
|
self._append_token_ids_no_cow(token_ids)
|
||||||
|
|
||||||
|
def append_token_ids(self, token_ids: List[int]) -> None:
|
||||||
|
"""Appends the given token IDs to the block and performs a
|
||||||
|
copy-on-write if necessary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids (Optional[List[int]]): The token IDs to be appended
|
||||||
|
to the block.
|
||||||
|
"""
|
||||||
|
self._append_token_ids_no_cow(token_ids)
|
||||||
|
|
||||||
|
if self._block_id is not None:
|
||||||
|
self._block_id = (self._allocator.cow_block_if_not_appendable(
|
||||||
|
self._cow_target))
|
||||||
|
|
||||||
|
def _append_token_ids_no_cow(self, token_ids: List[int]) -> None:
|
||||||
|
"""Appends the given token IDs to the block
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token_ids (List[int]): The token IDs to be appended to the block.
|
||||||
|
"""
|
||||||
|
if len(token_ids) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
assert len(token_ids) <= self.num_empty_slots
|
||||||
|
|
||||||
|
self._token_ids.extend(token_ids)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def computed(self) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@computed.setter
|
||||||
|
def computed(self, value) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_accessed(self) -> float:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@last_accessed.setter
|
||||||
|
def last_accessed(self, last_accessed_ts: float):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block_id(self) -> Optional[int]:
|
||||||
|
return self._block_id
|
||||||
|
|
||||||
|
@block_id.setter
|
||||||
|
def block_id(self, value: Optional[int]) -> None:
|
||||||
|
self._block_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_full(self) -> bool:
|
||||||
|
return self.num_empty_slots == 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_empty_slots(self) -> int:
|
||||||
|
return self._block_size - len(self.token_ids)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_ids(self) -> List[int]:
|
||||||
|
return self._token_ids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_tokens_total(self) -> int:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"num_tokens_total is not used for naive block")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def block_size(self) -> int:
|
||||||
|
return self._block_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prev_block(self) -> Optional["Block"]:
|
||||||
|
return self._prev_block
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_hash(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content_hash(self) -> Optional[int]:
|
||||||
|
return None
|
||||||
1134
vllm/core/block/prefix_caching_block.py
Normal file
1134
vllm/core/block/prefix_caching_block.py
Normal file
File diff suppressed because it is too large
Load Diff
27
vllm/core/block/utils.py
Normal file
27
vllm/core/block/utils.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Block manager utils."""
|
||||||
|
from vllm.sequence import SequenceGroup
|
||||||
|
from vllm.utils import (STR_NOT_IMPL_ENC_DEC_PREFIX_CACHE,
|
||||||
|
STR_NOT_IMPL_ENC_DEC_SWA)
|
||||||
|
|
||||||
|
|
||||||
|
def check_no_caching_or_swa_for_blockmgr_encdec(
|
||||||
|
block_mgr, seq_group: SequenceGroup) -> None:
|
||||||
|
'''
|
||||||
|
Enforce that prefix caching & sliding-window attention (SWA)
|
||||||
|
are currently unsupported *specifically* for encoder/decoder models.
|
||||||
|
|
||||||
|
Raises NotImplementedError if unsupported scenario is detected.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
|
||||||
|
* block_mgr: BlockSpaceManager instance
|
||||||
|
* seq_group: SequenceGroup passed to block_mgr
|
||||||
|
'''
|
||||||
|
|
||||||
|
if seq_group.is_encoder_decoder():
|
||||||
|
if block_mgr.max_block_sliding_window is not None:
|
||||||
|
raise NotImplementedError(STR_NOT_IMPL_ENC_DEC_SWA)
|
||||||
|
|
||||||
|
if block_mgr.enable_caching:
|
||||||
|
raise NotImplementedError(STR_NOT_IMPL_ENC_DEC_PREFIX_CACHE)
|
||||||
520
vllm/core/block_manager.py
Normal file
520
vllm/core/block_manager.py
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""A block manager that manages token blocks."""
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
from typing import Sequence as GenericSequence
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from vllm.core.block.block_table import BlockTable
|
||||||
|
from vllm.core.block.cpu_gpu_block_allocator import CpuGpuBlockAllocator
|
||||||
|
from vllm.core.block.interfaces import Block
|
||||||
|
from vllm.core.block.prefix_caching_block import (ComputedBlocksTracker,
|
||||||
|
LastAccessBlocksTracker)
|
||||||
|
from vllm.core.block.utils import check_no_caching_or_swa_for_blockmgr_encdec
|
||||||
|
from vllm.core.interfaces import AllocStatus, BlockSpaceManager
|
||||||
|
from vllm.sequence import Sequence, SequenceGroup, SequenceStatus
|
||||||
|
from vllm.utils import Device
|
||||||
|
|
||||||
|
SeqId = int
|
||||||
|
EncoderSeqId = str
|
||||||
|
|
||||||
|
|
||||||
|
class SelfAttnBlockSpaceManager(BlockSpaceManager):
|
||||||
|
"""BlockSpaceManager which manages the allocation of KV cache.
|
||||||
|
|
||||||
|
It owns responsibility for allocation, swapping, allocating memory for
|
||||||
|
autoregressively-generated tokens, and other advanced features such as
|
||||||
|
prefix caching, forking/copy-on-write, and sliding-window memory allocation.
|
||||||
|
|
||||||
|
This class implements the design described in
|
||||||
|
https://github.com/vllm-project/vllm/pull/3492.
|
||||||
|
|
||||||
|
Lookahead slots
|
||||||
|
The block manager has the notion of a "lookahead slot". These are slots
|
||||||
|
in the KV cache that are allocated for a sequence. Unlike the other
|
||||||
|
allocated slots, the content of these slots is undefined -- the worker
|
||||||
|
may use the memory allocations in any way.
|
||||||
|
|
||||||
|
In practice, a worker could use these lookahead slots to run multiple
|
||||||
|
forward passes for a single scheduler invocation. Each successive
|
||||||
|
forward pass would write KV activations to the corresponding lookahead
|
||||||
|
slot. This allows low inter-token latency use-cases, where the overhead
|
||||||
|
of continuous batching scheduling is amortized over >1 generated tokens.
|
||||||
|
|
||||||
|
Speculative decoding uses lookahead slots to store KV activations of
|
||||||
|
proposal tokens.
|
||||||
|
|
||||||
|
See https://github.com/vllm-project/vllm/pull/3250 for more information
|
||||||
|
on lookahead scheduling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block_size (int): The size of each memory block.
|
||||||
|
num_gpu_blocks (int): The number of memory blocks allocated on GPU.
|
||||||
|
num_cpu_blocks (int): The number of memory blocks allocated on CPU.
|
||||||
|
watermark (float, optional): The threshold used for memory swapping.
|
||||||
|
Defaults to 0.01.
|
||||||
|
sliding_window (Optional[int], optional): The size of the sliding
|
||||||
|
window. Defaults to None.
|
||||||
|
enable_caching (bool, optional): Flag indicating whether caching is
|
||||||
|
enabled. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
block_size: int,
|
||||||
|
num_gpu_blocks: int,
|
||||||
|
num_cpu_blocks: int,
|
||||||
|
watermark: float = 0.01,
|
||||||
|
sliding_window: Optional[int] = None,
|
||||||
|
enable_caching: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.block_size = block_size
|
||||||
|
self.num_total_gpu_blocks = num_gpu_blocks
|
||||||
|
self.num_total_cpu_blocks = num_cpu_blocks
|
||||||
|
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
# max_block_sliding_window is the max number of blocks that need to be
|
||||||
|
# allocated
|
||||||
|
self.max_block_sliding_window = None
|
||||||
|
if sliding_window is not None:
|
||||||
|
# +1 here because // rounds down
|
||||||
|
num_blocks = sliding_window // block_size + 1
|
||||||
|
# +1 here because the last block may not be full,
|
||||||
|
# and so the sequence stretches one more block at the beginning
|
||||||
|
# For example, if sliding_window is 3 and block_size is 4,
|
||||||
|
# we may need 2 blocks when the second block only holds 1 token.
|
||||||
|
self.max_block_sliding_window = num_blocks + 1
|
||||||
|
|
||||||
|
self.watermark = watermark
|
||||||
|
assert watermark >= 0.0
|
||||||
|
|
||||||
|
self.enable_caching = enable_caching
|
||||||
|
|
||||||
|
self.watermark_blocks = int(watermark * num_gpu_blocks)
|
||||||
|
|
||||||
|
self.block_allocator = CpuGpuBlockAllocator.create(
|
||||||
|
allocator_type="prefix_caching" if enable_caching else "naive",
|
||||||
|
num_gpu_blocks=num_gpu_blocks,
|
||||||
|
num_cpu_blocks=num_cpu_blocks,
|
||||||
|
block_size=block_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.block_tables: Dict[SeqId, BlockTable] = {}
|
||||||
|
self.cross_block_tables: Dict[EncoderSeqId, BlockTable] = {}
|
||||||
|
|
||||||
|
self._computed_blocks_tracker = ComputedBlocksTracker(
|
||||||
|
self.block_allocator, self.block_size, self.enable_caching)
|
||||||
|
self._last_access_blocks_tracker = LastAccessBlocksTracker(
|
||||||
|
self.block_allocator)
|
||||||
|
|
||||||
|
def can_allocate(self,
|
||||||
|
seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int = 0) -> AllocStatus:
|
||||||
|
# FIXME(woosuk): Here we assume that all sequences in the group share
|
||||||
|
# the same prompt. This may not be true for preempted sequences.
|
||||||
|
|
||||||
|
check_no_caching_or_swa_for_blockmgr_encdec(self, seq_group)
|
||||||
|
|
||||||
|
seq = seq_group.get_seqs(status=SequenceStatus.WAITING)[0]
|
||||||
|
num_required_blocks = BlockTable.get_num_required_blocks(
|
||||||
|
seq.get_token_ids(),
|
||||||
|
block_size=self.block_size,
|
||||||
|
num_lookahead_slots=num_lookahead_slots,
|
||||||
|
)
|
||||||
|
|
||||||
|
if seq_group.is_encoder_decoder():
|
||||||
|
encoder_seq = seq_group.get_encoder_seq()
|
||||||
|
assert encoder_seq is not None
|
||||||
|
num_required_blocks += BlockTable.get_num_required_blocks(
|
||||||
|
encoder_seq.get_token_ids(),
|
||||||
|
block_size=self.block_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.max_block_sliding_window is not None:
|
||||||
|
num_required_blocks = min(num_required_blocks,
|
||||||
|
self.max_block_sliding_window)
|
||||||
|
|
||||||
|
num_free_gpu_blocks = self.block_allocator.get_num_free_blocks(
|
||||||
|
device=Device.GPU)
|
||||||
|
|
||||||
|
# Use watermark to avoid frequent cache eviction.
|
||||||
|
if (self.num_total_gpu_blocks - num_required_blocks
|
||||||
|
< self.watermark_blocks):
|
||||||
|
return AllocStatus.NEVER
|
||||||
|
if num_free_gpu_blocks - num_required_blocks >= self.watermark_blocks:
|
||||||
|
return AllocStatus.OK
|
||||||
|
else:
|
||||||
|
return AllocStatus.LATER
|
||||||
|
|
||||||
|
def _allocate_sequence(self, seq: Sequence) -> BlockTable:
|
||||||
|
block_table = BlockTable(
|
||||||
|
block_size=self.block_size,
|
||||||
|
block_allocator=self.block_allocator,
|
||||||
|
max_block_sliding_window=self.max_block_sliding_window,
|
||||||
|
)
|
||||||
|
if seq.get_token_ids():
|
||||||
|
# NOTE: If there are any factors affecting the block besides
|
||||||
|
# token_ids, they should be added as input to extra_hash.
|
||||||
|
extra_hash = seq.extra_hash()
|
||||||
|
|
||||||
|
# Add blocks to the block table only if the sequence is non empty.
|
||||||
|
block_table.allocate(token_ids=seq.get_token_ids(),
|
||||||
|
extra_hash=extra_hash)
|
||||||
|
|
||||||
|
return block_table
|
||||||
|
|
||||||
|
def allocate(self, seq_group: SequenceGroup) -> None:
|
||||||
|
|
||||||
|
# Allocate self-attention block tables for decoder sequences
|
||||||
|
waiting_seqs = seq_group.get_seqs(status=SequenceStatus.WAITING)
|
||||||
|
assert not (set(seq.seq_id for seq in waiting_seqs)
|
||||||
|
& self.block_tables.keys()), "block table already exists"
|
||||||
|
|
||||||
|
# NOTE: Here we assume that all sequences in the group have the same
|
||||||
|
# prompt.
|
||||||
|
seq = waiting_seqs[0]
|
||||||
|
block_table: BlockTable = self._allocate_sequence(seq)
|
||||||
|
self.block_tables[seq.seq_id] = block_table
|
||||||
|
|
||||||
|
# Track seq
|
||||||
|
self._last_access_blocks_tracker.add_seq(seq.seq_id)
|
||||||
|
|
||||||
|
# Assign the block table for each sequence.
|
||||||
|
for seq in waiting_seqs[1:]:
|
||||||
|
self.block_tables[seq.seq_id] = block_table.fork()
|
||||||
|
|
||||||
|
# Track seq
|
||||||
|
self._last_access_blocks_tracker.add_seq(seq.seq_id)
|
||||||
|
|
||||||
|
# Allocate cross-attention block table for encoder sequence
|
||||||
|
#
|
||||||
|
# NOTE: Here we assume that all sequences in the group have the same
|
||||||
|
# encoder prompt.
|
||||||
|
request_id = seq_group.request_id
|
||||||
|
|
||||||
|
assert (request_id
|
||||||
|
not in self.cross_block_tables), \
|
||||||
|
"block table already exists"
|
||||||
|
|
||||||
|
check_no_caching_or_swa_for_blockmgr_encdec(self, seq_group)
|
||||||
|
|
||||||
|
if seq_group.is_encoder_decoder():
|
||||||
|
encoder_seq = seq_group.get_encoder_seq()
|
||||||
|
assert encoder_seq is not None
|
||||||
|
block_table = self._allocate_sequence(encoder_seq)
|
||||||
|
self.cross_block_tables[request_id] = block_table
|
||||||
|
|
||||||
|
def can_append_slots(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> bool:
|
||||||
|
"""Determine if there is enough space in the GPU KV cache to continue
|
||||||
|
generation of the specified sequence group.
|
||||||
|
|
||||||
|
We use a worst-case heuristic: assume each touched block will require a
|
||||||
|
new allocation (either via CoW or new block). We can append slots if the
|
||||||
|
number of touched blocks is less than the number of free blocks.
|
||||||
|
|
||||||
|
"Lookahead slots" are slots that are allocated in addition to the slots
|
||||||
|
for known tokens. The contents of the lookahead slots are not defined.
|
||||||
|
This is used by speculative decoding when speculating future tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
num_touched_blocks = 0
|
||||||
|
for seq in seq_group.get_seqs(status=SequenceStatus.RUNNING):
|
||||||
|
block_table = self.block_tables[seq.seq_id]
|
||||||
|
|
||||||
|
num_touched_blocks += (
|
||||||
|
block_table.get_num_blocks_touched_by_append_slots(
|
||||||
|
token_ids=block_table.get_unseen_token_ids(
|
||||||
|
seq.get_token_ids()),
|
||||||
|
num_lookahead_slots=num_lookahead_slots,
|
||||||
|
))
|
||||||
|
|
||||||
|
num_free_gpu_blocks = self.block_allocator.get_num_free_blocks(
|
||||||
|
Device.GPU)
|
||||||
|
return num_touched_blocks <= num_free_gpu_blocks
|
||||||
|
|
||||||
|
def append_slots(
|
||||||
|
self,
|
||||||
|
seq: Sequence,
|
||||||
|
num_lookahead_slots: int,
|
||||||
|
) -> List[Tuple[int, int]]:
|
||||||
|
|
||||||
|
block_table = self.block_tables[seq.seq_id]
|
||||||
|
|
||||||
|
block_table.append_token_ids(
|
||||||
|
token_ids=block_table.get_unseen_token_ids(seq.get_token_ids()),
|
||||||
|
num_lookahead_slots=num_lookahead_slots,
|
||||||
|
num_computed_slots=seq.data.get_num_computed_tokens(),
|
||||||
|
extra_hash=seq.extra_hash(),
|
||||||
|
)
|
||||||
|
# Return any new copy-on-writes.
|
||||||
|
new_cows = self.block_allocator.clear_copy_on_writes()
|
||||||
|
return new_cows
|
||||||
|
|
||||||
|
def free(self, seq: Sequence) -> None:
|
||||||
|
seq_id = seq.seq_id
|
||||||
|
|
||||||
|
if seq_id not in self.block_tables:
|
||||||
|
# Already freed or haven't been scheduled yet.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update seq block ids with the latest access time
|
||||||
|
self._last_access_blocks_tracker.update_seq_blocks_last_access(
|
||||||
|
seq_id, self.block_tables[seq.seq_id].physical_block_ids)
|
||||||
|
|
||||||
|
# Untrack seq
|
||||||
|
self._last_access_blocks_tracker.remove_seq(seq_id)
|
||||||
|
self._computed_blocks_tracker.remove_seq(seq_id)
|
||||||
|
|
||||||
|
# Free table/blocks
|
||||||
|
self.block_tables[seq_id].free()
|
||||||
|
del self.block_tables[seq_id]
|
||||||
|
|
||||||
|
def free_cross(self, seq_group: SequenceGroup) -> None:
|
||||||
|
request_id = seq_group.request_id
|
||||||
|
if request_id not in self.cross_block_tables:
|
||||||
|
# Already freed or hasn't been scheduled yet.
|
||||||
|
return
|
||||||
|
self.cross_block_tables[request_id].free()
|
||||||
|
del self.cross_block_tables[request_id]
|
||||||
|
|
||||||
|
def get_block_table(self, seq: Sequence) -> List[int]:
|
||||||
|
block_ids = self.block_tables[seq.seq_id].physical_block_ids
|
||||||
|
return block_ids # type: ignore
|
||||||
|
|
||||||
|
def get_cross_block_table(self, seq_group: SequenceGroup) -> List[int]:
|
||||||
|
request_id = seq_group.request_id
|
||||||
|
assert request_id in self.cross_block_tables
|
||||||
|
block_ids = self.cross_block_tables[request_id].physical_block_ids
|
||||||
|
assert all(b is not None for b in block_ids)
|
||||||
|
return block_ids # type: ignore
|
||||||
|
|
||||||
|
def access_all_blocks_in_seq(self, seq: Sequence, now: float):
|
||||||
|
if self.enable_caching:
|
||||||
|
# Record the latest access time for the sequence. The actual update
|
||||||
|
# of the block ids is deferred to the sequence free(..) call, since
|
||||||
|
# only during freeing of block ids, the blocks are actually added to
|
||||||
|
# the evictor (which is when the most updated time is required)
|
||||||
|
# (This avoids expensive calls to mark_blocks_as_accessed(..))
|
||||||
|
self._last_access_blocks_tracker.update_last_access(
|
||||||
|
seq.seq_id, now)
|
||||||
|
|
||||||
|
def mark_blocks_as_computed(self, seq_group: SequenceGroup,
|
||||||
|
token_chunk_size: int):
|
||||||
|
# If prefix caching is enabled, mark immutable blocks as computed
|
||||||
|
# right after they have been scheduled (for prefill). This assumes
|
||||||
|
# the scheduler is synchronous so blocks are actually computed when
|
||||||
|
# scheduling the next batch.
|
||||||
|
self.block_allocator.mark_blocks_as_computed([])
|
||||||
|
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, seqs: List[Sequence]) -> GenericSequence[int]:
|
||||||
|
"""Determine which blocks for which we skip prefill.
|
||||||
|
|
||||||
|
With prefix caching we can skip prefill for previously-generated blocks.
|
||||||
|
Currently, the attention implementation only supports skipping cached
|
||||||
|
blocks if they are a contiguous prefix of cached blocks.
|
||||||
|
|
||||||
|
This method determines which blocks can be safely skipped for all
|
||||||
|
sequences in the sequence group.
|
||||||
|
"""
|
||||||
|
computed_seq_block_ids = []
|
||||||
|
for seq in seqs:
|
||||||
|
all_blocks = self.block_tables[seq.seq_id].physical_block_ids
|
||||||
|
num_cached_tokens = (
|
||||||
|
self._computed_blocks_tracker.get_num_cached_tokens(seq))
|
||||||
|
assert num_cached_tokens % self.block_size == 0
|
||||||
|
num_cached_blocks = num_cached_tokens // self.block_size
|
||||||
|
computed_block_ids = all_blocks[:num_cached_blocks]
|
||||||
|
computed_seq_block_ids.append(computed_block_ids)
|
||||||
|
|
||||||
|
# NOTE(sang): This assumes seq_block_ids doesn't contain any None.
|
||||||
|
return self.block_allocator.get_common_computed_block_ids(
|
||||||
|
computed_seq_block_ids) # type: ignore
|
||||||
|
|
||||||
|
def fork(self, parent_seq: Sequence, child_seq: Sequence) -> None:
|
||||||
|
if parent_seq.seq_id not in self.block_tables:
|
||||||
|
# Parent sequence has either been freed or never existed.
|
||||||
|
return
|
||||||
|
src_block_table = self.block_tables[parent_seq.seq_id]
|
||||||
|
self.block_tables[child_seq.seq_id] = src_block_table.fork()
|
||||||
|
|
||||||
|
# Track child seq
|
||||||
|
self._last_access_blocks_tracker.add_seq(child_seq.seq_id)
|
||||||
|
|
||||||
|
def can_swap_in(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> AllocStatus:
|
||||||
|
"""Returns the AllocStatus for the given sequence_group
|
||||||
|
with num_lookahead_slots.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence_group (SequenceGroup): The sequence group to swap in.
|
||||||
|
num_lookahead_slots (int): Number of lookahead slots used in
|
||||||
|
speculative decoding, default to 0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AllocStatus: The AllocStatus for the given sequence group.
|
||||||
|
"""
|
||||||
|
return self._can_swap(seq_group, Device.GPU, SequenceStatus.SWAPPED,
|
||||||
|
num_lookahead_slots)
|
||||||
|
|
||||||
|
def swap_in(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
"""Returns the block id mapping (from CPU to GPU) generated by
|
||||||
|
swapping in the given seq_group with num_lookahead_slots.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_group (SequenceGroup): The sequence group to swap in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[int, int]]: The mapping of swapping block from CPU
|
||||||
|
to GPU.
|
||||||
|
"""
|
||||||
|
physical_block_id_mapping = []
|
||||||
|
for seq in seq_group.get_seqs(status=SequenceStatus.SWAPPED):
|
||||||
|
blocks = self.block_tables[seq.seq_id].blocks
|
||||||
|
if len(blocks) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seq_swap_mapping = self.block_allocator.swap(blocks=blocks,
|
||||||
|
src_device=Device.CPU,
|
||||||
|
dst_device=Device.GPU)
|
||||||
|
|
||||||
|
# Refresh the block ids of the table (post-swap)
|
||||||
|
self.block_tables[seq.seq_id].update(blocks)
|
||||||
|
|
||||||
|
seq_physical_block_id_mapping = {
|
||||||
|
self.block_allocator.get_physical_block_id(
|
||||||
|
Device.CPU, cpu_block_id):
|
||||||
|
self.block_allocator.get_physical_block_id(
|
||||||
|
Device.GPU, gpu_block_id)
|
||||||
|
for cpu_block_id, gpu_block_id in seq_swap_mapping.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
physical_block_id_mapping.extend(
|
||||||
|
list(seq_physical_block_id_mapping.items()))
|
||||||
|
|
||||||
|
return physical_block_id_mapping
|
||||||
|
|
||||||
|
def can_swap_out(self, seq_group: SequenceGroup) -> bool:
|
||||||
|
"""Returns whether we can swap out the given sequence_group
|
||||||
|
with num_lookahead_slots.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seq_group (SequenceGroup): The sequence group to swap out.
|
||||||
|
num_lookahead_slots (int): Number of lookahead slots used in
|
||||||
|
speculative decoding, default to 0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether it's possible to swap out current sequence group.
|
||||||
|
"""
|
||||||
|
alloc_status = self._can_swap(seq_group, Device.CPU,
|
||||||
|
SequenceStatus.RUNNING)
|
||||||
|
return alloc_status == AllocStatus.OK
|
||||||
|
|
||||||
|
def swap_out(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
"""Returns the block id mapping (from GPU to CPU) generated by
|
||||||
|
swapping out the given sequence_group with num_lookahead_slots.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence_group (SequenceGroup): The sequence group to swap out.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tuple[int, int]]: The mapping of swapping block from
|
||||||
|
GPU to CPU.
|
||||||
|
"""
|
||||||
|
physical_block_id_mapping = []
|
||||||
|
for seq in seq_group.get_seqs(status=SequenceStatus.RUNNING):
|
||||||
|
blocks = self.block_tables[seq.seq_id].blocks
|
||||||
|
if len(blocks) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seq_swap_mapping = self.block_allocator.swap(blocks=blocks,
|
||||||
|
src_device=Device.GPU,
|
||||||
|
dst_device=Device.CPU)
|
||||||
|
|
||||||
|
# Refresh the block ids of the table (post-swap)
|
||||||
|
self.block_tables[seq.seq_id].update(blocks)
|
||||||
|
|
||||||
|
seq_physical_block_id_mapping = {
|
||||||
|
self.block_allocator.get_physical_block_id(
|
||||||
|
Device.GPU, gpu_block_id):
|
||||||
|
self.block_allocator.get_physical_block_id(
|
||||||
|
Device.CPU, cpu_block_id)
|
||||||
|
for gpu_block_id, cpu_block_id in seq_swap_mapping.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
physical_block_id_mapping.extend(
|
||||||
|
list(seq_physical_block_id_mapping.items()))
|
||||||
|
|
||||||
|
return physical_block_id_mapping
|
||||||
|
|
||||||
|
def get_num_free_gpu_blocks(self) -> int:
|
||||||
|
return self.block_allocator.get_num_free_blocks(Device.GPU)
|
||||||
|
|
||||||
|
def get_num_free_cpu_blocks(self) -> int:
|
||||||
|
return self.block_allocator.get_num_free_blocks(Device.CPU)
|
||||||
|
|
||||||
|
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||||
|
return self.block_allocator.get_prefix_cache_hit_rate(device)
|
||||||
|
|
||||||
|
def reset_prefix_cache(self, device: Optional[Device] = None) -> bool:
|
||||||
|
return self.block_allocator.reset_prefix_cache(device)
|
||||||
|
|
||||||
|
def _can_swap(self,
|
||||||
|
seq_group: SequenceGroup,
|
||||||
|
device: Device,
|
||||||
|
status: SequenceStatus,
|
||||||
|
num_lookahead_slots: int = 0) -> AllocStatus:
|
||||||
|
"""Returns the AllocStatus for swapping in/out the given sequence_group
|
||||||
|
on to the 'device'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sequence_group (SequenceGroup): The sequence group to swap in/out.
|
||||||
|
device (Device): device to swap the 'seq_group' on.
|
||||||
|
status (SequenceStatus): The status of sequence which is needed
|
||||||
|
for action. RUNNING for swap out and SWAPPED for swap in
|
||||||
|
num_lookahead_slots (int): Number of lookahead slots used in
|
||||||
|
speculative decoding, default to 0.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AllocStatus: The AllocStatus for swapping in/out the given
|
||||||
|
sequence_group on to the 'device'.
|
||||||
|
"""
|
||||||
|
# First determine the number of blocks that will be touched by this
|
||||||
|
# swap. Then verify if there are available blocks in the device
|
||||||
|
# to perform the swap.
|
||||||
|
num_blocks_touched = 0
|
||||||
|
blocks: List[Block] = []
|
||||||
|
for seq in seq_group.get_seqs(status=status):
|
||||||
|
block_table = self.block_tables[seq.seq_id]
|
||||||
|
if block_table.blocks is not None:
|
||||||
|
# Compute the number blocks to touch for the tokens to be
|
||||||
|
# appended. This does NOT include the full blocks that need
|
||||||
|
# to be touched for the swap.
|
||||||
|
num_blocks_touched += \
|
||||||
|
block_table.get_num_blocks_touched_by_append_slots(
|
||||||
|
block_table.get_unseen_token_ids(seq.get_token_ids()),
|
||||||
|
num_lookahead_slots=num_lookahead_slots)
|
||||||
|
blocks.extend(block_table.blocks)
|
||||||
|
# Compute the number of full blocks to touch and add it to the
|
||||||
|
# existing count of blocks to touch.
|
||||||
|
num_blocks_touched += self.block_allocator.get_num_full_blocks_touched(
|
||||||
|
blocks, device=device)
|
||||||
|
|
||||||
|
watermark_blocks = 0
|
||||||
|
if device == Device.GPU:
|
||||||
|
watermark_blocks = self.watermark_blocks
|
||||||
|
|
||||||
|
if self.block_allocator.get_num_total_blocks(
|
||||||
|
device) < num_blocks_touched:
|
||||||
|
return AllocStatus.NEVER
|
||||||
|
elif self.block_allocator.get_num_free_blocks(
|
||||||
|
device) - num_blocks_touched >= watermark_blocks:
|
||||||
|
return AllocStatus.OK
|
||||||
|
else:
|
||||||
|
return AllocStatus.LATER
|
||||||
|
|
||||||
|
def get_num_cached_tokens(self, seq: Sequence) -> int:
|
||||||
|
"""Get the number of tokens in blocks that are already computed and
|
||||||
|
cached in the block manager for the sequence.
|
||||||
|
"""
|
||||||
|
return self._computed_blocks_tracker.get_num_cached_tokens(seq)
|
||||||
156
vllm/core/evictor.py
Normal file
156
vllm/core/evictor.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import heapq
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class EvictionPolicy(enum.Enum):
|
||||||
|
"""Enum for eviction policy used by make_evictor to instantiate the correct
|
||||||
|
Evictor subclass.
|
||||||
|
"""
|
||||||
|
LRU = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Evictor(ABC):
|
||||||
|
"""The Evictor subclasses should be used by the BlockAllocator class to
|
||||||
|
handle eviction of freed Blocks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __contains__(self, block_id: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def evict(self) -> Tuple[int, int]:
|
||||||
|
"""Runs the eviction algorithm and returns the evicted block's
|
||||||
|
content hash along with physical block id along with physical block id
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add(self, block_id: int, content_hash: int, num_hashed_tokens: int,
|
||||||
|
last_accessed: float):
|
||||||
|
"""Adds block to the evictor, making it a candidate for eviction"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, block_id: int, last_accessed: float):
|
||||||
|
"""Update corresponding block's access time in metadata"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove(self, block_id: int):
|
||||||
|
"""Remove a given block id from the cache."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def num_blocks(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BlockMetaData:
|
||||||
|
"""Data structure for storing key data describe cached block, so that
|
||||||
|
evitor could use to make its decision which one to choose for eviction
|
||||||
|
|
||||||
|
Here we use physical block id as the dict key, as there maybe several
|
||||||
|
blocks with the same content hash, but their physical id is unique.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, content_hash: int, num_hashed_tokens: int,
|
||||||
|
last_accessed: float):
|
||||||
|
self.content_hash = content_hash
|
||||||
|
self.num_hashed_tokens = num_hashed_tokens
|
||||||
|
self.last_accessed = last_accessed
|
||||||
|
|
||||||
|
|
||||||
|
class LRUEvictor(Evictor):
|
||||||
|
"""Evicts in a least-recently-used order using the last_accessed timestamp
|
||||||
|
that's recorded in the Block. If there are multiple blocks with
|
||||||
|
the same last_accessed time, then the one with the largest num_hashed_tokens
|
||||||
|
will be evicted. If two blocks each have the lowest last_accessed time and
|
||||||
|
highest num_hashed_tokens value, then one will be chose arbitrarily
|
||||||
|
"""
|
||||||
|
|
||||||
|
# CLEANUP_THRESHOLD determines the maximum allowable size of the priority
|
||||||
|
# queue relative to the free table size. When this threshold is exceeded,
|
||||||
|
# a cleanup operation is triggered to reduce memory usage.
|
||||||
|
CLEANUP_THRESHOLD = 50
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.free_table: Dict[int, BlockMetaData] = {}
|
||||||
|
self.priority_queue = []
|
||||||
|
|
||||||
|
def __contains__(self, block_id: int) -> bool:
|
||||||
|
return block_id in self.free_table
|
||||||
|
|
||||||
|
def evict(self) -> Tuple[int, int]:
|
||||||
|
if len(self.free_table) == 0:
|
||||||
|
raise ValueError("No usable cache memory left")
|
||||||
|
|
||||||
|
while self.priority_queue:
|
||||||
|
# We do not remove outdated entries from the priority queue at the
|
||||||
|
# time of updating the last_accessed timestamp. Instead, outdated
|
||||||
|
# entries are filtered out here during eviction. Outdated entries
|
||||||
|
# would either not in the free table, or have older last accessed
|
||||||
|
# time.
|
||||||
|
last_accessed, _, block_id, content_hash = heapq.heappop(
|
||||||
|
self.priority_queue)
|
||||||
|
if (block_id in self.free_table and
|
||||||
|
self.free_table[block_id].last_accessed == last_accessed):
|
||||||
|
self.free_table.pop(block_id)
|
||||||
|
return block_id, content_hash
|
||||||
|
|
||||||
|
raise ValueError("No usable cache memory left")
|
||||||
|
|
||||||
|
def add(self, block_id: int, content_hash: int, num_hashed_tokens: int,
|
||||||
|
last_accessed: float):
|
||||||
|
self.free_table[block_id] = BlockMetaData(content_hash,
|
||||||
|
num_hashed_tokens,
|
||||||
|
last_accessed)
|
||||||
|
heapq.heappush(
|
||||||
|
self.priority_queue,
|
||||||
|
(last_accessed, -num_hashed_tokens, block_id, content_hash))
|
||||||
|
self._cleanup_if_necessary()
|
||||||
|
|
||||||
|
def update(self, block_id: int, last_accessed: float):
|
||||||
|
self.free_table[block_id].last_accessed = last_accessed
|
||||||
|
|
||||||
|
def _cleanup_if_necessary(self):
|
||||||
|
if len(self.priority_queue) > LRUEvictor.CLEANUP_THRESHOLD * len(
|
||||||
|
self.free_table):
|
||||||
|
self._cleanup()
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
new_priority_queue: List[Tuple[float, int, int, int]] = []
|
||||||
|
|
||||||
|
for block_id, block in self.free_table.items():
|
||||||
|
new_priority_queue.append(
|
||||||
|
(block.last_accessed, -block.num_hashed_tokens, block_id,
|
||||||
|
block.content_hash))
|
||||||
|
heapq.heapify(new_priority_queue)
|
||||||
|
|
||||||
|
self.priority_queue = new_priority_queue
|
||||||
|
|
||||||
|
def remove(self, block_id: int):
|
||||||
|
if block_id not in self.free_table:
|
||||||
|
raise ValueError(
|
||||||
|
"Attempting to remove block that's not in the evictor")
|
||||||
|
self.free_table.pop(block_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_blocks(self) -> int:
|
||||||
|
return len(self.free_table)
|
||||||
|
|
||||||
|
|
||||||
|
def make_evictor(eviction_policy: EvictionPolicy) -> Evictor:
|
||||||
|
if eviction_policy == EvictionPolicy.LRU:
|
||||||
|
return LRUEvictor()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown cache eviction policy: {eviction_policy}")
|
||||||
134
vllm/core/interfaces.py
Normal file
134
vllm/core/interfaces.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import enum
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Optional
|
||||||
|
from typing import Sequence as GenericSequence
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from vllm.sequence import Sequence, SequenceGroup
|
||||||
|
from vllm.utils import Device
|
||||||
|
|
||||||
|
|
||||||
|
class AllocStatus(enum.Enum):
|
||||||
|
"""Result for BlockSpaceManager.can_allocate
|
||||||
|
|
||||||
|
1. Ok: seq_group can be allocated now.
|
||||||
|
2. Later: seq_group cannot be allocated.
|
||||||
|
The capacity of allocator is larger than seq_group required.
|
||||||
|
3. Never: seq_group can never be allocated.
|
||||||
|
The seq_group is too large to allocated in GPU.
|
||||||
|
"""
|
||||||
|
OK = enum.auto()
|
||||||
|
LATER = enum.auto()
|
||||||
|
NEVER = enum.auto()
|
||||||
|
|
||||||
|
|
||||||
|
class BlockSpaceManager(ABC):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_block_space_manager_class(version: str):
|
||||||
|
version = version.lower()
|
||||||
|
|
||||||
|
if version == "selfattn":
|
||||||
|
from vllm.core.block_manager import SelfAttnBlockSpaceManager
|
||||||
|
return SelfAttnBlockSpaceManager
|
||||||
|
|
||||||
|
if version == "placeholder":
|
||||||
|
from vllm.core.placeholder_block_space_manager import (
|
||||||
|
PlaceholderBlockSpaceManager)
|
||||||
|
return PlaceholderBlockSpaceManager
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown version {version=}")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_allocate(self,
|
||||||
|
seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int = 0) -> AllocStatus:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def allocate(self, seq_group: SequenceGroup) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_append_slots(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def append_slots(
|
||||||
|
self,
|
||||||
|
seq: Sequence,
|
||||||
|
num_lookahead_slots: int,
|
||||||
|
) -> List[Tuple[int, int]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def fork(self, parent_seq: Sequence, child_seq: Sequence) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_swap_in(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> AllocStatus:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def swap_in(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def can_swap_out(self, seq_group: SequenceGroup) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def swap_out(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def free(self, seq: Sequence) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_block_table(self, seq: Sequence) -> List[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_free_gpu_blocks(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_free_cpu_blocks(self) -> int:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def access_all_blocks_in_seq(
|
||||||
|
self,
|
||||||
|
seq: Sequence,
|
||||||
|
access_time: float,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_common_computed_block_ids(
|
||||||
|
self, seqs: List[Sequence]) -> GenericSequence[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def mark_blocks_as_computed(self, seq_group: SequenceGroup,
|
||||||
|
token_chunk_size: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||||
|
"""Prefix cache hit rate. -1 means not supported or disabled."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def reset_prefix_cache(self, device: Optional[Device] = None) -> bool:
|
||||||
|
"""Reset prefix cache for specified or all devices."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_num_cached_tokens(self, seq: Sequence) -> int:
|
||||||
|
pass
|
||||||
99
vllm/core/placeholder_block_space_manager.py
Normal file
99
vllm/core/placeholder_block_space_manager.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
from vllm.core.interfaces import AllocStatus, BlockSpaceManager
|
||||||
|
from vllm.sequence import Sequence, SequenceGroup
|
||||||
|
from vllm.utils import Device
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceholderBlockSpaceManager(BlockSpaceManager):
|
||||||
|
"""A version of BlockSpaceManager for use in environments
|
||||||
|
where block management is not required.
|
||||||
|
For example: pooling models or attention-free models like Mamba.
|
||||||
|
|
||||||
|
This class provides the same interface as BlockSpaceManager, but its
|
||||||
|
methods perform no actions or return simple values like True in specific
|
||||||
|
actions. It's designed to be used in scenarios where the overhead of
|
||||||
|
block management is unnecessary, such as in an embedding environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_allocate(self,
|
||||||
|
seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int = 0) -> AllocStatus:
|
||||||
|
# Always return OK for dummy purposes
|
||||||
|
return AllocStatus.OK
|
||||||
|
|
||||||
|
def allocate(self, seq_group: SequenceGroup) -> None:
|
||||||
|
# No actual allocation logic needed
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_append_slots(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def append_slots(
|
||||||
|
self,
|
||||||
|
seq: Sequence,
|
||||||
|
num_lookahead_slots: int,
|
||||||
|
) -> List[Tuple[int, int]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def fork(self, parent_seq: Sequence, child_seq: Sequence) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def can_swap_in(self, seq_group: SequenceGroup,
|
||||||
|
num_lookahead_slots: int) -> AllocStatus:
|
||||||
|
return AllocStatus.OK
|
||||||
|
|
||||||
|
def swap_in(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
return None # type: ignore
|
||||||
|
|
||||||
|
def can_swap_out(self, seq_group: SequenceGroup) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def swap_out(self, seq_group: SequenceGroup) -> List[Tuple[int, int]]:
|
||||||
|
return None # type: ignore
|
||||||
|
|
||||||
|
def free(self, seq: Sequence) -> None:
|
||||||
|
# No operation on free
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_block_table(self, seq: Sequence) -> List[int]:
|
||||||
|
return None # type: ignore
|
||||||
|
|
||||||
|
def get_num_free_gpu_blocks(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_num_free_cpu_blocks(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def access_all_blocks_in_seq(
|
||||||
|
self,
|
||||||
|
seq: Sequence,
|
||||||
|
access_time: float,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_common_computed_block_ids(self,
|
||||||
|
seq_group: List[Sequence]) -> List[int]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def mark_blocks_as_computed(self, seq_group: SequenceGroup,
|
||||||
|
token_chunk_size: int):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def reset_prefix_cache(self, device: Optional[Device] = None) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_num_cached_tokens(self, seq: Sequence) -> int:
|
||||||
|
return 0
|
||||||
2061
vllm/core/scheduler.py
Normal file
2061
vllm/core/scheduler.py
Normal file
File diff suppressed because it is too large
Load Diff
0
vllm/device_allocator/__init__.py
Normal file
0
vllm/device_allocator/__init__.py
Normal file
280
vllm/device_allocator/cumem.py
Normal file
280
vllm/device_allocator/cumem.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# cumem-based pytorch pluggable allocator to implement sleep mode.
|
||||||
|
# other approaches tried but failed:
|
||||||
|
# - cuda-python package binding
|
||||||
|
# - custom libcuda driver ctypes wrapper
|
||||||
|
# both of them failed because of cuda context mismatch.
|
||||||
|
# not sure why, they are created from a different context.
|
||||||
|
# the only successful approach is to call cuda driver API in C.
|
||||||
|
import dataclasses
|
||||||
|
import gc
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Any, Callable, Dict, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.utils import is_pin_memory_available
|
||||||
|
|
||||||
|
|
||||||
|
def find_loaded_library(lib_name) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
According to according to https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html,
|
||||||
|
the file `/proc/self/maps` contains the memory maps of the process, which includes the
|
||||||
|
shared libraries loaded by the process. We can use this file to find the path of the
|
||||||
|
a loaded library.
|
||||||
|
""" # noqa
|
||||||
|
found_line = None
|
||||||
|
with open("/proc/self/maps") as f:
|
||||||
|
for line in f:
|
||||||
|
if lib_name in line:
|
||||||
|
found_line = line
|
||||||
|
break
|
||||||
|
if found_line is None:
|
||||||
|
# the library is not loaded in the current process
|
||||||
|
return None
|
||||||
|
# if lib_name is libcudart, we need to match a line with:
|
||||||
|
# address /path/to/libcudart-hash.so.11.0
|
||||||
|
start = found_line.index("/")
|
||||||
|
path = found_line[start:].strip()
|
||||||
|
filename = path.split("/")[-1]
|
||||||
|
assert filename.rpartition(".so")[0].startswith(lib_name), \
|
||||||
|
f"Unexpected filename: {filename} for library {lib_name}"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
cumem_available = False
|
||||||
|
try:
|
||||||
|
from vllm.cumem_allocator import (init_module, python_create_and_map,
|
||||||
|
python_unmap_and_release)
|
||||||
|
from vllm.distributed.device_communicators.cuda_wrapper import (
|
||||||
|
CudaRTLibrary)
|
||||||
|
lib_name = find_loaded_library("cumem_allocator")
|
||||||
|
libcudart = CudaRTLibrary()
|
||||||
|
cumem_available = True
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
# rocm platform does not support cumem allocator
|
||||||
|
init_module = None
|
||||||
|
python_create_and_map = None
|
||||||
|
python_unmap_and_release = None
|
||||||
|
CudaRTLibrary = None
|
||||||
|
lib_name = None
|
||||||
|
libcudart = None
|
||||||
|
|
||||||
|
# py_device, py_alignedSize, py_d_mem, py_p_memHandle
|
||||||
|
HandleType = Tuple[int, int, int, int]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AllocationData:
|
||||||
|
handle: HandleType
|
||||||
|
tag: str
|
||||||
|
cpu_backup_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_map(allocation_handle: HandleType) -> None:
|
||||||
|
python_create_and_map(*allocation_handle)
|
||||||
|
|
||||||
|
|
||||||
|
def unmap_and_release(allocation_handle: HandleType) -> None:
|
||||||
|
python_unmap_and_release(*allocation_handle)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pluggable_allocator(
|
||||||
|
python_malloc_fn: Callable[[int],
|
||||||
|
int], python_free_func: Callable[[int, int],
|
||||||
|
None]
|
||||||
|
) -> torch.cuda.memory.CUDAPluggableAllocator:
|
||||||
|
init_module(python_malloc_fn, python_free_func)
|
||||||
|
new_alloc = torch.cuda.memory.CUDAPluggableAllocator(
|
||||||
|
lib_name, 'my_malloc', 'my_free')
|
||||||
|
return new_alloc
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def use_memory_pool_with_allocator(
|
||||||
|
python_malloc_fn: Callable[[int], int],
|
||||||
|
python_free_func: Callable[[int, int], None]) -> None:
|
||||||
|
new_alloc = get_pluggable_allocator(python_malloc_fn, python_free_func)
|
||||||
|
mem_pool = torch.cuda.memory.MemPool(new_alloc._allocator)
|
||||||
|
with torch.cuda.memory.use_mem_pool(mem_pool):
|
||||||
|
yield mem_pool, new_alloc
|
||||||
|
|
||||||
|
|
||||||
|
class CuMemAllocator:
|
||||||
|
"""
|
||||||
|
A singleton class that manages a memory pool for CUDA tensors.
|
||||||
|
The memory in this pool can be offloaded or discarded when the
|
||||||
|
allocator sleeps.
|
||||||
|
|
||||||
|
Inside the `use_memory_pool(tag)` context, all tensors created will
|
||||||
|
be allocated in the memory pool, and has the same tag as the
|
||||||
|
tag passed to the context.
|
||||||
|
|
||||||
|
When we call `sleep`, all tensors with the specified tag will be
|
||||||
|
offloaded to CPU memory, and the rest of the tensors will be discarded.
|
||||||
|
When we call `wake_up`, all tensors that are previously offloaded
|
||||||
|
will be loaded back to GPU memory, and the rest of the tensors will
|
||||||
|
have empty memory.
|
||||||
|
|
||||||
|
Why it needs to be a singleton?
|
||||||
|
When allocated tensors are garbage collected, PyTorch will call
|
||||||
|
the free callback, which will call the `python_free_callback` method.
|
||||||
|
The C-extension uses a global variable to store the function of an
|
||||||
|
instance of this class. If we create multiple instances of this class,
|
||||||
|
the global variable will be overwritten and the free callback will
|
||||||
|
not work as expected.
|
||||||
|
"""
|
||||||
|
instance: "CuMemAllocator" = None
|
||||||
|
default_tag: str = "default"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_instance() -> "CuMemAllocator":
|
||||||
|
"""
|
||||||
|
CuMemAllocator is a singleton class.
|
||||||
|
We cannot call the constructor directly.
|
||||||
|
Call this method to get the instance.
|
||||||
|
"""
|
||||||
|
assert cumem_available, "cumem allocator is not available"
|
||||||
|
if CuMemAllocator.instance is None:
|
||||||
|
CuMemAllocator.instance = CuMemAllocator()
|
||||||
|
return CuMemAllocator.instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
conf = os.environ.get("PYTORCH_CUDA_ALLOC_CONF", "")
|
||||||
|
assert "expandable_segments:True" not in conf, \
|
||||||
|
("Expandable segments are not compatible with memory pool. "
|
||||||
|
"Please track https://github.com/pytorch/pytorch/issues/147851 "
|
||||||
|
"for the latest updates.")
|
||||||
|
|
||||||
|
self.pointer_to_data: Dict[int, AllocationData] = {}
|
||||||
|
self.current_tag: str = CuMemAllocator.default_tag
|
||||||
|
self.allocator_and_pools: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def python_malloc_callback(self, allocation_handle: HandleType) -> None:
|
||||||
|
"""
|
||||||
|
Internal method to store the allocation data
|
||||||
|
when memory is allocated in the memory pool."""
|
||||||
|
py_d_mem = allocation_handle[2]
|
||||||
|
self.pointer_to_data[py_d_mem] = AllocationData(
|
||||||
|
allocation_handle, self.current_tag)
|
||||||
|
return
|
||||||
|
|
||||||
|
def python_free_callback(self, ptr: int) -> HandleType:
|
||||||
|
"""
|
||||||
|
Internal method to look up the allocation data
|
||||||
|
when memory is freed in the memory pool."""
|
||||||
|
data = self.pointer_to_data.pop(ptr)
|
||||||
|
if data.cpu_backup_tensor is not None:
|
||||||
|
data.cpu_backup_tensor = None
|
||||||
|
return data.handle
|
||||||
|
|
||||||
|
def sleep(
|
||||||
|
self,
|
||||||
|
offload_tags: Optional[Union[Tuple[str, ...],
|
||||||
|
str]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Put the allocator in sleep mode.
|
||||||
|
All data in the memory allocation with the specified tag will be
|
||||||
|
offloaded to CPU memory, and others will be discarded.
|
||||||
|
|
||||||
|
:param offload_tags: The tags of the memory allocation that will be
|
||||||
|
offloaded. The rest of the memory allocation will be discarded.
|
||||||
|
"""
|
||||||
|
if offload_tags is None:
|
||||||
|
# by default, allocated tensors are offloaded
|
||||||
|
# when the allocator sleeps
|
||||||
|
offload_tags = (CuMemAllocator.default_tag, )
|
||||||
|
elif isinstance(offload_tags, str):
|
||||||
|
offload_tags = (offload_tags, )
|
||||||
|
|
||||||
|
assert isinstance(offload_tags, tuple)
|
||||||
|
|
||||||
|
for ptr, data in self.pointer_to_data.items():
|
||||||
|
handle = data.handle
|
||||||
|
if data.tag in offload_tags:
|
||||||
|
size_in_bytes = handle[1]
|
||||||
|
cpu_backup_tensor = torch.empty(
|
||||||
|
size_in_bytes,
|
||||||
|
dtype=torch.uint8,
|
||||||
|
device='cpu',
|
||||||
|
pin_memory=is_pin_memory_available())
|
||||||
|
cpu_ptr = cpu_backup_tensor.data_ptr()
|
||||||
|
libcudart.cudaMemcpy(cpu_ptr, ptr, size_in_bytes)
|
||||||
|
data.cpu_backup_tensor = cpu_backup_tensor
|
||||||
|
unmap_and_release(handle)
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
torch.cuda.empty_cache()
|
||||||
|
|
||||||
|
def wake_up(self, tags: Optional[list[str]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Wake up the allocator from sleep mode.
|
||||||
|
All data that is previously offloaded will be loaded back to GPU
|
||||||
|
memory, and the rest of the data will have empty memory.
|
||||||
|
|
||||||
|
:param tags: The tags of the memory allocation that will be loaded
|
||||||
|
back to GPU memory. If None, all memory allocation will be loaded
|
||||||
|
back to GPU memory.
|
||||||
|
"""
|
||||||
|
for ptr, data in self.pointer_to_data.items():
|
||||||
|
if tags is None or data.tag in tags:
|
||||||
|
handle = data.handle
|
||||||
|
create_and_map(handle)
|
||||||
|
if data.cpu_backup_tensor is not None:
|
||||||
|
cpu_backup_tensor = data.cpu_backup_tensor
|
||||||
|
if cpu_backup_tensor is not None:
|
||||||
|
size_in_bytes = cpu_backup_tensor.numel(
|
||||||
|
) * cpu_backup_tensor.element_size()
|
||||||
|
cpu_ptr = cpu_backup_tensor.data_ptr()
|
||||||
|
libcudart.cudaMemcpy(ptr, cpu_ptr, size_in_bytes)
|
||||||
|
data.cpu_backup_tensor = None
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def use_memory_pool(self, tag: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
A context manager to use the memory pool.
|
||||||
|
All memory allocation created inside the context will be allocated
|
||||||
|
in the memory pool, and has the specified tag.
|
||||||
|
|
||||||
|
:param tag: The tag of the memory allocation. If None, the default tag
|
||||||
|
will be used.
|
||||||
|
"""
|
||||||
|
if tag is None:
|
||||||
|
tag = CuMemAllocator.default_tag
|
||||||
|
|
||||||
|
assert isinstance(tag, str)
|
||||||
|
|
||||||
|
old_tag = self.current_tag
|
||||||
|
self.current_tag = tag
|
||||||
|
with use_memory_pool_with_allocator(self.python_malloc_callback,
|
||||||
|
self.python_free_callback) as data:
|
||||||
|
# start to hit another PyTorch bug in PyTorch 2.6,
|
||||||
|
# possibly because of gc-related issue w.r.t. the allocator and
|
||||||
|
# the memory pool.
|
||||||
|
# to avoid the issue, we keep a reference of the data.
|
||||||
|
# see https://github.com/pytorch/pytorch/issues/146431 .
|
||||||
|
self.allocator_and_pools[tag] = data
|
||||||
|
yield
|
||||||
|
# PyTorch's bug, calling torch.cuda.empty_cache() will error
|
||||||
|
# when using pluggable allocator, see
|
||||||
|
# https://github.com/pytorch/pytorch/issues/145168 .
|
||||||
|
# if we have some memory allocated and then freed,
|
||||||
|
# the memory will not be released.
|
||||||
|
# right now it is fine, because we only use this allocator
|
||||||
|
# during weight loading and kv cache creation, where we only
|
||||||
|
# allocate memory.
|
||||||
|
# TODO: we need to find a way to release the memory,
|
||||||
|
# i.e. calling torch.cuda.empty_cache()
|
||||||
|
self.current_tag = old_tag
|
||||||
|
|
||||||
|
def get_current_usage(self) -> int:
|
||||||
|
"""
|
||||||
|
Get the total number of bytes allocated in the memory pool.
|
||||||
|
"""
|
||||||
|
sum_bytes: int = 0
|
||||||
|
for ptr, data in self.pointer_to_data.items():
|
||||||
|
handle = data.handle
|
||||||
|
sum_bytes += handle[1]
|
||||||
|
return sum_bytes
|
||||||
5
vllm/distributed/__init__.py
Normal file
5
vllm/distributed/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from .communication_op import *
|
||||||
|
from .parallel_state import *
|
||||||
|
from .utils import *
|
||||||
34
vllm/distributed/communication_op.py
Normal file
34
vllm/distributed/communication_op.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.distributed
|
||||||
|
|
||||||
|
from .parallel_state import get_tp_group
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_model_parallel_all_reduce(input_: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""All-reduce the input tensor across model parallel group."""
|
||||||
|
return get_tp_group().all_reduce(input_)
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_model_parallel_all_gather(input_: torch.Tensor,
|
||||||
|
dim: int = -1) -> torch.Tensor:
|
||||||
|
"""All-gather the input tensor across model parallel group."""
|
||||||
|
return get_tp_group().all_gather(input_, dim)
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_model_parallel_gather(input_: torch.Tensor,
|
||||||
|
dst: int = 0,
|
||||||
|
dim: int = -1) -> Optional[torch.Tensor]:
|
||||||
|
"""Gather the input tensor across model parallel group."""
|
||||||
|
return get_tp_group().gather(input_, dst, dim)
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_tensor_dict(tensor_dict: Optional[Dict[Any, Union[torch.Tensor,
|
||||||
|
Any]]] = None,
|
||||||
|
src: int = 0):
|
||||||
|
if not torch.distributed.is_initialized():
|
||||||
|
return tensor_dict
|
||||||
|
return get_tp_group().broadcast_tensor_dict(tensor_dict, src)
|
||||||
0
vllm/distributed/device_communicators/__init__.py
Normal file
0
vllm/distributed/device_communicators/__init__.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.distributed as dist
|
||||||
|
from torch.distributed import ProcessGroup
|
||||||
|
import ixformer.distributed as ixfd
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceCommunicatorBase:
|
||||||
|
"""
|
||||||
|
Base class for device-specific communicator.
|
||||||
|
It can use the `cpu_group` to initialize the communicator.
|
||||||
|
If the device has PyTorch integration (PyTorch can recognize its
|
||||||
|
communication backend), the `device_group` will also be given.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
cpu_group: ProcessGroup,
|
||||||
|
device: Optional[torch.device] = None,
|
||||||
|
device_group: Optional[ProcessGroup] = None,
|
||||||
|
unique_name: str = ""):
|
||||||
|
self.device = device or torch.device("cpu")
|
||||||
|
self.cpu_group = cpu_group
|
||||||
|
self.device_group = device_group
|
||||||
|
self.unique_name = unique_name
|
||||||
|
self.rank = dist.get_rank(cpu_group)
|
||||||
|
self.world_size = dist.get_world_size(cpu_group)
|
||||||
|
self.ranks = dist.get_process_group_ranks(cpu_group)
|
||||||
|
self.global_rank = dist.get_rank()
|
||||||
|
self.global_world_size = dist.get_world_size()
|
||||||
|
self.rank_in_group = dist.get_group_rank(self.cpu_group,
|
||||||
|
self.global_rank)
|
||||||
|
self.use_vllm_comm = os.environ.get("VLLM_FORCE_NCCL_COMM",None) not in ["1", "Y", "y"]
|
||||||
|
if "pp" in unique_name:
|
||||||
|
# pipeline parallel does not need custom allreduce
|
||||||
|
use_custom_allreduce = False
|
||||||
|
else:
|
||||||
|
from vllm.distributed.parallel_state import (
|
||||||
|
_ENABLE_CUSTOM_ALL_REDUCE)
|
||||||
|
use_custom_allreduce = _ENABLE_CUSTOM_ALL_REDUCE
|
||||||
|
self.use_custom_allreduce = use_custom_allreduce
|
||||||
|
# lazy import to avoid documentation build error
|
||||||
|
from vllm.distributed.device_communicators.custom_all_reduce import (
|
||||||
|
CustomAllreduce)
|
||||||
|
self.ca_comm: Optional[CustomAllreduce] = None
|
||||||
|
if use_custom_allreduce and self.world_size > 1:
|
||||||
|
# Initialize a custom fast all-reduce implementation.
|
||||||
|
self.ca_comm = CustomAllreduce(
|
||||||
|
group=self.cpu_group,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
def all_reduce(self, input_: torch.Tensor) -> torch.Tensor:
|
||||||
|
|
||||||
|
if self.world_size == 1:
|
||||||
|
return input_
|
||||||
|
|
||||||
|
if self.use_vllm_comm:
|
||||||
|
ixfd.all_reduce(input_, group=self.device_group, async_op=True)
|
||||||
|
else:
|
||||||
|
torch.distributed.all_reduce(input_, group=self.device_group)
|
||||||
|
return input_
|
||||||
|
|
||||||
|
def all_gather(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
|
||||||
|
if dim < 0:
|
||||||
|
# Convert negative dim to positive.
|
||||||
|
dim += input_.dim()
|
||||||
|
input_size = input_.size()
|
||||||
|
# NOTE: we have to use concat-style all-gather here,
|
||||||
|
# stack-style all-gather has compatibility issues with
|
||||||
|
# torch.compile . see https://github.com/pytorch/pytorch/issues/138795
|
||||||
|
output_size = (input_size[0] * self.world_size, ) + input_size[1:]
|
||||||
|
# Allocate output tensor.
|
||||||
|
output_tensor = torch.empty(output_size,
|
||||||
|
dtype=input_.dtype,
|
||||||
|
device=input_.device)
|
||||||
|
# All-gather.
|
||||||
|
if self.use_vllm_comm:
|
||||||
|
ixfd.all_gather_into_tensor(output_tensor,
|
||||||
|
input_,
|
||||||
|
group=self.device_group,
|
||||||
|
async_op=True)
|
||||||
|
else:
|
||||||
|
torch.distributed.all_gather_into_tensor(output_tensor,
|
||||||
|
input_,
|
||||||
|
group=self.device_group)
|
||||||
|
# Reshape
|
||||||
|
output_tensor = output_tensor.reshape((self.world_size, ) + input_size)
|
||||||
|
output_tensor = output_tensor.movedim(0, dim)
|
||||||
|
output_tensor = output_tensor.reshape(input_size[:dim] +
|
||||||
|
(self.world_size *
|
||||||
|
input_size[dim], ) +
|
||||||
|
input_size[dim + 1:])
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
def gather(self,
|
||||||
|
input_: torch.Tensor,
|
||||||
|
dst: int = 0,
|
||||||
|
dim: int = -1) -> Optional[torch.Tensor]:
|
||||||
|
"""
|
||||||
|
NOTE: We assume that the input tensor is on the same device across
|
||||||
|
all the ranks.
|
||||||
|
NOTE: `dst` is the local rank of the destination rank.
|
||||||
|
"""
|
||||||
|
world_size = self.world_size
|
||||||
|
assert -input_.dim() <= dim < input_.dim(), (
|
||||||
|
f"Invalid dim ({dim}) for input tensor with shape {input_.size()}")
|
||||||
|
if dim < 0:
|
||||||
|
# Convert negative dim to positive.
|
||||||
|
dim += input_.dim()
|
||||||
|
|
||||||
|
# Allocate output tensor.
|
||||||
|
if self.rank_in_group == dst:
|
||||||
|
gather_list = [torch.empty_like(input_) for _ in range(world_size)]
|
||||||
|
else:
|
||||||
|
gather_list = None
|
||||||
|
# Gather.
|
||||||
|
if self.use_vllm_comm:
|
||||||
|
ixfd.gather(input_,
|
||||||
|
gather_list,
|
||||||
|
dst=self.ranks[dst],
|
||||||
|
group=self.device_group,
|
||||||
|
async_op=True)
|
||||||
|
else:
|
||||||
|
torch.distributed.gather(input_,
|
||||||
|
gather_list,
|
||||||
|
dst=self.ranks[dst],
|
||||||
|
group=self.device_group)
|
||||||
|
if self.rank_in_group == dst:
|
||||||
|
output_tensor = torch.cat(gather_list, dim=dim)
|
||||||
|
else:
|
||||||
|
output_tensor = None
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
def send(self, tensor: torch.Tensor, dst: Optional[int] = None) -> None:
|
||||||
|
"""Sends a tensor to the destination rank in a non-blocking way"""
|
||||||
|
"""NOTE: `dst` is the local rank of the destination rank."""
|
||||||
|
if dst is None:
|
||||||
|
dst = (self.rank_in_group + 1) % self.world_size
|
||||||
|
if self.use_vllm_comm:
|
||||||
|
ixfd.send(tensor, self.ranks[dst], self.device_group)
|
||||||
|
else:
|
||||||
|
torch.distributed.send(tensor, self.ranks[dst], self.device_group)
|
||||||
|
|
||||||
|
def recv(self,
|
||||||
|
size: torch.Size,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
src: Optional[int] = None) -> torch.Tensor:
|
||||||
|
"""Receives a tensor from the source rank."""
|
||||||
|
"""NOTE: `src` is the local rank of the source rank."""
|
||||||
|
if src is None:
|
||||||
|
src = (self.rank_in_group - 1) % self.world_size
|
||||||
|
|
||||||
|
tensor = torch.empty(size, dtype=dtype, device=self.device)
|
||||||
|
if self.use_vllm_comm:
|
||||||
|
ixfd.recv(tensor, self.ranks[src], self.device_group)
|
||||||
|
else:
|
||||||
|
torch.distributed.recv(tensor, self.ranks[src], self.device_group)
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
||||||
139
vllm/distributed/device_communicators/cpu_communicator.py
Normal file
139
vllm/distributed/device_communicators/cpu_communicator.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch.distributed import ProcessGroup
|
||||||
|
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
from vllm.platforms.interface import CpuArchEnum
|
||||||
|
|
||||||
|
from .base_device_communicator import DeviceCommunicatorBase
|
||||||
|
|
||||||
|
|
||||||
|
class CpuCommunicator(DeviceCommunicatorBase):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
cpu_group: ProcessGroup,
|
||||||
|
device: Optional[torch.device] = None,
|
||||||
|
device_group: Optional[ProcessGroup] = None,
|
||||||
|
unique_name: str = ""):
|
||||||
|
super().__init__(cpu_group, device, device_group, unique_name)
|
||||||
|
self.dist_module = torch.distributed
|
||||||
|
|
||||||
|
if current_platform.get_cpu_architecture() == CpuArchEnum.X86:
|
||||||
|
self.dist_module = _CPUSHMDistributed(self)
|
||||||
|
|
||||||
|
def all_reduce(self, input_):
|
||||||
|
self.dist_module.all_reduce(input_, group=self.device_group)
|
||||||
|
return input_
|
||||||
|
|
||||||
|
def gather(self,
|
||||||
|
input_: torch.Tensor,
|
||||||
|
dst: int = 0,
|
||||||
|
dim: int = -1) -> Optional[torch.Tensor]:
|
||||||
|
"""
|
||||||
|
NOTE: We assume that the input tensor is on the same device across
|
||||||
|
all the ranks.
|
||||||
|
NOTE: `dst` is the local rank of the destination rank.
|
||||||
|
"""
|
||||||
|
world_size = self.world_size
|
||||||
|
assert -input_.dim() <= dim < input_.dim(), (
|
||||||
|
f"Invalid dim ({dim}) for input tensor with shape {input_.size()}")
|
||||||
|
if dim < 0:
|
||||||
|
# Convert negative dim to positive.
|
||||||
|
dim += input_.dim()
|
||||||
|
|
||||||
|
# Allocate output tensor.
|
||||||
|
if self.rank_in_group == dst:
|
||||||
|
gather_list = [torch.empty_like(input_) for _ in range(world_size)]
|
||||||
|
else:
|
||||||
|
gather_list = None
|
||||||
|
|
||||||
|
# Gather.
|
||||||
|
self.dist_module.gather(input_,
|
||||||
|
gather_list,
|
||||||
|
dst=self.ranks[dst],
|
||||||
|
group=self.device_group)
|
||||||
|
|
||||||
|
if self.rank_in_group == dst:
|
||||||
|
output_tensor = torch.cat(gather_list, dim=dim)
|
||||||
|
else:
|
||||||
|
output_tensor = None
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
def all_gather(self, input_: torch.Tensor, dim: int = -1) -> torch.Tensor:
|
||||||
|
if dim < 0:
|
||||||
|
# Convert negative dim to positive.
|
||||||
|
dim += input_.dim()
|
||||||
|
input_size = input_.size()
|
||||||
|
# NOTE: we have to use concat-style all-gather here,
|
||||||
|
# stack-style all-gather has compatibility issues with
|
||||||
|
# torch.compile . see https://github.com/pytorch/pytorch/issues/138795
|
||||||
|
output_size = (input_size[0] * self.world_size, ) + input_size[1:]
|
||||||
|
# Allocate output tensor.
|
||||||
|
output_tensor = torch.empty(output_size,
|
||||||
|
dtype=input_.dtype,
|
||||||
|
device=input_.device)
|
||||||
|
# All-gather.
|
||||||
|
self.dist_module.all_gather_into_tensor(output_tensor,
|
||||||
|
input_,
|
||||||
|
group=self.device_group)
|
||||||
|
|
||||||
|
# Reshape
|
||||||
|
output_tensor = output_tensor.reshape((self.world_size, ) + input_size)
|
||||||
|
output_tensor = output_tensor.movedim(0, dim)
|
||||||
|
output_tensor = output_tensor.reshape(input_size[:dim] +
|
||||||
|
(self.world_size *
|
||||||
|
input_size[dim], ) +
|
||||||
|
input_size[dim + 1:])
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
|
||||||
|
class _CPUSHMDistributed:
|
||||||
|
|
||||||
|
def __init__(self, communicator: CpuCommunicator):
|
||||||
|
instance_identifier = os.environ["VLLM_DIST_IDENT"]
|
||||||
|
self.communicator = communicator
|
||||||
|
|
||||||
|
group_ranks = [str(rank) for rank in self.communicator.ranks]
|
||||||
|
shm_group_identifier = f"[{'-'.join(group_ranks)}]"
|
||||||
|
self.group_name = f"{instance_identifier}-{shm_group_identifier}-cpushm"
|
||||||
|
|
||||||
|
self.handle = self._init_cpu_shm()
|
||||||
|
|
||||||
|
def _init_cpu_shm(self) -> int:
|
||||||
|
handle = torch.ops._C.init_shm_manager(
|
||||||
|
self.group_name,
|
||||||
|
self.communicator.world_size,
|
||||||
|
self.communicator.rank,
|
||||||
|
)
|
||||||
|
torch.distributed.barrier(self.communicator.device_group)
|
||||||
|
torch.ops._C.join_shm_manager(
|
||||||
|
handle,
|
||||||
|
self.group_name,
|
||||||
|
)
|
||||||
|
torch.distributed.barrier(self.communicator.device_group)
|
||||||
|
|
||||||
|
return handle
|
||||||
|
|
||||||
|
def all_reduce(self,
|
||||||
|
input: torch.Tensor,
|
||||||
|
group: Optional[ProcessGroup] = None) -> None:
|
||||||
|
torch.ops._C.shm_allreduce(self.handle, input)
|
||||||
|
|
||||||
|
def gather(self,
|
||||||
|
input: torch.Tensor,
|
||||||
|
gather_list: Optional[List[torch.Tensor]],
|
||||||
|
dst: int = -1,
|
||||||
|
group: Optional[ProcessGroup] = None) -> None:
|
||||||
|
# Note: different from the torch gather, here we use local dst rank.
|
||||||
|
torch.ops._C.shm_gather(self.handle, input, gather_list,
|
||||||
|
torch.distributed.get_group_rank(group, dst))
|
||||||
|
|
||||||
|
def all_gather_into_tensor(self,
|
||||||
|
output: torch.Tensor,
|
||||||
|
input: torch.Tensor,
|
||||||
|
group: Optional[ProcessGroup] = None) -> None:
|
||||||
|
torch.ops._C.shm_all_gather(self.handle, input, output)
|
||||||
106
vllm/distributed/device_communicators/cuda_communicator.py
Normal file
106
vllm/distributed/device_communicators/cuda_communicator.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from torch.distributed import ProcessGroup
|
||||||
|
|
||||||
|
from .base_device_communicator import DeviceCommunicatorBase
|
||||||
|
|
||||||
|
|
||||||
|
class CudaCommunicator(DeviceCommunicatorBase):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
cpu_group: ProcessGroup,
|
||||||
|
device: Optional[torch.device] = None,
|
||||||
|
device_group: Optional[ProcessGroup] = None,
|
||||||
|
unique_name: str = ""):
|
||||||
|
super().__init__(cpu_group, device, device_group, unique_name)
|
||||||
|
if "tp" not in unique_name:
|
||||||
|
# only tp uses custom allreduce
|
||||||
|
use_custom_allreduce = False
|
||||||
|
else:
|
||||||
|
from vllm.distributed.parallel_state import (
|
||||||
|
_ENABLE_CUSTOM_ALL_REDUCE)
|
||||||
|
use_custom_allreduce = _ENABLE_CUSTOM_ALL_REDUCE
|
||||||
|
use_pynccl = True
|
||||||
|
|
||||||
|
self.use_pynccl = use_pynccl
|
||||||
|
self.use_custom_allreduce = use_custom_allreduce
|
||||||
|
|
||||||
|
# lazy import to avoid documentation build error
|
||||||
|
from vllm.distributed.device_communicators.custom_all_reduce import (
|
||||||
|
CustomAllreduce)
|
||||||
|
from vllm.distributed.device_communicators.pynccl import (
|
||||||
|
PyNcclCommunicator)
|
||||||
|
|
||||||
|
self.pynccl_comm: Optional[PyNcclCommunicator] = None
|
||||||
|
if use_pynccl and self.world_size > 1:
|
||||||
|
self.pynccl_comm = PyNcclCommunicator(
|
||||||
|
group=self.cpu_group,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ca_comm: Optional[CustomAllreduce] = None
|
||||||
|
if use_custom_allreduce and self.world_size > 1:
|
||||||
|
# Initialize a custom fast all-reduce implementation.
|
||||||
|
self.ca_comm = CustomAllreduce(
|
||||||
|
group=self.cpu_group,
|
||||||
|
device=self.device,
|
||||||
|
)
|
||||||
|
|
||||||
|
def all_reduce(self, input_):
|
||||||
|
# always try custom allreduce first,
|
||||||
|
# and then pynccl.
|
||||||
|
ca_comm = self.ca_comm
|
||||||
|
if ca_comm is not None and not ca_comm.disabled and \
|
||||||
|
ca_comm.should_custom_ar(input_):
|
||||||
|
out = ca_comm.custom_all_reduce(input_)
|
||||||
|
assert out is not None
|
||||||
|
return out
|
||||||
|
pynccl_comm = self.pynccl_comm
|
||||||
|
assert pynccl_comm is not None
|
||||||
|
out = pynccl_comm.all_reduce(input_)
|
||||||
|
if out is None:
|
||||||
|
# fall back to the default all-reduce using PyTorch.
|
||||||
|
# this usually happens during testing.
|
||||||
|
# when we run the model, allreduce only happens for the TP
|
||||||
|
# group, where we always have either custom allreduce or pynccl.
|
||||||
|
out = input_.clone()
|
||||||
|
torch.distributed.all_reduce(out, group=self.device_group)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def send(self, tensor: torch.Tensor, dst: Optional[int] = None) -> None:
|
||||||
|
"""Sends a tensor to the destination rank in a non-blocking way"""
|
||||||
|
"""NOTE: `dst` is the local rank of the destination rank."""
|
||||||
|
if dst is None:
|
||||||
|
dst = (self.rank_in_group + 1) % self.world_size
|
||||||
|
|
||||||
|
pynccl_comm = self.pynccl_comm
|
||||||
|
if pynccl_comm is not None and not pynccl_comm.disabled:
|
||||||
|
pynccl_comm.send(tensor, dst)
|
||||||
|
else:
|
||||||
|
torch.distributed.send(tensor, self.ranks[dst], self.device_group)
|
||||||
|
|
||||||
|
def recv(self,
|
||||||
|
size: torch.Size,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
src: Optional[int] = None) -> torch.Tensor:
|
||||||
|
"""Receives a tensor from the source rank."""
|
||||||
|
"""NOTE: `src` is the local rank of the source rank."""
|
||||||
|
if src is None:
|
||||||
|
src = (self.rank_in_group - 1) % self.world_size
|
||||||
|
|
||||||
|
tensor = torch.empty(size, dtype=dtype, device=self.device)
|
||||||
|
pynccl_comm = self.pynccl_comm
|
||||||
|
if pynccl_comm is not None and not pynccl_comm.disabled:
|
||||||
|
pynccl_comm.recv(tensor, src)
|
||||||
|
else:
|
||||||
|
torch.distributed.recv(tensor, self.ranks[src], self.device_group)
|
||||||
|
return tensor
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
if self.pynccl_comm is not None:
|
||||||
|
self.pynccl_comm = None
|
||||||
|
if self.ca_comm is not None:
|
||||||
|
self.ca_comm = None
|
||||||
179
vllm/distributed/device_communicators/cuda_wrapper.py
Normal file
179
vllm/distributed/device_communicators/cuda_wrapper.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""This file is a pure Python wrapper for the cudart library.
|
||||||
|
It avoids the need to compile a separate shared library, and is
|
||||||
|
convenient for use when we just need to call a few functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
# this line makes it possible to directly load `libcudart.so` using `ctypes`
|
||||||
|
import torch # noqa
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
# === export types and functions from cudart to Python ===
|
||||||
|
# for the original cudart definition, please check
|
||||||
|
# https://docs.nvidia.com/cuda/cuda-runtime-api/index.html
|
||||||
|
|
||||||
|
cudaError_t = ctypes.c_int
|
||||||
|
cudaMemcpyKind = ctypes.c_int
|
||||||
|
|
||||||
|
|
||||||
|
class cudaIpcMemHandle_t(ctypes.Structure):
|
||||||
|
_fields_ = [("internal", ctypes.c_byte * 128)]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Function:
|
||||||
|
name: str
|
||||||
|
restype: Any
|
||||||
|
argtypes: List[Any]
|
||||||
|
|
||||||
|
|
||||||
|
def find_loaded_library(lib_name) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
According to according to https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html,
|
||||||
|
the file `/proc/self/maps` contains the memory maps of the process, which includes the
|
||||||
|
shared libraries loaded by the process. We can use this file to find the path of the
|
||||||
|
a loaded library.
|
||||||
|
""" # noqa
|
||||||
|
found = False
|
||||||
|
with open("/proc/self/maps") as f:
|
||||||
|
for line in f:
|
||||||
|
if lib_name in line:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
# the library is not loaded in the current process
|
||||||
|
return None
|
||||||
|
# if lib_name is libcudart, we need to match a line with:
|
||||||
|
# address /path/to/libcudart-hash.so.11.0
|
||||||
|
start = line.index("/")
|
||||||
|
path = line[start:].strip()
|
||||||
|
filename = path.split("/")[-1]
|
||||||
|
assert filename.rpartition(".so")[0].startswith(lib_name), \
|
||||||
|
f"Unexpected filename: {filename} for library {lib_name}"
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class CudaRTLibrary:
|
||||||
|
exported_functions = [
|
||||||
|
# cudaError_t cudaSetDevice ( int device )
|
||||||
|
Function("cudaSetDevice", cudaError_t, [ctypes.c_int]),
|
||||||
|
# cudaError_t cudaDeviceSynchronize ( void )
|
||||||
|
Function("cudaDeviceSynchronize", cudaError_t, []),
|
||||||
|
# cudaError_t cudaDeviceReset ( void )
|
||||||
|
Function("cudaDeviceReset", cudaError_t, []),
|
||||||
|
|
||||||
|
# const char* cudaGetErrorString ( cudaError_t error )
|
||||||
|
Function("cudaGetErrorString", ctypes.c_char_p, [cudaError_t]),
|
||||||
|
|
||||||
|
# cudaError_t cudaMalloc ( void** devPtr, size_t size )
|
||||||
|
Function("cudaMalloc", cudaError_t,
|
||||||
|
[ctypes.POINTER(ctypes.c_void_p), ctypes.c_size_t]),
|
||||||
|
# cudaError_t cudaFree ( void* devPtr )
|
||||||
|
Function("cudaFree", cudaError_t, [ctypes.c_void_p]),
|
||||||
|
# cudaError_t cudaMemset ( void* devPtr, int value, size_t count )
|
||||||
|
Function("cudaMemset", cudaError_t,
|
||||||
|
[ctypes.c_void_p, ctypes.c_int, ctypes.c_size_t]),
|
||||||
|
# cudaError_t cudaMemcpy ( void* dst, const void* src, size_t count, cudaMemcpyKind kind ) # noqa
|
||||||
|
Function("cudaMemcpy", cudaError_t, [
|
||||||
|
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, cudaMemcpyKind
|
||||||
|
]),
|
||||||
|
|
||||||
|
# cudaError_t cudaIpcGetMemHandle ( cudaIpcMemHandle_t* handle, void* devPtr ) # noqa
|
||||||
|
Function("cudaIpcGetMemHandle", cudaError_t,
|
||||||
|
[ctypes.POINTER(cudaIpcMemHandle_t), ctypes.c_void_p]),
|
||||||
|
# cudaError_t cudaIpcOpenMemHandle ( void** devPtr, cudaIpcMemHandle_t handle, unsigned int flags ) # noqa
|
||||||
|
Function("cudaIpcOpenMemHandle", cudaError_t, [
|
||||||
|
ctypes.POINTER(ctypes.c_void_p), cudaIpcMemHandle_t, ctypes.c_uint
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# class attribute to store the mapping from the path to the library
|
||||||
|
# to avoid loading the same library multiple times
|
||||||
|
path_to_library_cache: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# class attribute to store the mapping from library path
|
||||||
|
# to the corresponding dictionary
|
||||||
|
path_to_dict_mapping: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
def __init__(self, so_file: Optional[str] = None):
|
||||||
|
if so_file is None:
|
||||||
|
so_file = find_loaded_library("libcudart")
|
||||||
|
if so_file is None:
|
||||||
|
so_file = envs.VLLM_CUDART_SO_PATH # fallback to env var
|
||||||
|
assert so_file is not None, \
|
||||||
|
(
|
||||||
|
"libcudart is not loaded in the current process, "
|
||||||
|
"try setting VLLM_CUDART_SO_PATH"
|
||||||
|
)
|
||||||
|
if so_file not in CudaRTLibrary.path_to_library_cache:
|
||||||
|
lib = ctypes.CDLL(so_file)
|
||||||
|
CudaRTLibrary.path_to_library_cache[so_file] = lib
|
||||||
|
self.lib = CudaRTLibrary.path_to_library_cache[so_file]
|
||||||
|
|
||||||
|
if so_file not in CudaRTLibrary.path_to_dict_mapping:
|
||||||
|
_funcs = {}
|
||||||
|
for func in CudaRTLibrary.exported_functions:
|
||||||
|
f = getattr(self.lib, func.name)
|
||||||
|
f.restype = func.restype
|
||||||
|
f.argtypes = func.argtypes
|
||||||
|
_funcs[func.name] = f
|
||||||
|
CudaRTLibrary.path_to_dict_mapping[so_file] = _funcs
|
||||||
|
self.funcs = CudaRTLibrary.path_to_dict_mapping[so_file]
|
||||||
|
|
||||||
|
def CUDART_CHECK(self, result: cudaError_t) -> None:
|
||||||
|
if result != 0:
|
||||||
|
error_str = self.cudaGetErrorString(result)
|
||||||
|
raise RuntimeError(f"CUDART error: {error_str}")
|
||||||
|
|
||||||
|
def cudaGetErrorString(self, error: cudaError_t) -> str:
|
||||||
|
return self.funcs["cudaGetErrorString"](error).decode("utf-8")
|
||||||
|
|
||||||
|
def cudaSetDevice(self, device: int) -> None:
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaSetDevice"](device))
|
||||||
|
|
||||||
|
def cudaDeviceSynchronize(self) -> None:
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaDeviceSynchronize"]())
|
||||||
|
|
||||||
|
def cudaDeviceReset(self) -> None:
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaDeviceReset"]())
|
||||||
|
|
||||||
|
def cudaMalloc(self, size: int) -> ctypes.c_void_p:
|
||||||
|
devPtr = ctypes.c_void_p()
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaMalloc"](ctypes.byref(devPtr), size))
|
||||||
|
return devPtr
|
||||||
|
|
||||||
|
def cudaFree(self, devPtr: ctypes.c_void_p) -> None:
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaFree"](devPtr))
|
||||||
|
|
||||||
|
def cudaMemset(self, devPtr: ctypes.c_void_p, value: int,
|
||||||
|
count: int) -> None:
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaMemset"](devPtr, value, count))
|
||||||
|
|
||||||
|
def cudaMemcpy(self, dst: ctypes.c_void_p, src: ctypes.c_void_p,
|
||||||
|
count: int) -> None:
|
||||||
|
cudaMemcpyDefault = 4
|
||||||
|
kind = cudaMemcpyDefault
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaMemcpy"](dst, src, count, kind))
|
||||||
|
|
||||||
|
def cudaIpcGetMemHandle(self,
|
||||||
|
devPtr: ctypes.c_void_p) -> cudaIpcMemHandle_t:
|
||||||
|
handle = cudaIpcMemHandle_t()
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaIpcGetMemHandle"](
|
||||||
|
ctypes.byref(handle), devPtr))
|
||||||
|
return handle
|
||||||
|
|
||||||
|
def cudaIpcOpenMemHandle(self,
|
||||||
|
handle: cudaIpcMemHandle_t) -> ctypes.c_void_p:
|
||||||
|
cudaIpcMemLazyEnablePeerAccess = 1
|
||||||
|
devPtr = ctypes.c_void_p()
|
||||||
|
self.CUDART_CHECK(self.funcs["cudaIpcOpenMemHandle"](
|
||||||
|
ctypes.byref(devPtr), handle, cudaIpcMemLazyEnablePeerAccess))
|
||||||
|
return devPtr
|
||||||
301
vllm/distributed/device_communicators/custom_all_reduce.py
Normal file
301
vllm/distributed/device_communicators/custom_all_reduce.py
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.distributed as dist
|
||||||
|
from torch.distributed import ProcessGroup
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.distributed.device_communicators.custom_all_reduce_utils import (
|
||||||
|
gpu_p2p_access_check)
|
||||||
|
from vllm.distributed.parallel_state import in_the_same_node_as
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.platforms import current_platform
|
||||||
|
from vllm.utils import cuda_device_count_stateless
|
||||||
|
|
||||||
|
try:
|
||||||
|
ops.meta_size()
|
||||||
|
custom_ar = True
|
||||||
|
except Exception:
|
||||||
|
# For CPUs
|
||||||
|
custom_ar = False
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _can_p2p(rank: int, world_size: int) -> bool:
|
||||||
|
for i in range(world_size):
|
||||||
|
if i == rank:
|
||||||
|
continue
|
||||||
|
if envs.VLLM_SKIP_P2P_CHECK:
|
||||||
|
logger.info(
|
||||||
|
"Skipping P2P check and trusting the driver's P2P report.")
|
||||||
|
return torch.cuda.can_device_access_peer(rank, i)
|
||||||
|
if not gpu_p2p_access_check(rank, i):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_weak_contiguous(inp: torch.Tensor):
|
||||||
|
return inp.is_contiguous() or (inp.storage().nbytes() -
|
||||||
|
inp.storage_offset() * inp.element_size()
|
||||||
|
== inp.numel() * inp.element_size())
|
||||||
|
|
||||||
|
|
||||||
|
class CustomAllreduce:
|
||||||
|
|
||||||
|
_SUPPORTED_WORLD_SIZES = [2, 4, 6, 8]
|
||||||
|
|
||||||
|
# max_size: max supported allreduce size
|
||||||
|
def __init__(self,
|
||||||
|
group: ProcessGroup,
|
||||||
|
device: Union[int, str, torch.device],
|
||||||
|
max_size=8192 * 1024) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
group: the process group to work on. If None, it will use the
|
||||||
|
default process group.
|
||||||
|
device: the device to bind the CustomAllreduce to. If None,
|
||||||
|
it will be bind to f"cuda:{local_rank}".
|
||||||
|
It is the caller's responsibility to make sure each communicator
|
||||||
|
is bind to a unique device, and all communicators in this group
|
||||||
|
are in the same node.
|
||||||
|
"""
|
||||||
|
self._IS_CAPTURING = False
|
||||||
|
self.disabled = True
|
||||||
|
|
||||||
|
if not custom_ar:
|
||||||
|
# disable because of missing custom allreduce library
|
||||||
|
# e.g. in a non-GPU environment
|
||||||
|
logger.info("Custom allreduce is disabled because "
|
||||||
|
"of missing custom allreduce library")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.group = group
|
||||||
|
|
||||||
|
assert dist.get_backend(group) != dist.Backend.NCCL, (
|
||||||
|
"CustomAllreduce should be attached to a non-NCCL group.")
|
||||||
|
|
||||||
|
if not all(in_the_same_node_as(group, source_rank=0)):
|
||||||
|
# No need to initialize custom allreduce for multi-node case.
|
||||||
|
logger.warning(
|
||||||
|
"Custom allreduce is disabled because this process group"
|
||||||
|
" spans across nodes.")
|
||||||
|
return
|
||||||
|
|
||||||
|
rank = dist.get_rank(group=self.group)
|
||||||
|
self.rank = rank
|
||||||
|
world_size = dist.get_world_size(group=self.group)
|
||||||
|
if world_size == 1:
|
||||||
|
# No need to initialize custom allreduce for single GPU case.
|
||||||
|
return
|
||||||
|
|
||||||
|
if world_size not in CustomAllreduce._SUPPORTED_WORLD_SIZES:
|
||||||
|
logger.warning(
|
||||||
|
"Custom allreduce is disabled due to an unsupported world"
|
||||||
|
" size: %d. Supported world sizes: %s. To silence this "
|
||||||
|
"warning, specify disable_custom_all_reduce=True explicitly.",
|
||||||
|
world_size, str(CustomAllreduce._SUPPORTED_WORLD_SIZES))
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(device, int):
|
||||||
|
device = torch.device(f"cuda:{device}")
|
||||||
|
elif isinstance(device, str):
|
||||||
|
device = torch.device(device)
|
||||||
|
# now `device` is a `torch.device` object
|
||||||
|
assert isinstance(device, torch.device)
|
||||||
|
self.device = device
|
||||||
|
|
||||||
|
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||||
|
if cuda_visible_devices:
|
||||||
|
device_ids = list(map(int, cuda_visible_devices.split(",")))
|
||||||
|
else:
|
||||||
|
device_ids = list(range(cuda_device_count_stateless()))
|
||||||
|
|
||||||
|
physical_device_id = device_ids[device.index]
|
||||||
|
tensor = torch.tensor([physical_device_id],
|
||||||
|
dtype=torch.int,
|
||||||
|
device="cpu")
|
||||||
|
gather_list = [
|
||||||
|
torch.tensor([0], dtype=torch.int, device="cpu")
|
||||||
|
for _ in range(world_size)
|
||||||
|
]
|
||||||
|
dist.all_gather(gather_list, tensor, group=self.group)
|
||||||
|
physical_device_ids = [t.item() for t in gather_list]
|
||||||
|
|
||||||
|
# test nvlink first, this will filter out most of the cases
|
||||||
|
# where custom allreduce is not supported
|
||||||
|
# this checks hardware and driver support for NVLink
|
||||||
|
assert current_platform.is_cuda_alike()
|
||||||
|
fully_connected = current_platform.is_fully_connected(
|
||||||
|
physical_device_ids)
|
||||||
|
if world_size > 2 and not fully_connected:
|
||||||
|
logger.warning(
|
||||||
|
"Custom allreduce is disabled because it's not supported on"
|
||||||
|
" more than two PCIe-only GPUs. To silence this warning, "
|
||||||
|
"specify disable_custom_all_reduce=True explicitly.")
|
||||||
|
return
|
||||||
|
# test P2P capability, this checks software/cudaruntime support
|
||||||
|
# this is expensive to compute at the first time
|
||||||
|
# then we cache the result
|
||||||
|
# On AMD GPU, p2p is always enabled between XGMI connected GPUs
|
||||||
|
if not current_platform.is_rocm() and not _can_p2p(rank, world_size):
|
||||||
|
logger.warning(
|
||||||
|
"Custom allreduce is disabled because your platform lacks "
|
||||||
|
"GPU P2P capability or P2P test failed. To silence this "
|
||||||
|
"warning, specify disable_custom_all_reduce=True explicitly.")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.disabled = False
|
||||||
|
# Buffers memory are owned by this Python class and passed to C++.
|
||||||
|
# Meta data composes of two parts: meta data for synchronization and a
|
||||||
|
# temporary buffer for storing intermediate allreduce results.
|
||||||
|
self.meta_ptrs = self.create_shared_buffer(ops.meta_size() + max_size,
|
||||||
|
group=group,
|
||||||
|
uncached=True)
|
||||||
|
# This is a pre-registered IPC buffer. In eager mode, input tensors
|
||||||
|
# are first copied into this buffer before allreduce is performed
|
||||||
|
self.buffer_ptrs = self.create_shared_buffer(max_size, group=group)
|
||||||
|
# This is a buffer for storing the tuples of pointers pointing to
|
||||||
|
# IPC buffers from all ranks. Each registered tuple has size of
|
||||||
|
# 8*world_size bytes where world_size is at most 8. Allocating 8MB
|
||||||
|
# is enough for 131072 such tuples. The largest model I've seen only
|
||||||
|
# needs less than 10000 of registered tuples.
|
||||||
|
self.rank_data = torch.empty(8 * 1024 * 1024,
|
||||||
|
dtype=torch.uint8,
|
||||||
|
device=self.device)
|
||||||
|
self.max_size = max_size
|
||||||
|
self.rank = rank
|
||||||
|
self.world_size = world_size
|
||||||
|
self.fully_connected = fully_connected
|
||||||
|
self._ptr = ops.init_custom_ar(self.meta_ptrs, self.rank_data, rank,
|
||||||
|
self.fully_connected)
|
||||||
|
ops.register_buffer(self._ptr, self.buffer_ptrs)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def capture(self):
|
||||||
|
"""
|
||||||
|
The main responsibility of this context manager is the
|
||||||
|
`register_graph_buffers` call at the end of the context.
|
||||||
|
It records all the buffer addresses used in the CUDA graph.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._IS_CAPTURING = True
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._IS_CAPTURING = False
|
||||||
|
if not self.disabled:
|
||||||
|
self.register_graph_buffers()
|
||||||
|
|
||||||
|
def register_graph_buffers(self):
|
||||||
|
handle, offset = ops.get_graph_buffer_ipc_meta(self._ptr)
|
||||||
|
logger.info("Registering %d cuda graph addresses", len(offset))
|
||||||
|
# We cannot directly use `dist.all_gather_object` here
|
||||||
|
# because it is incompatible with `gloo` backend under inference mode.
|
||||||
|
# see https://github.com/pytorch/pytorch/issues/126032 for details.
|
||||||
|
all_data = [[None, None]
|
||||||
|
for _ in range(dist.get_world_size(group=self.group))]
|
||||||
|
all_data[self.rank] = [handle, offset]
|
||||||
|
ranks = sorted(dist.get_process_group_ranks(group=self.group))
|
||||||
|
for i, rank in enumerate(ranks):
|
||||||
|
dist.broadcast_object_list(all_data[i],
|
||||||
|
src=rank,
|
||||||
|
group=self.group,
|
||||||
|
device="cpu")
|
||||||
|
# Unpack list of tuples to tuple of lists.
|
||||||
|
handles = [d[0] for d in all_data] # type: ignore
|
||||||
|
offsets = [d[1] for d in all_data] # type: ignore
|
||||||
|
ops.register_graph_buffers(self._ptr, handles, offsets)
|
||||||
|
|
||||||
|
def should_custom_ar(self, inp: torch.Tensor):
|
||||||
|
if self.disabled:
|
||||||
|
return False
|
||||||
|
inp_size = inp.numel() * inp.element_size()
|
||||||
|
# custom allreduce requires input byte size to be multiples of 16
|
||||||
|
if inp_size % 16 != 0:
|
||||||
|
return False
|
||||||
|
if not is_weak_contiguous(inp):
|
||||||
|
return False
|
||||||
|
# for 4 or more non NVLink-capable GPUs, custom allreduce provides
|
||||||
|
# little performance improvement over NCCL.
|
||||||
|
if self.world_size == 2 or self.fully_connected:
|
||||||
|
return inp_size < self.max_size
|
||||||
|
return False
|
||||||
|
|
||||||
|
def all_reduce(self,
|
||||||
|
inp: torch.Tensor,
|
||||||
|
*,
|
||||||
|
out: torch.Tensor = None,
|
||||||
|
registered: bool = False):
|
||||||
|
"""Performs an out-of-place all reduce.
|
||||||
|
|
||||||
|
If registered is True, this assumes inp's pointer is already
|
||||||
|
IPC-registered. Otherwise, inp is first copied into a pre-registered
|
||||||
|
buffer.
|
||||||
|
"""
|
||||||
|
if out is None:
|
||||||
|
out = torch.empty_like(inp)
|
||||||
|
if registered:
|
||||||
|
ops.all_reduce(self._ptr, inp, out, 0, 0)
|
||||||
|
else:
|
||||||
|
ops.all_reduce(self._ptr, inp, out, self.buffer_ptrs[self.rank],
|
||||||
|
self.max_size)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def custom_all_reduce(self, input: torch.Tensor) -> Optional[torch.Tensor]:
|
||||||
|
"""The main allreduce API that provides support for cuda graph."""
|
||||||
|
# When custom allreduce is disabled, this will be None.
|
||||||
|
if self.disabled or not self.should_custom_ar(input):
|
||||||
|
return None
|
||||||
|
if self._IS_CAPTURING:
|
||||||
|
if torch.cuda.is_current_stream_capturing():
|
||||||
|
return self.all_reduce(input, registered=True)
|
||||||
|
else:
|
||||||
|
# If warm up, mimic the allocation pattern since custom
|
||||||
|
# allreduce is out-of-place.
|
||||||
|
return torch.empty_like(input)
|
||||||
|
else:
|
||||||
|
# Note: outside of cuda graph context, custom allreduce incurs a
|
||||||
|
# cost of cudaMemcpy, which should be small (<=1% of overall
|
||||||
|
# latency) compared to the performance gain of using custom kernels
|
||||||
|
return self.all_reduce(input, registered=False)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if not self.disabled and self._ptr:
|
||||||
|
ops.dispose(self._ptr)
|
||||||
|
self._ptr = 0
|
||||||
|
self.free_shared_buffer(self.meta_ptrs, rank=self.rank)
|
||||||
|
self.free_shared_buffer(self.buffer_ptrs, rank=self.rank)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_shared_buffer(size_in_bytes: int,
|
||||||
|
group: Optional[ProcessGroup] = None,
|
||||||
|
uncached: Optional[bool] = False) -> List[int]:
|
||||||
|
pointer, handle = ops.allocate_shared_buffer_and_handle(size_in_bytes)
|
||||||
|
|
||||||
|
world_size = dist.get_world_size(group=group)
|
||||||
|
rank = dist.get_rank(group=group)
|
||||||
|
handles = [None] * world_size
|
||||||
|
dist.all_gather_object(handles, handle, group=group)
|
||||||
|
|
||||||
|
pointers: List[int] = []
|
||||||
|
for i, h in enumerate(handles):
|
||||||
|
if i == rank:
|
||||||
|
pointers.append(pointer) # type: ignore
|
||||||
|
else:
|
||||||
|
pointers.append(ops.open_mem_handle(h))
|
||||||
|
return pointers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def free_shared_buffer(pointers: List[int],
|
||||||
|
group: Optional[ProcessGroup] = None,
|
||||||
|
rank: Optional[int] = 0) -> None:
|
||||||
|
if rank is None:
|
||||||
|
rank = dist.get_rank(group=group)
|
||||||
|
ops.free_shared_buffer(pointers[rank])
|
||||||
257
vllm/distributed/device_communicators/custom_all_reduce_utils.py
Normal file
257
vllm/distributed/device_communicators/custom_all_reduce_utils.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from itertools import product
|
||||||
|
from typing import Dict, List, Optional, Sequence
|
||||||
|
|
||||||
|
import torch.distributed as dist
|
||||||
|
import torch.multiprocessing as mp
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.distributed.device_communicators.cuda_wrapper import CudaRTLibrary
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.utils import (cuda_device_count_stateless,
|
||||||
|
update_environment_variables)
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def producer(batch_src: Sequence[int],
|
||||||
|
producer_queue,
|
||||||
|
consumer_queue,
|
||||||
|
result_queue,
|
||||||
|
cuda_visible_devices: Optional[str] = None):
|
||||||
|
if cuda_visible_devices is not None:
|
||||||
|
update_environment_variables(
|
||||||
|
{"CUDA_VISIBLE_DEVICES": cuda_visible_devices})
|
||||||
|
|
||||||
|
lib = CudaRTLibrary()
|
||||||
|
for i in batch_src:
|
||||||
|
lib.cudaSetDevice(i)
|
||||||
|
pointer = lib.cudaMalloc(1024)
|
||||||
|
lib.cudaMemset(pointer, 1, 1024)
|
||||||
|
lib.cudaDeviceSynchronize()
|
||||||
|
handle = lib.cudaIpcGetMemHandle(pointer)
|
||||||
|
producer_queue.put(handle)
|
||||||
|
open_success = consumer_queue.get()
|
||||||
|
if open_success:
|
||||||
|
# use two queues to simulate barrier
|
||||||
|
producer_queue.put(0)
|
||||||
|
consumer_queue.get()
|
||||||
|
# check if the memory is modified
|
||||||
|
host_data = (ctypes.c_char * 1024)()
|
||||||
|
lib.cudaMemcpy(host_data, pointer, 1024) # type: ignore
|
||||||
|
for i in range(1024):
|
||||||
|
if ord(host_data[i]) != 2:
|
||||||
|
open_success = False
|
||||||
|
break
|
||||||
|
result_queue.put(open_success)
|
||||||
|
lib.cudaDeviceReset()
|
||||||
|
|
||||||
|
|
||||||
|
def consumer(batch_tgt: Sequence[int],
|
||||||
|
producer_queue,
|
||||||
|
consumer_queue,
|
||||||
|
result_queue,
|
||||||
|
cuda_visible_devices: Optional[str] = None):
|
||||||
|
if cuda_visible_devices is not None:
|
||||||
|
update_environment_variables(
|
||||||
|
{"CUDA_VISIBLE_DEVICES": cuda_visible_devices})
|
||||||
|
|
||||||
|
lib = CudaRTLibrary()
|
||||||
|
for j in batch_tgt:
|
||||||
|
lib.cudaSetDevice(j)
|
||||||
|
handle = producer_queue.get()
|
||||||
|
open_success = False
|
||||||
|
try:
|
||||||
|
pointer = lib.cudaIpcOpenMemHandle(handle) # type: ignore
|
||||||
|
open_success = True
|
||||||
|
except RuntimeError:
|
||||||
|
# cannot error out here, because the producer process
|
||||||
|
# is still waiting for the response.
|
||||||
|
pass
|
||||||
|
consumer_queue.put(open_success)
|
||||||
|
if open_success:
|
||||||
|
# modify the memory
|
||||||
|
lib.cudaMemset(pointer, 2, 1024)
|
||||||
|
lib.cudaDeviceSynchronize()
|
||||||
|
# use two queues to simulate barrier
|
||||||
|
producer_queue.get()
|
||||||
|
consumer_queue.put(0)
|
||||||
|
# check if the memory is modified
|
||||||
|
host_data = (ctypes.c_char * 1024)()
|
||||||
|
lib.cudaMemcpy(host_data, pointer, 1024) # type: ignore
|
||||||
|
for i in range(1024):
|
||||||
|
if ord(host_data[i]) != 2:
|
||||||
|
open_success = False
|
||||||
|
break
|
||||||
|
result_queue.put(open_success)
|
||||||
|
lib.cudaDeviceReset()
|
||||||
|
|
||||||
|
|
||||||
|
def can_actually_p2p(
|
||||||
|
batch_src: Sequence[int],
|
||||||
|
batch_tgt: Sequence[int],
|
||||||
|
) -> Sequence[bool]:
|
||||||
|
"""
|
||||||
|
Usually, checking if P2P access is enabled can be done by
|
||||||
|
`torch.cuda.can_device_access_peer(src, tgt)`. However, sometimes
|
||||||
|
the driver might be broken, and `torch.cuda.can_device_access_peer(src, tgt)`
|
||||||
|
returns `True` even if P2P access is not actually possible.
|
||||||
|
See https://github.com/vllm-project/vllm/issues/2728 and
|
||||||
|
https://forums.developer.nvidia.com/t/direct-gpu-gpu-communication-does-not-seem-to-work-properly/283264/10
|
||||||
|
Therefore, we have to perform a real P2P access to check if it is actually
|
||||||
|
possible.
|
||||||
|
|
||||||
|
Note on p2p and cuda IPC:
|
||||||
|
Usually, one process uses one GPU:
|
||||||
|
GPU src --> cuda context src --> tensor src --> process src
|
||||||
|
|
||||||
|
We need to combine p2p and cuda IPC, so that:
|
||||||
|
GPU src --> cuda context src --> tensor src --> process src
|
||||||
|
|shared|
|
||||||
|
GPU tgt --> cuda context tgt --> tensor tgt --> process tgt
|
||||||
|
That is to say, process src creates a tensor in GPU src, passes IPC handle to
|
||||||
|
process tgt, and process tgt accesses the tensor in GPU tgt. Any operation on the
|
||||||
|
tensor in process tgt will be reflected in the tensor in process src, because
|
||||||
|
they are the same memory segment.
|
||||||
|
It is important to note that process tgt accesses the tensor in GPU tgt, not
|
||||||
|
GPU src. That's why we need p2p access.
|
||||||
|
|
||||||
|
The most time-consuming part is the process creation. To avoid creating
|
||||||
|
processes for every pair of GPUs, we use batched testing. We create two
|
||||||
|
processes for testing all pairs of GPUs in batch. The trick is to reset
|
||||||
|
the device after each test (which is not available in PyTorch).
|
||||||
|
""" # noqa
|
||||||
|
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||||
|
# pass the CUDA_VISIBLE_DEVICES to the child process
|
||||||
|
# to make sure they see the same set of GPUs
|
||||||
|
|
||||||
|
# make sure the processes are spawned
|
||||||
|
smp = mp.get_context("spawn")
|
||||||
|
producer_queue = smp.Queue()
|
||||||
|
consumer_queue = smp.Queue()
|
||||||
|
result_queue = smp.Queue()
|
||||||
|
p_src = smp.Process(target=producer,
|
||||||
|
args=(batch_src, producer_queue, consumer_queue,
|
||||||
|
result_queue, cuda_visible_devices))
|
||||||
|
p_tgt = smp.Process(target=consumer,
|
||||||
|
args=(batch_tgt, producer_queue, consumer_queue,
|
||||||
|
result_queue, cuda_visible_devices))
|
||||||
|
p_src.start()
|
||||||
|
p_tgt.start()
|
||||||
|
p_src.join()
|
||||||
|
p_tgt.join()
|
||||||
|
assert p_src.exitcode == 0 and p_tgt.exitcode == 0
|
||||||
|
result: List[bool] = []
|
||||||
|
for src, tgt in zip(batch_src, batch_tgt):
|
||||||
|
a = result_queue.get()
|
||||||
|
b = result_queue.get()
|
||||||
|
if a != b:
|
||||||
|
logger.warning(
|
||||||
|
"Two processes do not agree on the P2P access"
|
||||||
|
" status on %d -> %d, treat as disabled.", src, tgt)
|
||||||
|
result.append(False)
|
||||||
|
else:
|
||||||
|
result.append(a)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# why do we need this cache?
|
||||||
|
# we are testing peer-to-peer (p2p) access between GPUs,across processes.
|
||||||
|
# if we test it every time, it will be very slow, because we need to create
|
||||||
|
# N * N * 2 processes, where N is the world size. This is very slow.
|
||||||
|
# to reduce the time, we use a cache file to store the p2p access status.
|
||||||
|
# the cache file is generated by the master process if it does not exist.
|
||||||
|
# then all the processes can read the cache file to check the p2p access status.
|
||||||
|
# Note that the cache file is suffixed by the CUDA_VISIBLE_DEVICES, so that we
|
||||||
|
# can have different cache files for different CUDA_VISIBLE_DEVICES settings,
|
||||||
|
# e.g. used by different vllm engines. The device id in the cache file is a
|
||||||
|
# **local** device id, i.e. from 0 to num_dev-1, where num_dev is the number
|
||||||
|
# of visible devices in the vllm engine.
|
||||||
|
_gpu_p2p_access_cache: Optional[Dict[str, bool]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_p2p_access_check(src: int, tgt: int) -> bool:
|
||||||
|
"""Check if GPU src can access GPU tgt."""
|
||||||
|
|
||||||
|
# if the cache variable is already calculated,
|
||||||
|
# read from the cache instead of checking it again
|
||||||
|
global _gpu_p2p_access_cache
|
||||||
|
if _gpu_p2p_access_cache is not None:
|
||||||
|
return _gpu_p2p_access_cache[f"{src}->{tgt}"]
|
||||||
|
|
||||||
|
is_distributed = dist.is_initialized()
|
||||||
|
|
||||||
|
num_dev = cuda_device_count_stateless()
|
||||||
|
cuda_visible_devices = envs.CUDA_VISIBLE_DEVICES
|
||||||
|
if cuda_visible_devices is None:
|
||||||
|
cuda_visible_devices = ",".join(str(i) for i in range(num_dev))
|
||||||
|
|
||||||
|
path = os.path.join(
|
||||||
|
envs.VLLM_CACHE_ROOT,
|
||||||
|
f"gpu_p2p_access_cache_for_{cuda_visible_devices}.json")
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
from vllm.distributed.parallel_state import get_world_group
|
||||||
|
if ((not is_distributed or get_world_group().local_rank == 0)
|
||||||
|
and (not os.path.exists(path))):
|
||||||
|
# only the local master process (with local_rank == 0) can
|
||||||
|
# enter this block to calculate the cache
|
||||||
|
logger.info("generating GPU P2P access cache in %s", path)
|
||||||
|
cache: Dict[str, bool] = {}
|
||||||
|
ids = list(range(num_dev))
|
||||||
|
# batch of all pairs of GPUs
|
||||||
|
batch_src, batch_tgt = zip(*list(product(ids, ids)))
|
||||||
|
# NOTE: we use `subprocess` rather than `multiprocessing` here
|
||||||
|
# because the caller might not have `if __name__ == "__main__":`,
|
||||||
|
# in that case we cannot use spawn method in multiprocessing.
|
||||||
|
# However, `can_actually_p2p` requires spawn method.
|
||||||
|
# The fix is, we use `subprocess` to call the function,
|
||||||
|
# where we have `if __name__ == "__main__":` in this file.
|
||||||
|
|
||||||
|
# use a temporary file to store the result
|
||||||
|
# we don't use the output of the subprocess directly,
|
||||||
|
# because the subprocess might produce logging output
|
||||||
|
with tempfile.NamedTemporaryFile() as output_file:
|
||||||
|
input_bytes = pickle.dumps(
|
||||||
|
(batch_src, batch_tgt, output_file.name))
|
||||||
|
returned = subprocess.run([sys.executable, __file__],
|
||||||
|
input=input_bytes,
|
||||||
|
capture_output=True)
|
||||||
|
# check if the subprocess is successful
|
||||||
|
try:
|
||||||
|
returned.check_returncode()
|
||||||
|
except Exception as e:
|
||||||
|
# wrap raised exception to provide more information
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Error happened when batch testing "
|
||||||
|
f"peer-to-peer access from {batch_src} to {batch_tgt}:\n"
|
||||||
|
f"{returned.stderr.decode()}") from e
|
||||||
|
with open(output_file.name, "rb") as f:
|
||||||
|
result = pickle.load(f)
|
||||||
|
for _i, _j, r in zip(batch_src, batch_tgt, result):
|
||||||
|
cache[f"{_i}->{_j}"] = r
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(cache, f, indent=4)
|
||||||
|
if is_distributed:
|
||||||
|
get_world_group().barrier()
|
||||||
|
logger.info("reading GPU P2P access cache from %s", path)
|
||||||
|
with open(path) as f:
|
||||||
|
cache = json.load(f)
|
||||||
|
_gpu_p2p_access_cache = cache
|
||||||
|
return _gpu_p2p_access_cache[f"{src}->{tgt}"]
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["gpu_p2p_access_check"]
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
batch_src, batch_tgt, output_file = pickle.loads(sys.stdin.buffer.read())
|
||||||
|
result = can_actually_p2p(batch_src, batch_tgt)
|
||||||
|
with open(output_file, "wb") as f:
|
||||||
|
f.write(pickle.dumps(result))
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user