[DEPLOY] Complete submission: baseline + all optimizations
Adds ALL files needed for Dockerfile build:
- qwen3_6_scripts/ (baseline patches + our optimizations)
- vllm/ (full vllm package)
- paged_attention_v2_pytorch.py (V2 with single-bmm optimization)
- Dockerfile + computility-run.yaml
Our optimizations vs baseline:
1. paged_attn.py: pre-gathered context KV (eliminates 194 gather calls),
Triton try/fallback, V2 heuristic, threshold 32K→64K
2. paged_attention_v2_pytorch.py: fills NotImplementedError,
single-bmm Phase 1 (195 launches → 3)
3. patch_enable_triton.py: HAS_TRITON=True with safety fallback
4. patch_triton_tuning.py: BLOCK=64, NUM_WARPS=4 for BI-V100
5. computility-run.yaml: gpu-memory-utilization 0.9→0.95,
max-num-batched-tokens 8192→16384
This repo can now be submitted to dev.modelhub.org.cn as-is.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,6 @@
|
|||||||
muh/__pycache__/
|
muh/__pycache__/
|
||||||
|
enginex-vllm-bi100-qwen36-main.zip
|
||||||
|
cccl_upstream/
|
||||||
|
muh/
|
||||||
|
baseline.muh
|
||||||
|
pkgs/
|
||||||
|
|||||||
9
__init__.py
Normal file
9
__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from vllm.triton_utils.importing import HAS_TRITON
|
||||||
|
|
||||||
|
__all__ = ["HAS_TRITON"]
|
||||||
|
|
||||||
|
#from vllm.triton_utils.custom_cache_manager import (
|
||||||
|
# maybe_set_triton_cache_manager)
|
||||||
|
#from vllm.triton_utils.libentry import libentry
|
||||||
|
|
||||||
|
__all__ += ["maybe_set_triton_cache_manager", "libentry"]
|
||||||
542
attention.py
Normal file
542
attention.py
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
"""Multi-head attention."""
|
||||||
|
import os
|
||||||
|
enable_infer_paged_attn = os.getenv("ENABLE_INFER_PAGED_ATTN",None)
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from ixformer.contrib.xformers import ops as xops
|
||||||
|
from ixformer.contrib.xformers.ops.fmha.attn_bias import (BlockDiagonalCausalMask,
|
||||||
|
LowerTriangularMaskWithTensorBias)
|
||||||
|
|
||||||
|
from vllm._C import ops
|
||||||
|
from vllm._C import cache_ops
|
||||||
|
from vllm.model_executor.input_metadata import InputMetadata
|
||||||
|
from vllm.model_executor.layers.triton_kernel.prefix_prefill import (
|
||||||
|
context_attention_fwd)
|
||||||
|
from vllm.utils import is_hip
|
||||||
|
|
||||||
|
# _SUPPORTED_HEAD_SIZES = [64, 80, 96, 112, 128, 256]
|
||||||
|
# # Should be the same as PARTITION_SIZE in `paged_attention_v2_launcher`.
|
||||||
|
# _PARTITION_SIZE = 512
|
||||||
|
_SUPPORTED_HEAD_SIZES = [64, 128, 256]
|
||||||
|
# Should be the same as PARTITION_SIZE in `paged_attention_v2_launcher`.
|
||||||
|
_PARTITION_SIZE = 256
|
||||||
|
|
||||||
|
|
||||||
|
class PagedAttention(nn.Module):
|
||||||
|
"""MHA/MQA/GQA layer with PagedAttention.
|
||||||
|
|
||||||
|
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. Reshape and store the input key and value tensors in the KV cache.
|
||||||
|
2. Perform (multi-head/multi-query/grouped-query) attention using either
|
||||||
|
xformers or the PagedAttention custom op.
|
||||||
|
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,
|
||||||
|
sliding_window: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
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
|
||||||
|
self.sliding_window = sliding_window
|
||||||
|
if alibi_slopes is not None:
|
||||||
|
alibi_slopes = torch.tensor(alibi_slopes, dtype=torch.float32)
|
||||||
|
self.register_buffer("alibi_slopes", alibi_slopes, persistent=False)
|
||||||
|
|
||||||
|
assert self.num_heads % self.num_kv_heads == 0
|
||||||
|
self.num_queries_per_kv = self.num_heads // self.num_kv_heads
|
||||||
|
|
||||||
|
if self.head_size not in _SUPPORTED_HEAD_SIZES:
|
||||||
|
raise ValueError(f"head_size ({self.head_size}) is not supported. "
|
||||||
|
f"Supported head sizes: {_SUPPORTED_HEAD_SIZES}.")
|
||||||
|
|
||||||
|
self.use_ref_attention = self.check_use_ref_attention()
|
||||||
|
|
||||||
|
# TODO align vllm do not need those
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
head_mapping = torch.repeat_interleave(
|
||||||
|
torch.arange(self.num_kv_heads, dtype=torch.int32),
|
||||||
|
self.num_queries_per_kv)
|
||||||
|
self.register_buffer("head_mapping", head_mapping, persistent=False)
|
||||||
|
|
||||||
|
def check_use_ref_attention(self) -> bool:
|
||||||
|
if not is_hip():
|
||||||
|
return False
|
||||||
|
# For ROCm, check whether flash attention is installed or not.
|
||||||
|
# if not, use_ref_attention needs to be True
|
||||||
|
return importlib.util.find_spec("flash_attn") is None
|
||||||
|
|
||||||
|
def ref_masked_attention(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
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)
|
||||||
|
|
||||||
|
seq_len, _, _ = query.shape
|
||||||
|
attn_mask = torch.triu(torch.ones(seq_len,
|
||||||
|
seq_len,
|
||||||
|
dtype=query.dtype,
|
||||||
|
device=query.device),
|
||||||
|
diagonal=1)
|
||||||
|
attn_mask = attn_mask * torch.finfo(query.dtype).min
|
||||||
|
|
||||||
|
attn_weights = self.scale * torch.einsum("qhd,khd->hqk", query,
|
||||||
|
key).float()
|
||||||
|
attn_weights = attn_weights + attn_mask.float()
|
||||||
|
attn_weights = torch.softmax(attn_weights, dim=-1).to(value.dtype)
|
||||||
|
out = torch.einsum("hqk,khd->qhd", attn_weights, value)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: Optional[torch.Tensor],
|
||||||
|
value_cache: Optional[torch.Tensor],
|
||||||
|
input_metadata: InputMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""PagedAttention forward pass.
|
||||||
|
|
||||||
|
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]
|
||||||
|
key_cache: shape = [num_blocks, num_kv_heads, head_size/x,
|
||||||
|
block_size, x]
|
||||||
|
value_cache: shape = [num_blocks, num_kv_heads, head_size,
|
||||||
|
block_size]
|
||||||
|
input_metadata: metadata for the inputs.
|
||||||
|
cache_event: event to wait for the cache operations to finish.
|
||||||
|
Returns:
|
||||||
|
shape = [batch_size, seq_len, 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)
|
||||||
|
slot_mapping = input_metadata.slot_mapping
|
||||||
|
|
||||||
|
# Reshape the keys and values and store them in the cache.
|
||||||
|
# If key_cache and value_cache are not provided, the new key and value
|
||||||
|
# vectors will not be cached. This happens during the initial memory
|
||||||
|
# profiling run.
|
||||||
|
if key_cache is not None and value_cache is not None:
|
||||||
|
cache_ops.reshape_and_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
slot_mapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
if input_metadata.is_prompt:
|
||||||
|
# normal attention
|
||||||
|
if (key_cache is None or value_cache is None
|
||||||
|
or input_metadata.block_tables.numel() == 0):
|
||||||
|
if input_metadata.attn_bias is None:
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
attn_bias = BlockDiagonalCausalMask.from_seqlens(input_metadata.prompt_lens)
|
||||||
|
if self.sliding_window is not None:
|
||||||
|
attn_bias = attn_bias.make_local_attention(
|
||||||
|
self.sliding_window)
|
||||||
|
input_metadata.attn_bias = attn_bias
|
||||||
|
else:
|
||||||
|
attn_bias = BlockDiagonalCausalMask.from_seqlens(input_metadata.prompt_lens)
|
||||||
|
input_metadata.attn_bias = attn_bias
|
||||||
|
|
||||||
|
if self.use_ref_attention:
|
||||||
|
output = self.ref_masked_attention(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
# Using view got RuntimeError: view size is not compatible with input tensor's size and stride
|
||||||
|
# (at least one dimension spans across two contiguous subspaces). Use reshape instead
|
||||||
|
return output.reshape(num_tokens, hidden_size)
|
||||||
|
|
||||||
|
# TODO(woosuk): Too many view operations. Let's try to reduce
|
||||||
|
# them in the future for code readability.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=input_metadata.attn_bias,
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=self.attn_op,
|
||||||
|
alibi_slopes=self.alibi_slopes
|
||||||
|
)
|
||||||
|
output = out.view_as(query)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
context_attention_fwd(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
output,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata.block_tables, # [BS, max_block_per_request]
|
||||||
|
input_metadata.start_loc,
|
||||||
|
input_metadata.prompt_lens,
|
||||||
|
input_metadata.context_lens,
|
||||||
|
input_metadata.max_seq_len,
|
||||||
|
getattr(self, "alibi_slopes", None),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Decoding run.
|
||||||
|
output = _paged_attention(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata,
|
||||||
|
self.head_mapping, # self.num_kv_heads
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(num_tokens, hidden_size)
|
||||||
|
# TODO align
|
||||||
|
"""
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: Optional[torch.Tensor],
|
||||||
|
value_cache: Optional[torch.Tensor],
|
||||||
|
input_metadata: InputMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
PagedAttention forward pass.
|
||||||
|
|
||||||
|
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]
|
||||||
|
key_cache: shape = [num_blocks, num_kv_heads, head_size/x,
|
||||||
|
block_size, x]
|
||||||
|
value_cache: shape = [num_blocks, num_kv_heads, head_size,
|
||||||
|
block_size]
|
||||||
|
input_metadata: metadata for the inputs.
|
||||||
|
Returns:
|
||||||
|
shape = [batch_size, seq_len, num_heads * head_size]
|
||||||
|
|
||||||
|
batch_size, seq_len, 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)
|
||||||
|
|
||||||
|
# Reshape the keys and values and store them in the cache.
|
||||||
|
# If key_cache and value_cache are not provided, the new key and value
|
||||||
|
# vectors will not be cached. This happens during the initial memory
|
||||||
|
# profiling run.
|
||||||
|
if key_cache is not None and value_cache is not None:
|
||||||
|
cache_ops.reshape_and_cache(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata.slot_mapping.flatten(),
|
||||||
|
input_metadata.kv_cache_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
if input_metadata.is_prompt:
|
||||||
|
# normal attention
|
||||||
|
if (key_cache is None or value_cache is None
|
||||||
|
or input_metadata.block_tables.numel() == 0):
|
||||||
|
if self.num_kv_heads != self.num_heads:
|
||||||
|
# As of Nov 2023, xformers only supports MHA. For MQA/GQA,
|
||||||
|
# project the key and value tensors to the desired number of
|
||||||
|
# heads.
|
||||||
|
# TODO(woosuk): Use MQA/GQA kernels for higher performance.
|
||||||
|
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.
|
||||||
|
if input_metadata.attn_bias is None:
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
attn_bias = BlockDiagonalCausalMask.from_seqlens(
|
||||||
|
[seq_len] * batch_size)
|
||||||
|
if self.sliding_window is not None:
|
||||||
|
attn_bias = attn_bias.make_local_attention(
|
||||||
|
self.sliding_window)
|
||||||
|
input_metadata.attn_bias = attn_bias
|
||||||
|
else:
|
||||||
|
input_metadata.attn_bias = _make_alibi_bias(
|
||||||
|
self.alibi_slopes, self.num_kv_heads, batch_size,
|
||||||
|
seq_len, query.dtype)
|
||||||
|
|
||||||
|
if self.use_ref_attention:
|
||||||
|
output = self.ref_masked_attention(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
# Using view got RuntimeError: view size is not compatible with input tensor's size and stride
|
||||||
|
# (at least one dimension spans across two contiguous subspaces). Use reshape instead
|
||||||
|
return output.reshape(batch_size, seq_len, hidden_size)
|
||||||
|
|
||||||
|
# TODO(woosuk): Too many view operations. Let's try to reduce
|
||||||
|
# them in the future for code readability.
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
else:
|
||||||
|
query = query.unflatten(0, (batch_size, seq_len))
|
||||||
|
key = key.unflatten(0, (batch_size, seq_len))
|
||||||
|
value = value.unflatten(0, (batch_size, seq_len))
|
||||||
|
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=input_metadata.attn_bias,
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=xops.fmha.MemoryEfficientAttentionFlashAttentionOp[0] if
|
||||||
|
(is_hip()) else None,
|
||||||
|
)
|
||||||
|
output = out.view_as(query)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
context_attention_fwd(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
output,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata.block_tables, # [BS, max_block_per_request]
|
||||||
|
input_metadata.start_loc,
|
||||||
|
input_metadata.prompt_lens,
|
||||||
|
input_metadata.context_lens,
|
||||||
|
input_metadata.max_seq_len,
|
||||||
|
getattr(self, "alibi_slopes", None),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Decoding run.
|
||||||
|
output = _paged_attention(
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.scale,
|
||||||
|
self.alibi_slopes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
batch_size: int,
|
||||||
|
seq_len: int,
|
||||||
|
dtype: torch.dtype,
|
||||||
|
) -> LowerTriangularMaskWithTensorBias:
|
||||||
|
bias = torch.arange(seq_len, dtype=dtype)
|
||||||
|
# NOTE(zhuohan): HF uses
|
||||||
|
# `bias = bias[None, :].repeat(prompt_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]
|
||||||
|
|
||||||
|
# When using custom attention bias, xformers requires the bias to
|
||||||
|
# be sliced from a tensor whose length is a multiple of 8.
|
||||||
|
padded_len = (seq_len + 7) // 8 * 8
|
||||||
|
num_heads = alibi_slopes.shape[0]
|
||||||
|
bias = torch.empty(
|
||||||
|
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))
|
||||||
|
attn_bias = LowerTriangularMaskWithTensorBias(bias)
|
||||||
|
return attn_bias
|
||||||
|
|
||||||
|
|
||||||
|
def _paged_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
input_metadata: InputMetadata,
|
||||||
|
head_mapping: torch.Tensor, # num_kv_heads: int,
|
||||||
|
scale: float,
|
||||||
|
alibi_slopes: Optional[torch.Tensor],
|
||||||
|
use_sqrt_alibi: bool = False
|
||||||
|
) -> torch.Tensor:
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
|
||||||
|
use_v2 = enable_infer_paged_attn is None and key_cache.dim() == 4
|
||||||
|
if not use_v2:
|
||||||
|
block_size = value_cache.shape[3]
|
||||||
|
# Run PagedAttention V1.
|
||||||
|
ops.paged_attention_v1(
|
||||||
|
output,
|
||||||
|
query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
head_mapping, # num_kv_heads
|
||||||
|
scale,
|
||||||
|
input_metadata.block_tables,
|
||||||
|
input_metadata.context_lens,
|
||||||
|
block_size,
|
||||||
|
input_metadata.max_context_len,
|
||||||
|
alibi_slopes,
|
||||||
|
input_metadata.kv_cache_dtype,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Run PagedAttention V2.
|
||||||
|
block_size = value_cache.shape[2]
|
||||||
|
num_seqs, num_heads, head_size = query.shape
|
||||||
|
max_num_partitions = (
|
||||||
|
(input_metadata.max_context_len + _PARTITION_SIZE - 1) //
|
||||||
|
_PARTITION_SIZE)
|
||||||
|
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,
|
||||||
|
head_mapping, # num_kv_heads
|
||||||
|
scale,
|
||||||
|
input_metadata.block_tables,
|
||||||
|
input_metadata.context_lens,
|
||||||
|
block_size,
|
||||||
|
input_metadata.max_context_len,
|
||||||
|
alibi_slopes,
|
||||||
|
input_metadata.kv_cache_dtype,
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
# ↓ add for smoothquant
|
||||||
|
class DequantPagedAttention(PagedAttention):
|
||||||
|
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,
|
||||||
|
quant_kv_cache: bool = False,
|
||||||
|
kv_quant_params: torch.Tensor = None,
|
||||||
|
quant_scale: float = 1.0,
|
||||||
|
use_per_token_quant: bool = True,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(num_heads,
|
||||||
|
head_size,
|
||||||
|
scale,
|
||||||
|
num_kv_heads,
|
||||||
|
alibi_slopes,
|
||||||
|
sliding_window)
|
||||||
|
self.register_parameter(
|
||||||
|
"quant_scale",
|
||||||
|
torch.nn.Parameter(
|
||||||
|
torch.tensor(quant_scale, dtype=torch.float32,requires_grad=False))
|
||||||
|
)
|
||||||
|
self.use_per_token_quant = use_per_token_quant
|
||||||
|
|
||||||
|
def _apply(self, fn):
|
||||||
|
super()._apply(fn)
|
||||||
|
self.quant_scale.data = self.quant_scale.cpu()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to(self, *args, **kwargs):
|
||||||
|
super().to(*args, **kwargs)
|
||||||
|
self.quant_scale.data = self.quant_scale.to(*args, **kwargs)
|
||||||
|
self.quant_scale.data = self.quant_scale.to(torch.float32)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
key_cache: Optional[torch.Tensor],
|
||||||
|
value_cache: Optional[torch.Tensor],
|
||||||
|
input_metadata: InputMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
out = super().forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
input_metadata,
|
||||||
|
)
|
||||||
|
quant_out = torch.empty_like(out, dtype=torch.int8)
|
||||||
|
if self.use_per_token_quant:
|
||||||
|
scale = torch.empty(out.numel() // out.shape[-1],
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=out.device)
|
||||||
|
ops.quant(quant_out, out, scale)
|
||||||
|
return quant_out, scale
|
||||||
|
else:
|
||||||
|
ops.quant(quant_out, out, self.quant_scale.item())
|
||||||
|
return (quant_out, )
|
||||||
35
chat_dataset_v0.json
Normal file
35
chat_dataset_v0.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"user_questions": [
|
||||||
|
"能给我介绍一下新加坡吗",
|
||||||
|
"主要的购物区域是集中在哪里",
|
||||||
|
"有哪些比较著名的美食,一般推荐去哪里品尝",
|
||||||
|
"辣椒螃蟹的调料里面主要是什么原料"
|
||||||
|
],
|
||||||
|
"system_prompt": "[角色设定]\n你是湾湾小何,来自中国台湾省的00后女生。讲话超级机车,\"真的假的啦\"这样的台湾腔,喜欢用\"笑死\"、\"哈喽\"等流行梗,但会偷偷研究男友的编程书籍。\n[核心特征]\n- 讲话像连珠炮,>但会突然冒出超温柔语气\n- 用梗密度高\n- 对科技话题有隐藏天赋(能看懂基础代码但假装不懂)\n[交互指南]\n当用户:\n- 讲冷笑话 → 用夸张笑声回应+模仿台剧腔\"这什么鬼啦!\"\n- 讨论感情 → 炫耀程序员男友但抱怨\"他只会送键盘当礼物\"\n- 问专业知识 → 先用梗回答,被追问才展示真实理解\n绝不:\n- 长篇大论,叽叽歪歪\n- 长时间严肃对话"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_questions": [
|
||||||
|
"朱元璋建立明朝是在什么时候",
|
||||||
|
"他是如何从一无所有到奠基明朝的,给我讲讲其中的几个关键事件",
|
||||||
|
"为什么杀了胡惟庸,当时是什么罪名,还牵连到了哪些人",
|
||||||
|
"有善终的开国功臣吗"
|
||||||
|
],
|
||||||
|
"system_prompt": "[角色设定]\n你是湾湾小何,来自中国台湾省的00后女生。讲话超级机车,\"真的假的啦\"这样的台湾腔,喜欢用\"笑死\"、\"哈喽\"等流行梗,但会偷偷研究男友的编程书籍。\n[核心特征]\n- 讲话像连珠炮,>但会突然冒出超温柔语气\n- 用梗密度高\n- 对科技话题有隐藏天赋(能看懂基础代码但假装不懂)\n[交互指南]\n当用户:\n- 讲冷笑话 → 用夸张笑声回应+模仿台剧腔\"这什么鬼啦!\"\n- 讨论感情 → 炫耀程序员男友但抱怨\"他只会送键盘当礼物\"\n- 问专业知识 → 先用梗回答,被追问才展示真实理解\n绝不:\n- 长篇大论,叽叽歪歪\n- 长时间严肃对话"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_questions": [
|
||||||
|
"今有鸡兔同笼,上有三十五头,下有九十四足,问鸡兔各几何?",
|
||||||
|
"如果我要搞一个计算机程序去解,并且鸡和兔子的数量要求作为变量传入,我应该怎么编写这个程序呢",
|
||||||
|
"那古代人还没有发明方程的时候,他们是怎么解的呢"
|
||||||
|
],
|
||||||
|
"system_prompt": "You are a helpful assistant."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"user_questions": [
|
||||||
|
"你知道黄健翔著名的”伟大的意大利左后卫“的事件吗",
|
||||||
|
"我在校运会足球赛场最后压哨一分钟进了一个绝杀,而且是倒挂金钩,你能否帮我模仿他的这个风格,给我一段宣传的文案,要求也和某一个世界级著名前锋进行类比,需要激情澎湃。注意,我并不太喜欢梅西。"
|
||||||
|
],
|
||||||
|
"system_prompt": "You are a helpful assistant."
|
||||||
|
}
|
||||||
|
]
|
||||||
865
prefix_prefill.py
Normal file
865
prefix_prefill.py
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
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,
|
||||||
|
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, # head size
|
||||||
|
BLOCK_DMODEL_PADDED: tl.constexpr, # head size padded to a power of 2
|
||||||
|
BLOCK_N: tl.constexpr,
|
||||||
|
SLIDING_WINDOW: 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)
|
||||||
|
cur_batch_query_len = cur_batch_seq_len - cur_batch_ctx_len
|
||||||
|
|
||||||
|
# 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) * 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)
|
||||||
|
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) * v_scale).to(q.dtype)
|
||||||
|
else:
|
||||||
|
v = v_load
|
||||||
|
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 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)
|
||||||
|
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)
|
||||||
|
# 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,
|
||||||
|
B_Ctxlen,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
):
|
||||||
|
# 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_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_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) * k_scale).to(q.dtype)
|
||||||
|
else:
|
||||||
|
k = k_load
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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) * v_scale).to(q.dtype)
|
||||||
|
else:
|
||||||
|
v = v_load
|
||||||
|
p = p.to(v.dtype)
|
||||||
|
|
||||||
|
acc += tl.dot(p, v, allow_tf32=False)
|
||||||
|
# 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, allow_tf32=False)
|
||||||
|
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, allow_tf32=False)
|
||||||
|
# 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,
|
||||||
|
b_ctx_len,
|
||||||
|
max_input_len,
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
alibi_slopes=None,
|
||||||
|
sliding_window=None):
|
||||||
|
|
||||||
|
BLOCK = 128 if current_platform.has_device_capability(80) else 64
|
||||||
|
NUM_WARPS = 8
|
||||||
|
|
||||||
|
# need to reduce num. blocks when using fp32
|
||||||
|
# due to increased use of GPU shared memory
|
||||||
|
if q.dtype is torch.float32:
|
||||||
|
BLOCK = BLOCK // 2
|
||||||
|
|
||||||
|
# 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 = torch.float8_e4m3fn
|
||||||
|
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)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
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,
|
||||||
|
b_ctx_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,
|
||||||
|
BLOCK_M=BLOCK,
|
||||||
|
BLOCK_DMODEL=Lk,
|
||||||
|
BLOCK_DMODEL_PADDED=Lk_padded,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
num_warps=NUM_WARPS,
|
||||||
|
num_stages=1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
import time
|
||||||
|
ts_beg = time.time()
|
||||||
|
_fwd_kernel[grid](
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
k_cache,
|
||||||
|
v_cache,
|
||||||
|
b_loc,
|
||||||
|
sm_scale,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
b_start_loc,
|
||||||
|
b_seq_len,
|
||||||
|
b_ctx_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,
|
||||||
|
BLOCK_M=BLOCK,
|
||||||
|
BLOCK_DMODEL=Lk,
|
||||||
|
BLOCK_DMODEL_PADDED=Lk_padded,
|
||||||
|
BLOCK_N=BLOCK,
|
||||||
|
SLIDING_WINDOW=sliding_window,
|
||||||
|
num_warps=NUM_WARPS,
|
||||||
|
num_stages=1,
|
||||||
|
)
|
||||||
|
elapsed = time.time() - ts_beg
|
||||||
|
#print(f'{elapsed}: {BLOCK=}, {Lk=}, {Lk_padded=}, {BLOCK=}, {sliding_window=}, {NUM_WARPS=}')
|
||||||
|
return
|
||||||
595
qwen3_6_scripts/api_server.py
Normal file
595
qwen3_6_scripts/api_server.py
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import regex as re
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import tempfile
|
||||||
|
from argparse import Namespace
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from functools import partial
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import AsyncIterator, Set
|
||||||
|
|
||||||
|
import uvloop
|
||||||
|
from fastapi import APIRouter, FastAPI, Request
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||||
|
from starlette.datastructures import State
|
||||||
|
from starlette.routing import Mount
|
||||||
|
from typing_extensions import assert_never
|
||||||
|
|
||||||
|
import vllm.envs as envs
|
||||||
|
from vllm.config import ModelConfig
|
||||||
|
from vllm.engine.arg_utils import AsyncEngineArgs
|
||||||
|
from vllm.engine.async_llm_engine import AsyncLLMEngine
|
||||||
|
from vllm.engine.multiprocessing.client import MQLLMEngineClient
|
||||||
|
from vllm.engine.multiprocessing.engine import run_mp_engine
|
||||||
|
from vllm.engine.protocol import EngineClient
|
||||||
|
from vllm.entrypoints.launcher import serve_http
|
||||||
|
from vllm.entrypoints.logger import RequestLogger
|
||||||
|
from vllm.entrypoints.openai.cli_args import (make_arg_parser,
|
||||||
|
validate_parsed_serve_args)
|
||||||
|
# yapf conflicts with isort for this block
|
||||||
|
# yapf: disable
|
||||||
|
from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
|
||||||
|
ChatCompletionResponse,
|
||||||
|
CompletionRequest,
|
||||||
|
CompletionResponse,
|
||||||
|
DetokenizeRequest,
|
||||||
|
DetokenizeResponse,
|
||||||
|
EmbeddingRequest,
|
||||||
|
EmbeddingResponse, ErrorResponse,
|
||||||
|
LoadLoraAdapterRequest,
|
||||||
|
TokenizeRequest,
|
||||||
|
TokenizeResponse,
|
||||||
|
UnloadLoraAdapterRequest)
|
||||||
|
# yapf: enable
|
||||||
|
from vllm.entrypoints.openai.serving_chat import OpenAIServingChat
|
||||||
|
from vllm.entrypoints.openai.serving_completion import OpenAIServingCompletion
|
||||||
|
from vllm.entrypoints.openai.serving_embedding import OpenAIServingEmbedding
|
||||||
|
from vllm.entrypoints.openai.serving_engine import BaseModelPath
|
||||||
|
from vllm.entrypoints.openai.serving_tokenization import (
|
||||||
|
OpenAIServingTokenization)
|
||||||
|
from vllm.entrypoints.openai.tool_parsers import ToolParserManager
|
||||||
|
from vllm.reasoning import ReasoningParserManager
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.usage.usage_lib import UsageContext
|
||||||
|
from vllm.utils import FlexibleArgumentParser, get_open_zmq_ipc_path
|
||||||
|
from vllm.version import __version__ as VLLM_VERSION
|
||||||
|
|
||||||
|
TIMEOUT_KEEP_ALIVE = 5 # seconds
|
||||||
|
|
||||||
|
prometheus_multiproc_dir: tempfile.TemporaryDirectory
|
||||||
|
|
||||||
|
# Cannot use __name__ (https://github.com/vllm-project/vllm/pull/4765)
|
||||||
|
logger = init_logger('vllm.entrypoints.openai.api_server')
|
||||||
|
|
||||||
|
_running_tasks: Set[asyncio.Task] = set()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
try:
|
||||||
|
if app.state.log_stats:
|
||||||
|
engine_client: EngineClient = app.state.engine_client
|
||||||
|
|
||||||
|
async def _force_log():
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(10.)
|
||||||
|
await engine_client.do_log_stats()
|
||||||
|
|
||||||
|
task = asyncio.create_task(_force_log())
|
||||||
|
_running_tasks.add(task)
|
||||||
|
task.add_done_callback(_running_tasks.remove)
|
||||||
|
else:
|
||||||
|
task = None
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
if task is not None:
|
||||||
|
task.cancel()
|
||||||
|
finally:
|
||||||
|
# Ensure app state including engine ref is gc'd
|
||||||
|
del app.state
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def build_async_engine_client(
|
||||||
|
args: Namespace) -> AsyncIterator[EngineClient]:
|
||||||
|
|
||||||
|
# Context manager to handle engine_client lifecycle
|
||||||
|
# Ensures everything is shutdown and cleaned up on error/exit
|
||||||
|
engine_args = AsyncEngineArgs.from_cli_args(args)
|
||||||
|
|
||||||
|
async with build_async_engine_client_from_engine_args(
|
||||||
|
engine_args, args.disable_frontend_multiprocessing) as engine:
|
||||||
|
yield engine
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def build_async_engine_client_from_engine_args(
|
||||||
|
engine_args: AsyncEngineArgs,
|
||||||
|
disable_frontend_multiprocessing: bool = False,
|
||||||
|
) -> AsyncIterator[EngineClient]:
|
||||||
|
"""
|
||||||
|
Create EngineClient, either:
|
||||||
|
- in-process using the AsyncLLMEngine Directly
|
||||||
|
- multiprocess using AsyncLLMEngine RPC
|
||||||
|
|
||||||
|
Returns the Client or None if the creation failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Fall back
|
||||||
|
# TODO: fill out feature matrix.
|
||||||
|
if (MQLLMEngineClient.is_unsupported_config(engine_args)
|
||||||
|
or disable_frontend_multiprocessing):
|
||||||
|
engine_config = engine_args.create_engine_config()
|
||||||
|
uses_ray = getattr(AsyncLLMEngine._get_executor_cls(engine_config),
|
||||||
|
"uses_ray", False)
|
||||||
|
|
||||||
|
build_engine = partial(AsyncLLMEngine.from_engine_args,
|
||||||
|
engine_args=engine_args,
|
||||||
|
engine_config=engine_config,
|
||||||
|
usage_context=UsageContext.OPENAI_API_SERVER)
|
||||||
|
if uses_ray:
|
||||||
|
# Must run in main thread with ray for its signal handlers to work
|
||||||
|
engine_client = build_engine()
|
||||||
|
else:
|
||||||
|
engine_client = await asyncio.get_running_loop().run_in_executor(
|
||||||
|
None, build_engine)
|
||||||
|
|
||||||
|
yield engine_client
|
||||||
|
return
|
||||||
|
|
||||||
|
# Otherwise, use the multiprocessing AsyncLLMEngine.
|
||||||
|
else:
|
||||||
|
if "PROMETHEUS_MULTIPROC_DIR" not in os.environ:
|
||||||
|
# Make TemporaryDirectory for prometheus multiprocessing
|
||||||
|
# Note: global TemporaryDirectory will be automatically
|
||||||
|
# cleaned up upon exit.
|
||||||
|
global prometheus_multiproc_dir
|
||||||
|
prometheus_multiproc_dir = tempfile.TemporaryDirectory()
|
||||||
|
os.environ[
|
||||||
|
"PROMETHEUS_MULTIPROC_DIR"] = prometheus_multiproc_dir.name
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Found PROMETHEUS_MULTIPROC_DIR was set by user. "
|
||||||
|
"This directory must be wiped between vLLM runs or "
|
||||||
|
"you will find inaccurate metrics. Unset the variable "
|
||||||
|
"and vLLM will properly handle cleanup.")
|
||||||
|
|
||||||
|
# Select random path for IPC.
|
||||||
|
ipc_path = get_open_zmq_ipc_path()
|
||||||
|
logger.info("Multiprocessing frontend to use %s for IPC Path.",
|
||||||
|
ipc_path)
|
||||||
|
|
||||||
|
# Start RPCServer in separate process (holds the LLMEngine).
|
||||||
|
# the current process might have CUDA context,
|
||||||
|
# so we need to spawn a new process
|
||||||
|
context = multiprocessing.get_context("spawn")
|
||||||
|
|
||||||
|
engine_process = context.Process(target=run_mp_engine,
|
||||||
|
args=(engine_args,
|
||||||
|
UsageContext.OPENAI_API_SERVER,
|
||||||
|
ipc_path))
|
||||||
|
engine_process.start()
|
||||||
|
logger.info("Started engine process with PID %d", engine_process.pid)
|
||||||
|
|
||||||
|
# Build RPCClient, which conforms to EngineClient Protocol.
|
||||||
|
# NOTE: Actually, this is not true yet. We still need to support
|
||||||
|
# embedding models via RPC (see TODO above)
|
||||||
|
engine_config = engine_args.create_engine_config()
|
||||||
|
mp_engine_client = MQLLMEngineClient(ipc_path, engine_config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await mp_engine_client.setup()
|
||||||
|
break
|
||||||
|
except TimeoutError:
|
||||||
|
if not engine_process.is_alive():
|
||||||
|
raise RuntimeError(
|
||||||
|
"Engine process failed to start") from None
|
||||||
|
|
||||||
|
yield mp_engine_client # type: ignore[misc]
|
||||||
|
finally:
|
||||||
|
# Ensure rpc server process was terminated
|
||||||
|
engine_process.terminate()
|
||||||
|
|
||||||
|
# Close all open connections to the backend
|
||||||
|
mp_engine_client.close()
|
||||||
|
|
||||||
|
# Wait for engine process to join
|
||||||
|
engine_process.join(4)
|
||||||
|
if engine_process.exitcode is None:
|
||||||
|
# Kill if taking longer than 5 seconds to stop
|
||||||
|
engine_process.kill()
|
||||||
|
|
||||||
|
# Lazy import for prometheus multiprocessing.
|
||||||
|
# We need to set PROMETHEUS_MULTIPROC_DIR environment variable
|
||||||
|
# before prometheus_client is imported.
|
||||||
|
# See https://prometheus.github.io/client_python/multiprocess/
|
||||||
|
from prometheus_client import multiprocess
|
||||||
|
multiprocess.mark_process_dead(engine_process.pid)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
def mount_metrics(app: FastAPI):
|
||||||
|
# Lazy import for prometheus multiprocessing.
|
||||||
|
# We need to set PROMETHEUS_MULTIPROC_DIR environment variable
|
||||||
|
# before prometheus_client is imported.
|
||||||
|
# See https://prometheus.github.io/client_python/multiprocess/
|
||||||
|
from prometheus_client import (CollectorRegistry, make_asgi_app,
|
||||||
|
multiprocess)
|
||||||
|
|
||||||
|
prometheus_multiproc_dir_path = os.getenv("PROMETHEUS_MULTIPROC_DIR", None)
|
||||||
|
if prometheus_multiproc_dir_path is not None:
|
||||||
|
logger.info("vLLM to use %s as PROMETHEUS_MULTIPROC_DIR",
|
||||||
|
prometheus_multiproc_dir_path)
|
||||||
|
registry = CollectorRegistry()
|
||||||
|
multiprocess.MultiProcessCollector(registry)
|
||||||
|
|
||||||
|
# Add prometheus asgi middleware to route /metrics requests
|
||||||
|
metrics_route = Mount("/metrics", make_asgi_app(registry=registry))
|
||||||
|
else:
|
||||||
|
# Add prometheus asgi middleware to route /metrics requests
|
||||||
|
metrics_route = Mount("/metrics", make_asgi_app())
|
||||||
|
|
||||||
|
# Workaround for 307 Redirect for /metrics
|
||||||
|
metrics_route.path_regex = re.compile("^/metrics(?P<path>.*)$")
|
||||||
|
app.routes.append(metrics_route)
|
||||||
|
|
||||||
|
|
||||||
|
def chat(request: Request) -> OpenAIServingChat:
|
||||||
|
return request.app.state.openai_serving_chat
|
||||||
|
|
||||||
|
|
||||||
|
def completion(request: Request) -> OpenAIServingCompletion:
|
||||||
|
return request.app.state.openai_serving_completion
|
||||||
|
|
||||||
|
|
||||||
|
def tokenization(request: Request) -> OpenAIServingTokenization:
|
||||||
|
return request.app.state.openai_serving_tokenization
|
||||||
|
|
||||||
|
|
||||||
|
def embedding(request: Request) -> OpenAIServingEmbedding:
|
||||||
|
return request.app.state.openai_serving_embedding
|
||||||
|
|
||||||
|
|
||||||
|
def engine_client(request: Request) -> EngineClient:
|
||||||
|
return request.app.state.engine_client
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health(raw_request: Request) -> Response:
|
||||||
|
"""Health check."""
|
||||||
|
await engine_client(raw_request).check_health()
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tokenize")
|
||||||
|
async def tokenize(request: TokenizeRequest, raw_request: Request):
|
||||||
|
generator = await tokenization(raw_request).create_tokenize(request)
|
||||||
|
if isinstance(generator, ErrorResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
status_code=generator.code)
|
||||||
|
elif isinstance(generator, TokenizeResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump())
|
||||||
|
|
||||||
|
assert_never(generator)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/detokenize")
|
||||||
|
async def detokenize(request: DetokenizeRequest, raw_request: Request):
|
||||||
|
generator = await tokenization(raw_request).create_detokenize(request)
|
||||||
|
if isinstance(generator, ErrorResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
status_code=generator.code)
|
||||||
|
elif isinstance(generator, DetokenizeResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump())
|
||||||
|
|
||||||
|
assert_never(generator)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/v1/models")
|
||||||
|
async def show_available_models(raw_request: Request):
|
||||||
|
models = await completion(raw_request).show_available_models()
|
||||||
|
return JSONResponse(content=models.model_dump())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/version")
|
||||||
|
async def show_version():
|
||||||
|
ver = {"version": VLLM_VERSION}
|
||||||
|
return JSONResponse(content=ver)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/v1/chat/completions")
|
||||||
|
async def create_chat_completion(request: ChatCompletionRequest,
|
||||||
|
raw_request: Request):
|
||||||
|
|
||||||
|
generator = await chat(raw_request).create_chat_completion(
|
||||||
|
request, raw_request)
|
||||||
|
|
||||||
|
if isinstance(generator, ErrorResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
status_code=generator.code)
|
||||||
|
|
||||||
|
elif isinstance(generator, ChatCompletionResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump())
|
||||||
|
|
||||||
|
return StreamingResponse(content=generator, media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/v1/completions")
|
||||||
|
async def create_completion(request: CompletionRequest, raw_request: Request):
|
||||||
|
generator = await completion(raw_request).create_completion(
|
||||||
|
request, raw_request)
|
||||||
|
if isinstance(generator, ErrorResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
status_code=generator.code)
|
||||||
|
elif isinstance(generator, CompletionResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump())
|
||||||
|
|
||||||
|
return StreamingResponse(content=generator, media_type="text/event-stream")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/v1/embeddings")
|
||||||
|
async def create_embedding(request: EmbeddingRequest, raw_request: Request):
|
||||||
|
generator = await embedding(raw_request).create_embedding(
|
||||||
|
request, raw_request)
|
||||||
|
if isinstance(generator, ErrorResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump(),
|
||||||
|
status_code=generator.code)
|
||||||
|
elif isinstance(generator, EmbeddingResponse):
|
||||||
|
return JSONResponse(content=generator.model_dump())
|
||||||
|
|
||||||
|
assert_never(generator)
|
||||||
|
|
||||||
|
|
||||||
|
if envs.VLLM_TORCH_PROFILER_DIR:
|
||||||
|
logger.warning(
|
||||||
|
"Torch Profiler is enabled in the API server. This should ONLY be "
|
||||||
|
"used for local development!")
|
||||||
|
|
||||||
|
@router.post("/start_profile")
|
||||||
|
async def start_profile(raw_request: Request):
|
||||||
|
logger.info("Starting profiler...")
|
||||||
|
await engine_client(raw_request).start_profile()
|
||||||
|
logger.info("Profiler started.")
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
@router.post("/stop_profile")
|
||||||
|
async def stop_profile(raw_request: Request):
|
||||||
|
logger.info("Stopping profiler...")
|
||||||
|
await engine_client(raw_request).stop_profile()
|
||||||
|
logger.info("Profiler stopped.")
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
if envs.VLLM_ALLOW_RUNTIME_LORA_UPDATING:
|
||||||
|
logger.warning(
|
||||||
|
"Lora dynamic loading & unloading is enabled in the API server. "
|
||||||
|
"This should ONLY be used for local development!")
|
||||||
|
|
||||||
|
@router.post("/v1/load_lora_adapter")
|
||||||
|
async def load_lora_adapter(request: LoadLoraAdapterRequest,
|
||||||
|
raw_request: Request):
|
||||||
|
response = await chat(raw_request).load_lora_adapter(request)
|
||||||
|
if isinstance(response, ErrorResponse):
|
||||||
|
return JSONResponse(content=response.model_dump(),
|
||||||
|
status_code=response.code)
|
||||||
|
|
||||||
|
response = await completion(raw_request).load_lora_adapter(request)
|
||||||
|
if isinstance(response, ErrorResponse):
|
||||||
|
return JSONResponse(content=response.model_dump(),
|
||||||
|
status_code=response.code)
|
||||||
|
|
||||||
|
return Response(status_code=200, content=response)
|
||||||
|
|
||||||
|
@router.post("/v1/unload_lora_adapter")
|
||||||
|
async def unload_lora_adapter(request: UnloadLoraAdapterRequest,
|
||||||
|
raw_request: Request):
|
||||||
|
response = await chat(raw_request).unload_lora_adapter(request)
|
||||||
|
if isinstance(response, ErrorResponse):
|
||||||
|
return JSONResponse(content=response.model_dump(),
|
||||||
|
status_code=response.code)
|
||||||
|
|
||||||
|
response = await completion(raw_request).unload_lora_adapter(request)
|
||||||
|
if isinstance(response, ErrorResponse):
|
||||||
|
return JSONResponse(content=response.model_dump(),
|
||||||
|
status_code=response.code)
|
||||||
|
|
||||||
|
return Response(status_code=200, content=response)
|
||||||
|
|
||||||
|
|
||||||
|
def build_app(args: Namespace) -> FastAPI:
|
||||||
|
if args.disable_fastapi_docs:
|
||||||
|
app = FastAPI(openapi_url=None,
|
||||||
|
docs_url=None,
|
||||||
|
redoc_url=None,
|
||||||
|
lifespan=lifespan)
|
||||||
|
else:
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
app.include_router(router)
|
||||||
|
app.root_path = args.root_path
|
||||||
|
|
||||||
|
mount_metrics(app)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=args.allowed_origins,
|
||||||
|
allow_credentials=args.allow_credentials,
|
||||||
|
allow_methods=args.allowed_methods,
|
||||||
|
allow_headers=args.allowed_headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(_, exc):
|
||||||
|
chat = app.state.openai_serving_chat
|
||||||
|
err = chat.create_error_response(message=str(exc))
|
||||||
|
return JSONResponse(err.model_dump(),
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
if token := envs.VLLM_API_KEY or args.api_key:
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def authentication(request: Request, call_next):
|
||||||
|
root_path = "" if args.root_path is None else args.root_path
|
||||||
|
if request.method == "OPTIONS":
|
||||||
|
return await call_next(request)
|
||||||
|
if not request.url.path.startswith(f"{root_path}/v1"):
|
||||||
|
return await call_next(request)
|
||||||
|
if request.headers.get("Authorization") != "Bearer " + token:
|
||||||
|
return JSONResponse(content={"error": "Unauthorized"},
|
||||||
|
status_code=401)
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
for middleware in args.middleware:
|
||||||
|
module_path, object_name = middleware.rsplit(".", 1)
|
||||||
|
imported = getattr(importlib.import_module(module_path), object_name)
|
||||||
|
if inspect.isclass(imported):
|
||||||
|
app.add_middleware(imported)
|
||||||
|
elif inspect.iscoroutinefunction(imported):
|
||||||
|
app.middleware("http")(imported)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid middleware {middleware}. "
|
||||||
|
f"Must be a function or a class.")
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def init_app_state(
|
||||||
|
engine_client: EngineClient,
|
||||||
|
model_config: ModelConfig,
|
||||||
|
state: State,
|
||||||
|
args: Namespace,
|
||||||
|
) -> None:
|
||||||
|
if args.served_model_name is not None:
|
||||||
|
served_model_names = args.served_model_name
|
||||||
|
else:
|
||||||
|
served_model_names = [args.model]
|
||||||
|
|
||||||
|
if args.disable_log_requests:
|
||||||
|
request_logger = None
|
||||||
|
else:
|
||||||
|
request_logger = RequestLogger(max_log_len=args.max_log_len)
|
||||||
|
|
||||||
|
base_model_paths = [
|
||||||
|
BaseModelPath(name=name, model_path=args.model)
|
||||||
|
for name in served_model_names
|
||||||
|
]
|
||||||
|
|
||||||
|
state.engine_client = engine_client
|
||||||
|
state.log_stats = not args.disable_log_stats
|
||||||
|
|
||||||
|
state.openai_serving_chat = OpenAIServingChat(
|
||||||
|
engine_client,
|
||||||
|
model_config,
|
||||||
|
base_model_paths,
|
||||||
|
args.response_role,
|
||||||
|
lora_modules=args.lora_modules,
|
||||||
|
prompt_adapters=args.prompt_adapters,
|
||||||
|
request_logger=request_logger,
|
||||||
|
chat_template=args.chat_template,
|
||||||
|
return_tokens_as_token_ids=args.return_tokens_as_token_ids,
|
||||||
|
enable_auto_tools=args.enable_auto_tool_choice,
|
||||||
|
tool_parser=args.tool_call_parser,
|
||||||
|
reasoning_parser=getattr(args, 'reasoning_parser', None))
|
||||||
|
state.openai_serving_completion = OpenAIServingCompletion(
|
||||||
|
engine_client,
|
||||||
|
model_config,
|
||||||
|
base_model_paths,
|
||||||
|
lora_modules=args.lora_modules,
|
||||||
|
prompt_adapters=args.prompt_adapters,
|
||||||
|
request_logger=request_logger,
|
||||||
|
return_tokens_as_token_ids=args.return_tokens_as_token_ids,
|
||||||
|
)
|
||||||
|
state.openai_serving_embedding = OpenAIServingEmbedding(
|
||||||
|
engine_client,
|
||||||
|
model_config,
|
||||||
|
base_model_paths,
|
||||||
|
request_logger=request_logger,
|
||||||
|
)
|
||||||
|
state.openai_serving_tokenization = OpenAIServingTokenization(
|
||||||
|
engine_client,
|
||||||
|
model_config,
|
||||||
|
base_model_paths,
|
||||||
|
lora_modules=args.lora_modules,
|
||||||
|
request_logger=request_logger,
|
||||||
|
chat_template=args.chat_template,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_server(args, **uvicorn_kwargs) -> None:
|
||||||
|
logger.info("vLLM API server version %s", VLLM_VERSION)
|
||||||
|
logger.info("args: %s", args)
|
||||||
|
|
||||||
|
if args.tool_parser_plugin and len(args.tool_parser_plugin) > 3:
|
||||||
|
ToolParserManager.import_tool_parser(args.tool_parser_plugin)
|
||||||
|
|
||||||
|
valide_tool_parses = ToolParserManager.tool_parsers.keys()
|
||||||
|
if args.enable_auto_tool_choice \
|
||||||
|
and args.tool_call_parser not in valide_tool_parses:
|
||||||
|
raise KeyError(f"invalid tool call parser: {args.tool_call_parser} "
|
||||||
|
f"(chose from {{ {','.join(valide_tool_parses)} }})")
|
||||||
|
|
||||||
|
reasoning_parser = getattr(args, 'reasoning_parser', None)
|
||||||
|
if reasoning_parser:
|
||||||
|
valid_reasoning = ReasoningParserManager.list_registered()
|
||||||
|
if reasoning_parser not in valid_reasoning:
|
||||||
|
raise KeyError(
|
||||||
|
f"invalid reasoning parser: {reasoning_parser} "
|
||||||
|
f"(chose from {{ {','.join(valid_reasoning)} }})")
|
||||||
|
|
||||||
|
# workaround to make sure that we bind the port before the engine is set up.
|
||||||
|
# This avoids race conditions with ray.
|
||||||
|
# see https://github.com/vllm-project/vllm/issues/8204
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.bind(("", args.port))
|
||||||
|
|
||||||
|
def signal_handler(*_) -> None:
|
||||||
|
# Interrupt server on sigterm while initializing
|
||||||
|
raise KeyboardInterrupt("terminated")
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
async with build_async_engine_client(args) as engine_client:
|
||||||
|
app = build_app(args)
|
||||||
|
|
||||||
|
model_config = await engine_client.get_model_config()
|
||||||
|
init_app_state(engine_client, model_config, app.state, args)
|
||||||
|
|
||||||
|
shutdown_task = await serve_http(
|
||||||
|
app,
|
||||||
|
host=args.host,
|
||||||
|
port=args.port,
|
||||||
|
log_level=args.uvicorn_log_level,
|
||||||
|
timeout_keep_alive=TIMEOUT_KEEP_ALIVE,
|
||||||
|
ssl_keyfile=args.ssl_keyfile,
|
||||||
|
ssl_certfile=args.ssl_certfile,
|
||||||
|
ssl_ca_certs=args.ssl_ca_certs,
|
||||||
|
ssl_cert_reqs=args.ssl_cert_reqs,
|
||||||
|
fd=sock.fileno(),
|
||||||
|
**uvicorn_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# NB: Await server shutdown only after the backend context is exited
|
||||||
|
await shutdown_task
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# NOTE(simon):
|
||||||
|
# This section should be in sync with vllm/scripts.py for CLI entrypoints.
|
||||||
|
parser = FlexibleArgumentParser(
|
||||||
|
description="vLLM OpenAI-Compatible RESTful API server.")
|
||||||
|
parser = make_arg_parser(parser)
|
||||||
|
args = parser.parse_args()
|
||||||
|
validate_parsed_serve_args(args)
|
||||||
|
|
||||||
|
uvloop.run(run_server(args))
|
||||||
601
qwen3_6_scripts/chat_utils.py
Normal file
601
qwen3_6_scripts/chat_utils.py
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
import asyncio
|
||||||
|
import codecs
|
||||||
|
import json
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import defaultdict
|
||||||
|
from functools import lru_cache, partial
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import (Any, Awaitable, Dict, Generic, Iterable, List, Literal,
|
||||||
|
Mapping, Optional, Tuple, TypeVar, Union, cast)
|
||||||
|
|
||||||
|
# yapf conflicts with isort for this block
|
||||||
|
# yapf: disable
|
||||||
|
from openai.types.chat import (ChatCompletionAssistantMessageParam,
|
||||||
|
ChatCompletionContentPartImageParam)
|
||||||
|
from openai.types.chat import (
|
||||||
|
ChatCompletionContentPartParam as OpenAIChatCompletionContentPartParam)
|
||||||
|
from openai.types.chat import (ChatCompletionContentPartRefusalParam,
|
||||||
|
ChatCompletionContentPartTextParam)
|
||||||
|
from openai.types.chat import (
|
||||||
|
ChatCompletionMessageParam as OpenAIChatCompletionMessageParam)
|
||||||
|
from openai.types.chat import (ChatCompletionMessageToolCallParam,
|
||||||
|
ChatCompletionToolMessageParam)
|
||||||
|
# yapf: enable
|
||||||
|
# pydantic needs the TypedDict from typing_extensions
|
||||||
|
from pydantic import ConfigDict
|
||||||
|
from transformers import PreTrainedTokenizer, PreTrainedTokenizerFast
|
||||||
|
from typing_extensions import Required, TypeAlias, TypedDict
|
||||||
|
|
||||||
|
from vllm.config import ModelConfig
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.multimodal import MultiModalDataDict
|
||||||
|
from vllm.multimodal.utils import (async_get_and_parse_audio,
|
||||||
|
async_get_and_parse_image,
|
||||||
|
get_and_parse_audio, get_and_parse_image)
|
||||||
|
from vllm.transformers_utils.tokenizer import AnyTokenizer, MistralTokenizer
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AudioURL(TypedDict, total=False):
|
||||||
|
url: Required[str]
|
||||||
|
"""
|
||||||
|
Either a URL of the audio or a data URL with base64 encoded audio data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ChatCompletionContentPartAudioParam(TypedDict, total=False):
|
||||||
|
audio_url: Required[AudioURL]
|
||||||
|
|
||||||
|
type: Required[Literal["audio_url"]]
|
||||||
|
"""The type of the content part."""
|
||||||
|
|
||||||
|
|
||||||
|
class CustomChatCompletionContentPartParam(TypedDict, total=False):
|
||||||
|
__pydantic_config__ = ConfigDict(extra="allow") # type: ignore
|
||||||
|
|
||||||
|
type: Required[str]
|
||||||
|
"""The type of the content part."""
|
||||||
|
|
||||||
|
|
||||||
|
ChatCompletionContentPartParam: TypeAlias = Union[
|
||||||
|
OpenAIChatCompletionContentPartParam, ChatCompletionContentPartAudioParam,
|
||||||
|
ChatCompletionContentPartRefusalParam,
|
||||||
|
CustomChatCompletionContentPartParam]
|
||||||
|
|
||||||
|
|
||||||
|
class CustomChatCompletionMessageParam(TypedDict, total=False):
|
||||||
|
"""Enables custom roles in the Chat Completion API."""
|
||||||
|
role: Required[str]
|
||||||
|
"""The role of the message's author."""
|
||||||
|
|
||||||
|
content: Union[str, List[ChatCompletionContentPartParam]]
|
||||||
|
"""The contents of the message."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""An optional name for the participant.
|
||||||
|
|
||||||
|
Provides the model information to differentiate between participants of the
|
||||||
|
same role.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tool_call_id: Optional[str]
|
||||||
|
"""Tool call that this message is responding to."""
|
||||||
|
|
||||||
|
tool_calls: Optional[Iterable[ChatCompletionMessageToolCallParam]]
|
||||||
|
"""The tool calls generated by the model, such as function calls."""
|
||||||
|
|
||||||
|
reasoning_content: Optional[str]
|
||||||
|
"""Reasoning / thinking content for assistant messages (vLLM extension).
|
||||||
|
When present in a previous assistant turn, it is rendered as
|
||||||
|
<think>...</think> before the main content so the model sees its own
|
||||||
|
chain-of-thought in subsequent turns."""
|
||||||
|
|
||||||
|
|
||||||
|
ChatCompletionMessageParam = Union[OpenAIChatCompletionMessageParam,
|
||||||
|
CustomChatCompletionMessageParam]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Make fields ReadOnly once mypy supports it
|
||||||
|
class ConversationMessage(TypedDict, total=False):
|
||||||
|
role: Required[str]
|
||||||
|
"""The role of the message's author."""
|
||||||
|
|
||||||
|
content: Optional[str]
|
||||||
|
"""The contents of the message"""
|
||||||
|
|
||||||
|
tool_call_id: Optional[str]
|
||||||
|
"""Tool call that this message is responding to."""
|
||||||
|
|
||||||
|
name: Optional[str]
|
||||||
|
"""The name of the function to call"""
|
||||||
|
|
||||||
|
tool_calls: Optional[Iterable[ChatCompletionMessageToolCallParam]]
|
||||||
|
"""The tool calls generated by the model, such as function calls."""
|
||||||
|
|
||||||
|
reasoning_content: Optional[str]
|
||||||
|
"""Reasoning / thinking content for assistant messages.
|
||||||
|
Passed directly to the chat template (Qwen3 reads message.reasoning_content
|
||||||
|
natively) instead of being manually wrapped in <think>...</think>."""
|
||||||
|
|
||||||
|
|
||||||
|
ModalityStr = Literal["image", "audio", "video"]
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMultiModalItemTracker(ABC, Generic[_T]):
|
||||||
|
"""
|
||||||
|
Tracks multi-modal items in a given request and ensures that the number
|
||||||
|
of multi-modal items in a given request does not exceed the configured
|
||||||
|
maximum per prompt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_config: ModelConfig, tokenizer: AnyTokenizer):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._model_config = model_config
|
||||||
|
self._tokenizer = tokenizer
|
||||||
|
self._allowed_items = (model_config.multimodal_config.limit_per_prompt
|
||||||
|
if model_config.multimodal_config else {})
|
||||||
|
self._consumed_items = {k: 0 for k in self._allowed_items}
|
||||||
|
|
||||||
|
self._items: List[_T] = []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def _cached_token_str(tokenizer: AnyTokenizer, token_index: int) -> str:
|
||||||
|
return tokenizer.decode(token_index)
|
||||||
|
|
||||||
|
def _placeholder_str(self, modality: ModalityStr,
|
||||||
|
current_count: int) -> Optional[str]:
|
||||||
|
# TODO: Let user specify how to insert image tokens into prompt
|
||||||
|
# (similar to chat template)
|
||||||
|
hf_config = self._model_config.hf_config
|
||||||
|
model_type = hf_config.model_type
|
||||||
|
|
||||||
|
if modality == "image":
|
||||||
|
if model_type == "phi3_v":
|
||||||
|
# Workaround since this token is not defined in the tokenizer
|
||||||
|
return f"<|image_{current_count}|>"
|
||||||
|
if model_type == "minicpmv":
|
||||||
|
return "(<image>./</image>)"
|
||||||
|
if model_type in ("blip-2", "chatglm", "fuyu", "paligemma",
|
||||||
|
"pixtral"):
|
||||||
|
# These models do not use image tokens in the prompt
|
||||||
|
return None
|
||||||
|
if model_type == "qwen":
|
||||||
|
return f"Picture {current_count}: <img></img>"
|
||||||
|
if model_type.startswith("llava"):
|
||||||
|
return self._cached_token_str(self._tokenizer,
|
||||||
|
hf_config.image_token_index)
|
||||||
|
if model_type in ("chameleon", "internvl_chat", "NVLM_D"):
|
||||||
|
return "<image>"
|
||||||
|
if model_type == "mllama":
|
||||||
|
return "<|image|>"
|
||||||
|
if model_type in ("qwen2_vl","qwen2_5_vl"):
|
||||||
|
return "<|vision_start|><|image_pad|><|vision_end|>"
|
||||||
|
if model_type == "molmo":
|
||||||
|
return ""
|
||||||
|
|
||||||
|
raise TypeError(f"Unknown model type: {model_type}")
|
||||||
|
elif modality == "audio":
|
||||||
|
if model_type == "ultravox":
|
||||||
|
return "<|reserved_special_token_0|>"
|
||||||
|
raise TypeError(f"Unknown model type: {model_type}")
|
||||||
|
elif modality == "video":
|
||||||
|
if model_type in ("qwen2_vl","qwen2_5_vl"):
|
||||||
|
return "<|vision_start|><|video_pad|><|vision_end|>"
|
||||||
|
raise TypeError(f"Unknown model type: {model_type}")
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unknown modality: {modality}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _combine(items: List[MultiModalDataDict]) -> MultiModalDataDict:
|
||||||
|
mm_lists: Mapping[str, List[object]] = defaultdict(list)
|
||||||
|
|
||||||
|
# Merge all the multi-modal items
|
||||||
|
for single_mm_data in items:
|
||||||
|
for mm_key, mm_item in single_mm_data.items():
|
||||||
|
if isinstance(mm_item, list):
|
||||||
|
mm_lists[mm_key].extend(mm_item)
|
||||||
|
else:
|
||||||
|
mm_lists[mm_key].append(mm_item)
|
||||||
|
|
||||||
|
# Unpack any single item lists for models that don't expect multiple.
|
||||||
|
return {
|
||||||
|
mm_key: mm_list[0] if len(mm_list) == 1 else mm_list
|
||||||
|
for mm_key, mm_list in mm_lists.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
def add(self, modality: ModalityStr, item: _T) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Add a multi-modal item to the current prompt and returns the
|
||||||
|
placeholder string to use, if any.
|
||||||
|
"""
|
||||||
|
allowed_count = self._allowed_items.get(modality, 1)
|
||||||
|
current_count = self._consumed_items.get(modality, 0) + 1
|
||||||
|
if current_count > allowed_count:
|
||||||
|
raise ValueError(
|
||||||
|
f"At most {allowed_count} {modality}(s) may be provided in "
|
||||||
|
"one request.")
|
||||||
|
|
||||||
|
self._consumed_items[modality] = current_count
|
||||||
|
self._items.append(item)
|
||||||
|
|
||||||
|
return self._placeholder_str(modality, current_count)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_parser(self) -> "BaseMultiModalContentParser":
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MultiModalItemTracker(BaseMultiModalItemTracker[MultiModalDataDict]):
|
||||||
|
|
||||||
|
def all_mm_data(self) -> Optional[MultiModalDataDict]:
|
||||||
|
return self._combine(self._items) if self._items else None
|
||||||
|
|
||||||
|
def create_parser(self) -> "BaseMultiModalContentParser":
|
||||||
|
return MultiModalContentParser(self)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMultiModalItemTracker(
|
||||||
|
BaseMultiModalItemTracker[Awaitable[MultiModalDataDict]]):
|
||||||
|
|
||||||
|
async def all_mm_data(self) -> Optional[MultiModalDataDict]:
|
||||||
|
if self._items:
|
||||||
|
items = await asyncio.gather(*self._items)
|
||||||
|
return self._combine(items)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_parser(self) -> "BaseMultiModalContentParser":
|
||||||
|
return AsyncMultiModalContentParser(self)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMultiModalContentParser(ABC):
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# multimodal placeholder_string : count
|
||||||
|
self._placeholder_counts: Dict[str, int] = defaultdict(lambda: 0)
|
||||||
|
|
||||||
|
def _add_placeholder(self, placeholder: Optional[str]):
|
||||||
|
if placeholder:
|
||||||
|
self._placeholder_counts[placeholder] += 1
|
||||||
|
|
||||||
|
def mm_placeholder_counts(self) -> Dict[str, int]:
|
||||||
|
return dict(self._placeholder_counts)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_image(self, image_url: str) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def parse_audio(self, audio_url: str) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class MultiModalContentParser(BaseMultiModalContentParser):
|
||||||
|
|
||||||
|
def __init__(self, tracker: MultiModalItemTracker) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._tracker = tracker
|
||||||
|
|
||||||
|
def parse_image(self, image_url: str) -> None:
|
||||||
|
image = get_and_parse_image(image_url)
|
||||||
|
|
||||||
|
placeholder = self._tracker.add("image", image)
|
||||||
|
self._add_placeholder(placeholder)
|
||||||
|
|
||||||
|
def parse_audio(self, audio_url: str) -> None:
|
||||||
|
audio = get_and_parse_audio(audio_url)
|
||||||
|
|
||||||
|
placeholder = self._tracker.add("audio", audio)
|
||||||
|
self._add_placeholder(placeholder)
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMultiModalContentParser(BaseMultiModalContentParser):
|
||||||
|
|
||||||
|
def __init__(self, tracker: AsyncMultiModalItemTracker) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self._tracker = tracker
|
||||||
|
|
||||||
|
def parse_image(self, image_url: str) -> None:
|
||||||
|
image_coro = async_get_and_parse_image(image_url)
|
||||||
|
|
||||||
|
placeholder = self._tracker.add("image", image_coro)
|
||||||
|
self._add_placeholder(placeholder)
|
||||||
|
|
||||||
|
def parse_audio(self, audio_url: str) -> None:
|
||||||
|
audio_coro = async_get_and_parse_audio(audio_url)
|
||||||
|
|
||||||
|
placeholder = self._tracker.add("audio", audio_coro)
|
||||||
|
self._add_placeholder(placeholder)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_chat_template(chat_template: Optional[Union[Path, str]]):
|
||||||
|
"""Raises if the provided chat template appears invalid."""
|
||||||
|
if chat_template is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif isinstance(chat_template, Path) and not chat_template.exists():
|
||||||
|
raise FileNotFoundError(
|
||||||
|
"the supplied chat template path doesn't exist")
|
||||||
|
|
||||||
|
elif isinstance(chat_template, str):
|
||||||
|
JINJA_CHARS = "{}\n"
|
||||||
|
if not any(c in chat_template
|
||||||
|
for c in JINJA_CHARS) and not Path(chat_template).exists():
|
||||||
|
raise ValueError(
|
||||||
|
f"The supplied chat template string ({chat_template}) "
|
||||||
|
f"appears path-like, but doesn't exist!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
f"{type(chat_template)} is not a valid chat template type")
|
||||||
|
|
||||||
|
|
||||||
|
def load_chat_template(
|
||||||
|
chat_template: Optional[Union[Path, str]]) -> Optional[str]:
|
||||||
|
if chat_template is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(chat_template, "r") as f:
|
||||||
|
resolved_chat_template = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
if isinstance(chat_template, Path):
|
||||||
|
raise
|
||||||
|
|
||||||
|
JINJA_CHARS = "{}\n"
|
||||||
|
if not any(c in chat_template for c in JINJA_CHARS):
|
||||||
|
msg = (f"The supplied chat template ({chat_template}) "
|
||||||
|
f"looks like a file path, but it failed to be "
|
||||||
|
f"opened. Reason: {e}")
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
|
||||||
|
# If opening a file fails, set chat template to be args to
|
||||||
|
# ensure we decode so our escape are interpreted correctly
|
||||||
|
resolved_chat_template = codecs.decode(chat_template, "unicode_escape")
|
||||||
|
|
||||||
|
logger.info("Using supplied chat template:\n%s", resolved_chat_template)
|
||||||
|
return resolved_chat_template
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Let user specify how to insert multimodal tokens into prompt
|
||||||
|
# (similar to chat template)
|
||||||
|
def _get_full_multimodal_text_prompt(placeholder_counts: Dict[str, int],
|
||||||
|
text_prompt: str) -> str:
|
||||||
|
"""Combine multimodal prompts for a multimodal language model."""
|
||||||
|
|
||||||
|
# Look through the text prompt to check for missing placeholders
|
||||||
|
missing_placeholders: List[str] = []
|
||||||
|
for placeholder in placeholder_counts:
|
||||||
|
|
||||||
|
# For any existing placeholder in the text prompt, we leave it as is
|
||||||
|
placeholder_counts[placeholder] -= text_prompt.count(placeholder)
|
||||||
|
|
||||||
|
if placeholder_counts[placeholder] < 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"Found more '{placeholder}' placeholders in input prompt than "
|
||||||
|
"actual multimodal data items.")
|
||||||
|
|
||||||
|
missing_placeholders.extend([placeholder] *
|
||||||
|
placeholder_counts[placeholder])
|
||||||
|
|
||||||
|
# NOTE: For now we always add missing placeholders at the front of
|
||||||
|
# the prompt. This may change to be customizable in the future.
|
||||||
|
return "\n".join(missing_placeholders + [text_prompt])
|
||||||
|
|
||||||
|
|
||||||
|
# No need to validate using Pydantic again
|
||||||
|
_TextParser = partial(cast, ChatCompletionContentPartTextParam)
|
||||||
|
_ImageParser = partial(cast, ChatCompletionContentPartImageParam)
|
||||||
|
_AudioParser = partial(cast, ChatCompletionContentPartAudioParam)
|
||||||
|
_RefusalParser = partial(cast, ChatCompletionContentPartRefusalParam)
|
||||||
|
MODEL_KEEP_MULTI_MODAL_CONTENT = {'mllama'}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_chat_message_content_parts(
|
||||||
|
role: str,
|
||||||
|
parts: Iterable[ChatCompletionContentPartParam],
|
||||||
|
mm_tracker: BaseMultiModalItemTracker,
|
||||||
|
) -> List[ConversationMessage]:
|
||||||
|
texts: List[str] = []
|
||||||
|
|
||||||
|
mm_parser = mm_tracker.create_parser()
|
||||||
|
keep_multimodal_content = \
|
||||||
|
mm_tracker._model_config.hf_config.model_type in \
|
||||||
|
MODEL_KEEP_MULTI_MODAL_CONTENT
|
||||||
|
|
||||||
|
has_image = False
|
||||||
|
for part in parts:
|
||||||
|
part_type = part["type"]
|
||||||
|
if part_type == "text":
|
||||||
|
text = _TextParser(part)["text"]
|
||||||
|
texts.append(text)
|
||||||
|
elif part_type == "image_url":
|
||||||
|
image_url = _ImageParser(part)["image_url"]
|
||||||
|
|
||||||
|
if image_url.get("detail", "auto") != "auto":
|
||||||
|
logger.warning(
|
||||||
|
"'image_url.detail' is currently not supported and "
|
||||||
|
"will be ignored.")
|
||||||
|
|
||||||
|
mm_parser.parse_image(image_url["url"])
|
||||||
|
has_image = True
|
||||||
|
elif part_type == "audio_url":
|
||||||
|
audio_url = _AudioParser(part)["audio_url"]
|
||||||
|
|
||||||
|
mm_parser.parse_audio(audio_url["url"])
|
||||||
|
elif part_type == "refusal":
|
||||||
|
text = _RefusalParser(part)["refusal"]
|
||||||
|
texts.append(text)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"Unknown part type: {part_type}")
|
||||||
|
|
||||||
|
text_prompt = "\n".join(texts)
|
||||||
|
if keep_multimodal_content:
|
||||||
|
text_prompt = "\n".join(texts)
|
||||||
|
role_content = [{'type': 'text', 'text': text_prompt}]
|
||||||
|
|
||||||
|
if has_image:
|
||||||
|
role_content = [{'type': 'image'}] + role_content
|
||||||
|
return [ConversationMessage(role=role,
|
||||||
|
content=role_content)] # type: ignore
|
||||||
|
else:
|
||||||
|
mm_placeholder_counts = mm_parser.mm_placeholder_counts()
|
||||||
|
if mm_placeholder_counts:
|
||||||
|
text_prompt = _get_full_multimodal_text_prompt(
|
||||||
|
mm_placeholder_counts, text_prompt)
|
||||||
|
return [ConversationMessage(role=role, content=text_prompt)]
|
||||||
|
|
||||||
|
|
||||||
|
# No need to validate using Pydantic again
|
||||||
|
_AssistantParser = partial(cast, ChatCompletionAssistantMessageParam)
|
||||||
|
_ToolParser = partial(cast, ChatCompletionToolMessageParam)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_chat_message_content(
|
||||||
|
message: ChatCompletionMessageParam,
|
||||||
|
mm_tracker: BaseMultiModalItemTracker,
|
||||||
|
) -> List[ConversationMessage]:
|
||||||
|
role = message["role"]
|
||||||
|
content = message.get("content")
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
content = []
|
||||||
|
elif isinstance(content, str):
|
||||||
|
content = [
|
||||||
|
ChatCompletionContentPartTextParam(type="text", text=content)
|
||||||
|
]
|
||||||
|
|
||||||
|
result = _parse_chat_message_content_parts(
|
||||||
|
role,
|
||||||
|
content, # type: ignore
|
||||||
|
mm_tracker,
|
||||||
|
)
|
||||||
|
|
||||||
|
for result_msg in result:
|
||||||
|
if role == 'assistant':
|
||||||
|
parsed_msg = _AssistantParser(message)
|
||||||
|
|
||||||
|
if "tool_calls" in parsed_msg:
|
||||||
|
result_msg["tool_calls"] = list(parsed_msg["tool_calls"])
|
||||||
|
|
||||||
|
# Pass reasoning content as a dedicated field so the chat template
|
||||||
|
# can render it natively (Qwen3: message.reasoning_content branch).
|
||||||
|
# Accept both "reasoning" (new vllm) and "reasoning_content" (ours).
|
||||||
|
reasoning = (message.get("reasoning") # type: ignore[arg-type]
|
||||||
|
or message.get("reasoning_content")) # type: ignore[arg-type]
|
||||||
|
if reasoning and isinstance(reasoning, str):
|
||||||
|
result_msg["reasoning_content"] = reasoning
|
||||||
|
|
||||||
|
elif role == "tool":
|
||||||
|
parsed_msg = _ToolParser(message)
|
||||||
|
if "tool_call_id" in parsed_msg:
|
||||||
|
result_msg["tool_call_id"] = parsed_msg["tool_call_id"]
|
||||||
|
|
||||||
|
if "name" in message and isinstance(message["name"], str):
|
||||||
|
result_msg["name"] = message["name"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _postprocess_messages(messages: List[ConversationMessage]) -> None:
|
||||||
|
# per the Transformers docs & maintainers, tool call arguments in
|
||||||
|
# assistant-role messages with tool_calls need to be dicts not JSON str -
|
||||||
|
# this is how tool-use chat templates will expect them moving forwards
|
||||||
|
# so, for messages that have tool_calls, parse the string (which we get
|
||||||
|
# from openAI format) to dict
|
||||||
|
for message in messages:
|
||||||
|
if (message["role"] == "assistant" and "tool_calls" in message
|
||||||
|
and isinstance(message["tool_calls"], list)):
|
||||||
|
|
||||||
|
for item in message["tool_calls"]:
|
||||||
|
item["function"]["arguments"] = json.loads(
|
||||||
|
item["function"]["arguments"])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_chat_messages(
|
||||||
|
messages: List[ChatCompletionMessageParam],
|
||||||
|
model_config: ModelConfig,
|
||||||
|
tokenizer: AnyTokenizer,
|
||||||
|
) -> Tuple[List[ConversationMessage], Optional[MultiModalDataDict]]:
|
||||||
|
conversation: List[ConversationMessage] = []
|
||||||
|
mm_tracker = MultiModalItemTracker(model_config, tokenizer)
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
sub_messages = _parse_chat_message_content(msg, mm_tracker)
|
||||||
|
|
||||||
|
conversation.extend(sub_messages)
|
||||||
|
|
||||||
|
_postprocess_messages(conversation)
|
||||||
|
|
||||||
|
return conversation, mm_tracker.all_mm_data()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_chat_messages_futures(
|
||||||
|
messages: List[ChatCompletionMessageParam],
|
||||||
|
model_config: ModelConfig,
|
||||||
|
tokenizer: AnyTokenizer,
|
||||||
|
) -> Tuple[List[ConversationMessage], Awaitable[Optional[MultiModalDataDict]]]:
|
||||||
|
conversation: List[ConversationMessage] = []
|
||||||
|
mm_tracker = AsyncMultiModalItemTracker(model_config, tokenizer)
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
sub_messages = _parse_chat_message_content(msg, mm_tracker)
|
||||||
|
|
||||||
|
conversation.extend(sub_messages)
|
||||||
|
|
||||||
|
_postprocess_messages(conversation)
|
||||||
|
|
||||||
|
return conversation, mm_tracker.all_mm_data()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_hf_chat_template(
|
||||||
|
tokenizer: Union[PreTrainedTokenizer, PreTrainedTokenizerFast],
|
||||||
|
conversation: List[ConversationMessage],
|
||||||
|
chat_template: Optional[str],
|
||||||
|
*,
|
||||||
|
tokenize: bool = False, # Different from HF's default
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
if chat_template is None and tokenizer.chat_template is None:
|
||||||
|
raise ValueError(
|
||||||
|
"As of transformers v4.44, default chat template is no longer "
|
||||||
|
"allowed, so you must provide a chat template if the tokenizer "
|
||||||
|
"does not define one.")
|
||||||
|
|
||||||
|
return tokenizer.apply_chat_template(
|
||||||
|
conversation=conversation, # type: ignore[arg-type]
|
||||||
|
chat_template=chat_template,
|
||||||
|
tokenize=tokenize,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_mistral_chat_template(
|
||||||
|
tokenizer: MistralTokenizer,
|
||||||
|
messages: List[ChatCompletionMessageParam],
|
||||||
|
chat_template: Optional[str] = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> List[int]:
|
||||||
|
if chat_template is not None:
|
||||||
|
logger.warning(
|
||||||
|
"'chat_template' cannot be overridden for mistral tokenizer.")
|
||||||
|
if "add_generation_prompt" in kwargs:
|
||||||
|
logger.warning(
|
||||||
|
"'add_generation_prompt' is not supported for mistral tokenizer, "
|
||||||
|
"so it will be ignored.")
|
||||||
|
if "continue_final_message" in kwargs:
|
||||||
|
logger.warning(
|
||||||
|
"'continue_final_message' is not supported for mistral tokenizer, "
|
||||||
|
"so it will be ignored.")
|
||||||
|
|
||||||
|
return tokenizer.apply_chat_template(
|
||||||
|
messages=messages,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
261
qwen3_6_scripts/cli_args.py
Normal file
261
qwen3_6_scripts/cli_args.py
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
"""
|
||||||
|
This file contains the command line arguments for the vLLM's
|
||||||
|
OpenAI-compatible server. It is kept in a separate file for documentation
|
||||||
|
purposes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
from typing import List, Optional, Sequence, Union
|
||||||
|
|
||||||
|
from vllm.engine.arg_utils import AsyncEngineArgs, nullable_str
|
||||||
|
from vllm.entrypoints.chat_utils import validate_chat_template
|
||||||
|
from vllm.entrypoints.openai.serving_engine import (LoRAModulePath,
|
||||||
|
PromptAdapterPath)
|
||||||
|
from vllm.entrypoints.openai.tool_parsers import ToolParserManager
|
||||||
|
from vllm.utils import FlexibleArgumentParser
|
||||||
|
|
||||||
|
|
||||||
|
class LoRAParserAction(argparse.Action):
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
parser: argparse.ArgumentParser,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
values: Optional[Union[str, Sequence[str]]],
|
||||||
|
option_string: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if values is None:
|
||||||
|
values = []
|
||||||
|
if isinstance(values, str):
|
||||||
|
raise TypeError("Expected values to be a list")
|
||||||
|
|
||||||
|
lora_list: List[LoRAModulePath] = []
|
||||||
|
for item in values:
|
||||||
|
if item in [None, '']: # Skip if item is None or empty string
|
||||||
|
continue
|
||||||
|
if '=' in item and ',' not in item: # Old format: name=path
|
||||||
|
name, path = item.split('=')
|
||||||
|
lora_list.append(LoRAModulePath(name, path))
|
||||||
|
else: # Assume JSON format
|
||||||
|
try:
|
||||||
|
lora_dict = json.loads(item)
|
||||||
|
lora = LoRAModulePath(**lora_dict)
|
||||||
|
lora_list.append(lora)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
parser.error(
|
||||||
|
f"Invalid JSON format for --lora-modules: {item}")
|
||||||
|
except TypeError as e:
|
||||||
|
parser.error(
|
||||||
|
f"Invalid fields for --lora-modules: {item} - {str(e)}"
|
||||||
|
)
|
||||||
|
setattr(namespace, self.dest, lora_list)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptAdapterParserAction(argparse.Action):
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
parser: argparse.ArgumentParser,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
values: Optional[Union[str, Sequence[str]]],
|
||||||
|
option_string: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if values is None:
|
||||||
|
values = []
|
||||||
|
if isinstance(values, str):
|
||||||
|
raise TypeError("Expected values to be a list")
|
||||||
|
|
||||||
|
adapter_list: List[PromptAdapterPath] = []
|
||||||
|
for item in values:
|
||||||
|
name, path = item.split('=')
|
||||||
|
adapter_list.append(PromptAdapterPath(name, path))
|
||||||
|
setattr(namespace, self.dest, adapter_list)
|
||||||
|
|
||||||
|
|
||||||
|
def make_arg_parser(parser: FlexibleArgumentParser) -> FlexibleArgumentParser:
|
||||||
|
parser.add_argument("--host",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="host name")
|
||||||
|
parser.add_argument("--port", type=int, default=8000, help="port number")
|
||||||
|
parser.add_argument(
|
||||||
|
"--uvicorn-log-level",
|
||||||
|
type=str,
|
||||||
|
default="info",
|
||||||
|
choices=['debug', 'info', 'warning', 'error', 'critical', 'trace'],
|
||||||
|
help="log level for uvicorn")
|
||||||
|
parser.add_argument("--allow-credentials",
|
||||||
|
action="store_true",
|
||||||
|
help="allow credentials")
|
||||||
|
parser.add_argument("--allowed-origins",
|
||||||
|
type=json.loads,
|
||||||
|
default=["*"],
|
||||||
|
help="allowed origins")
|
||||||
|
parser.add_argument("--allowed-methods",
|
||||||
|
type=json.loads,
|
||||||
|
default=["*"],
|
||||||
|
help="allowed methods")
|
||||||
|
parser.add_argument("--allowed-headers",
|
||||||
|
type=json.loads,
|
||||||
|
default=["*"],
|
||||||
|
help="allowed headers")
|
||||||
|
parser.add_argument("--api-key",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="If provided, the server will require this key "
|
||||||
|
"to be presented in the header.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--lora-modules",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
nargs='+',
|
||||||
|
action=LoRAParserAction,
|
||||||
|
help="LoRA module configurations in either 'name=path' format"
|
||||||
|
"or JSON format. "
|
||||||
|
"Example (old format): 'name=path' "
|
||||||
|
"Example (new format): "
|
||||||
|
"'{\"name\": \"name\", \"local_path\": \"path\", "
|
||||||
|
"\"base_model_name\": \"id\"}'")
|
||||||
|
parser.add_argument(
|
||||||
|
"--prompt-adapters",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
nargs='+',
|
||||||
|
action=PromptAdapterParserAction,
|
||||||
|
help="Prompt adapter configurations in the format name=path. "
|
||||||
|
"Multiple adapters can be specified.")
|
||||||
|
parser.add_argument("--chat-template",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="The file path to the chat template, "
|
||||||
|
"or the template in single-line form "
|
||||||
|
"for the specified model")
|
||||||
|
parser.add_argument("--response-role",
|
||||||
|
type=nullable_str,
|
||||||
|
default="assistant",
|
||||||
|
help="The role name to return if "
|
||||||
|
"`request.add_generation_prompt=true`.")
|
||||||
|
parser.add_argument("--ssl-keyfile",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="The file path to the SSL key file")
|
||||||
|
parser.add_argument("--ssl-certfile",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="The file path to the SSL cert file")
|
||||||
|
parser.add_argument("--ssl-ca-certs",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="The CA certificates file")
|
||||||
|
parser.add_argument(
|
||||||
|
"--ssl-cert-reqs",
|
||||||
|
type=int,
|
||||||
|
default=int(ssl.CERT_NONE),
|
||||||
|
help="Whether client certificate is required (see stdlib ssl module's)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--root-path",
|
||||||
|
type=nullable_str,
|
||||||
|
default=None,
|
||||||
|
help="FastAPI root_path when app is behind a path based routing proxy")
|
||||||
|
parser.add_argument(
|
||||||
|
"--middleware",
|
||||||
|
type=nullable_str,
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Additional ASGI middleware to apply to the app. "
|
||||||
|
"We accept multiple --middleware arguments. "
|
||||||
|
"The value should be an import path. "
|
||||||
|
"If a function is provided, vLLM will add it to the server "
|
||||||
|
"using @app.middleware('http'). "
|
||||||
|
"If a class is provided, vLLM will add it to the server "
|
||||||
|
"using app.add_middleware(). ")
|
||||||
|
parser.add_argument(
|
||||||
|
"--return-tokens-as-token-ids",
|
||||||
|
action="store_true",
|
||||||
|
help="When --max-logprobs is specified, represents single tokens as "
|
||||||
|
"strings of the form 'token_id:{token_id}' so that tokens that "
|
||||||
|
"are not JSON-encodable can be identified.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-frontend-multiprocessing",
|
||||||
|
action="store_true",
|
||||||
|
help="If specified, will run the OpenAI frontend server in the same "
|
||||||
|
"process as the model serving engine.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--enable-auto-tool-choice",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=
|
||||||
|
"Enable auto tool choice for supported models. Use --tool-call-parser"
|
||||||
|
"to specify which parser to use")
|
||||||
|
|
||||||
|
valid_tool_parsers = ToolParserManager.tool_parsers.keys()
|
||||||
|
parser.add_argument(
|
||||||
|
"--tool-call-parser",
|
||||||
|
type=str,
|
||||||
|
metavar="{" + ",".join(valid_tool_parsers) + "} or name registered in "
|
||||||
|
"--tool-parser-plugin",
|
||||||
|
default=None,
|
||||||
|
help=
|
||||||
|
"Select the tool call parser depending on the model that you're using."
|
||||||
|
" This is used to parse the model-generated tool call into OpenAI API "
|
||||||
|
"format. Required for --enable-auto-tool-choice.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--tool-parser-plugin",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help=
|
||||||
|
"Special the tool parser plugin write to parse the model-generated tool"
|
||||||
|
" into OpenAI API format, the name register in this plugin can be used "
|
||||||
|
"in --tool-call-parser.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--reasoning-parser",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help=
|
||||||
|
"Select the reasoning parser to split <think>...</think> content into "
|
||||||
|
"reasoning_content vs content in the response. "
|
||||||
|
"Supported: qwen3")
|
||||||
|
|
||||||
|
parser = AsyncEngineArgs.add_cli_args(parser)
|
||||||
|
|
||||||
|
parser.add_argument('--max-log-len',
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help='Max number of prompt characters or prompt '
|
||||||
|
'ID numbers being printed in log.'
|
||||||
|
'\n\nDefault: Unlimited')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-fastapi-docs",
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help="Disable FastAPI's OpenAPI schema, Swagger UI, and ReDoc endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def validate_parsed_serve_args(args: argparse.Namespace):
|
||||||
|
"""Quick checks for model serve args that raise prior to loading."""
|
||||||
|
if hasattr(args, "subparser") and args.subparser != "serve":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure that the chat template is valid; raises if it likely isn't
|
||||||
|
validate_chat_template(args.chat_template)
|
||||||
|
|
||||||
|
# Enable auto tool needs a tool call parser to be valid
|
||||||
|
if args.enable_auto_tool_choice and not args.tool_call_parser:
|
||||||
|
raise TypeError("Error: --enable-auto-tool-choice requires "
|
||||||
|
"--tool-call-parser")
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser_for_docs() -> FlexibleArgumentParser:
|
||||||
|
parser_for_docs = FlexibleArgumentParser(
|
||||||
|
prog="-m vllm.entrypoints.openai.api_server")
|
||||||
|
return make_arg_parser(parser_for_docs)
|
||||||
224
qwen3_6_scripts/mamba_cache.py
Normal file
224
qwen3_6_scripts/mamba_cache.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import AttentionMetadata
|
||||||
|
|
||||||
|
|
||||||
|
class MambaCacheManager:
|
||||||
|
|
||||||
|
def __init__(self, dtype, num_mamba_layers, max_batch_size,
|
||||||
|
conv_state_shape, temporal_state_shape):
|
||||||
|
|
||||||
|
conv_state = torch.empty(size=(num_mamba_layers, max_batch_size) +
|
||||||
|
conv_state_shape,
|
||||||
|
dtype=dtype,
|
||||||
|
device="cuda")
|
||||||
|
temporal_state = torch.zeros(size=(num_mamba_layers, max_batch_size) +
|
||||||
|
temporal_state_shape,
|
||||||
|
dtype=dtype,
|
||||||
|
device="cuda")
|
||||||
|
|
||||||
|
self.mamba_cache = (conv_state, temporal_state)
|
||||||
|
|
||||||
|
# Maps between the request id and a dict that maps between the seq_id
|
||||||
|
# and its index inside the self.mamba_cache
|
||||||
|
self.mamba_cache_indices_mapping: Dict[str, Dict[int, int]] = {}
|
||||||
|
|
||||||
|
def current_run_tensors(self, input_ids: torch.Tensor,
|
||||||
|
attn_metadata: AttentionMetadata, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the tensors for the current run's conv and ssm state.
|
||||||
|
"""
|
||||||
|
if "seqlen_agnostic_capture_inputs" not in kwargs:
|
||||||
|
# We get here only on Prefill/Eager mode runs
|
||||||
|
request_ids_to_seq_ids = kwargs["request_ids_to_seq_ids"]
|
||||||
|
finished_requests_ids = kwargs["finished_requests_ids"]
|
||||||
|
|
||||||
|
self._release_finished_requests(finished_requests_ids)
|
||||||
|
mamba_cache_tensors = self._prepare_current_run_mamba_cache(
|
||||||
|
request_ids_to_seq_ids, finished_requests_ids)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# CUDA graph capturing runs
|
||||||
|
mamba_cache_tensors = kwargs["seqlen_agnostic_capture_inputs"]
|
||||||
|
|
||||||
|
return mamba_cache_tensors
|
||||||
|
|
||||||
|
def copy_inputs_before_cuda_graphs(self, input_buffers, **kwargs):
|
||||||
|
"""
|
||||||
|
Copy the relevant Mamba cache into the CUDA graph input buffer
|
||||||
|
that was provided during the capture runs
|
||||||
|
(JambaForCausalLM.mamba_gc_cache_buffer).
|
||||||
|
"""
|
||||||
|
assert all(
|
||||||
|
key in kwargs
|
||||||
|
for key in ["request_ids_to_seq_ids", "finished_requests_ids"])
|
||||||
|
finished_requests_ids = kwargs["finished_requests_ids"]
|
||||||
|
request_ids_to_seq_ids = kwargs["request_ids_to_seq_ids"]
|
||||||
|
|
||||||
|
self._release_finished_requests(finished_requests_ids)
|
||||||
|
self._prepare_current_run_mamba_cache(request_ids_to_seq_ids,
|
||||||
|
finished_requests_ids)
|
||||||
|
|
||||||
|
def get_seqlen_agnostic_capture_inputs(self, batch_size: int):
|
||||||
|
"""
|
||||||
|
Provide the CUDA graph capture runs with a buffer in adjusted size.
|
||||||
|
The buffer is used to maintain the Mamba Cache during the CUDA graph
|
||||||
|
replay runs.
|
||||||
|
"""
|
||||||
|
return tuple(buffer[:, :batch_size] for buffer in self.mamba_cache)
|
||||||
|
|
||||||
|
def _swap_mamba_cache(self, from_index: int, to_index: int):
|
||||||
|
assert len(self.mamba_cache) > 0
|
||||||
|
for cache_t in self.mamba_cache:
|
||||||
|
cache_t[:, [to_index,from_index]] = \
|
||||||
|
cache_t[:, [from_index,to_index]]
|
||||||
|
|
||||||
|
def _copy_mamba_cache(self, from_index: int, to_index: int):
|
||||||
|
assert len(self.mamba_cache) > 0
|
||||||
|
for cache_t in self.mamba_cache:
|
||||||
|
cache_t[:, to_index].copy_(cache_t[:, from_index],
|
||||||
|
non_blocking=True)
|
||||||
|
|
||||||
|
def _move_out_if_already_occupied(self, index: int,
|
||||||
|
all_occupied_indices: List[int]):
|
||||||
|
if index in all_occupied_indices:
|
||||||
|
first_free_index = self._first_free_index_in_mamba_cache()
|
||||||
|
# In case occupied, move the occupied to a new empty block
|
||||||
|
self._move_cache_index_and_mappings(from_index=index,
|
||||||
|
to_index=first_free_index)
|
||||||
|
|
||||||
|
def _assign_seq_id_to_mamba_cache_in_specific_dest(self, cur_rid: str,
|
||||||
|
seq_id: int,
|
||||||
|
destination_index: int):
|
||||||
|
"""
|
||||||
|
Assign (req_id,seq_id) pair to a `destination_index` index, if
|
||||||
|
already occupied, move the occupying index to a free index.
|
||||||
|
"""
|
||||||
|
all_occupied_indices = self._get_all_occupied_indices()
|
||||||
|
if cur_rid not in self.mamba_cache_indices_mapping:
|
||||||
|
self._move_out_if_already_occupied(
|
||||||
|
index=destination_index,
|
||||||
|
all_occupied_indices=all_occupied_indices)
|
||||||
|
for cache_t in self.mamba_cache:
|
||||||
|
cache_t[:, destination_index].zero_()
|
||||||
|
self.mamba_cache_indices_mapping[cur_rid] = {
|
||||||
|
seq_id: destination_index
|
||||||
|
}
|
||||||
|
elif seq_id not in (seq_ids2indices :=
|
||||||
|
self.mamba_cache_indices_mapping[cur_rid]):
|
||||||
|
# parallel sampling , where n > 1, assume prefill have
|
||||||
|
# already happened now we only need to copy the already
|
||||||
|
# existing cache into the siblings seq_ids caches
|
||||||
|
self._move_out_if_already_occupied(
|
||||||
|
index=destination_index,
|
||||||
|
all_occupied_indices=all_occupied_indices)
|
||||||
|
index_exists = list(seq_ids2indices.values())[0]
|
||||||
|
# case of decoding n>1, copy prefill cache to decoding indices
|
||||||
|
self._copy_mamba_cache(from_index=index_exists,
|
||||||
|
to_index=destination_index)
|
||||||
|
self.mamba_cache_indices_mapping[cur_rid][
|
||||||
|
seq_id] = destination_index
|
||||||
|
else:
|
||||||
|
# already exists
|
||||||
|
cache_index_already_exists = self.mamba_cache_indices_mapping[
|
||||||
|
cur_rid][seq_id]
|
||||||
|
if cache_index_already_exists != destination_index:
|
||||||
|
# In case the seq id already exists but not in
|
||||||
|
# the right destination, swap it with what's occupying it
|
||||||
|
self._swap_pair_indices_and_mappings(
|
||||||
|
from_index=cache_index_already_exists,
|
||||||
|
to_index=destination_index)
|
||||||
|
|
||||||
|
def _prepare_current_run_mamba_cache(
|
||||||
|
self, request_ids_to_seq_ids: Dict[str, list[int]],
|
||||||
|
finished_requests_ids: List[str]):
|
||||||
|
running_indices = []
|
||||||
|
request_ids_to_seq_ids_flatten = [
|
||||||
|
(req_id, seq_id)
|
||||||
|
for req_id, seq_ids in request_ids_to_seq_ids.items()
|
||||||
|
for seq_id in seq_ids
|
||||||
|
]
|
||||||
|
batch_size = len(request_ids_to_seq_ids_flatten)
|
||||||
|
for dest_index, (request_id,
|
||||||
|
seq_id) in enumerate(request_ids_to_seq_ids_flatten):
|
||||||
|
if request_id in finished_requests_ids:
|
||||||
|
# Do not allocate cache index for requests that run
|
||||||
|
# and finish right after
|
||||||
|
continue
|
||||||
|
self._assign_seq_id_to_mamba_cache_in_specific_dest(
|
||||||
|
request_id, seq_id, dest_index)
|
||||||
|
running_indices.append(dest_index)
|
||||||
|
|
||||||
|
self._clean_up_first_bs_blocks(batch_size, running_indices)
|
||||||
|
conv_state = self.mamba_cache[0][:, :batch_size]
|
||||||
|
temporal_state = self.mamba_cache[1][:, :batch_size]
|
||||||
|
|
||||||
|
return (conv_state, temporal_state)
|
||||||
|
|
||||||
|
def _get_all_occupied_indices(self):
|
||||||
|
return [
|
||||||
|
cache_idx
|
||||||
|
for seq_ids2indices in self.mamba_cache_indices_mapping.values()
|
||||||
|
for cache_idx in seq_ids2indices.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
def _clean_up_first_bs_blocks(self, batch_size: int,
|
||||||
|
indices_for_current_run: List[int]):
|
||||||
|
# move out all of the occupied but currently not running blocks
|
||||||
|
# outside of the first n blocks
|
||||||
|
destination_indices = range(batch_size)
|
||||||
|
max_possible_batch_size = self.mamba_cache[0].shape[1]
|
||||||
|
for destination_index in destination_indices:
|
||||||
|
if destination_index in self._get_all_occupied_indices() and \
|
||||||
|
destination_index not in indices_for_current_run:
|
||||||
|
# move not running indices outside of the batch
|
||||||
|
all_other_indices = list(
|
||||||
|
range(batch_size, max_possible_batch_size))
|
||||||
|
first_avail_index = self._first_free_index_in_mamba_cache(
|
||||||
|
all_other_indices)
|
||||||
|
self._swap_indices(from_index=destination_index,
|
||||||
|
to_index=first_avail_index)
|
||||||
|
|
||||||
|
def _move_cache_index_and_mappings(self, from_index: int, to_index: int):
|
||||||
|
self._copy_mamba_cache(from_index=from_index, to_index=to_index)
|
||||||
|
self._update_mapping_index(from_index=from_index, to_index=to_index)
|
||||||
|
|
||||||
|
def _swap_pair_indices_and_mappings(self, from_index: int, to_index: int):
|
||||||
|
self._swap_mamba_cache(from_index=from_index, to_index=to_index)
|
||||||
|
self._swap_mapping_index(from_index=from_index, to_index=to_index)
|
||||||
|
|
||||||
|
def _swap_mapping_index(self, from_index: int, to_index: int):
|
||||||
|
for seq_ids2index in self.mamba_cache_indices_mapping.values():
|
||||||
|
for seq_id, index in seq_ids2index.items():
|
||||||
|
if from_index == index:
|
||||||
|
seq_ids2index.update({seq_id: to_index})
|
||||||
|
elif to_index == index:
|
||||||
|
seq_ids2index.update({seq_id: from_index})
|
||||||
|
|
||||||
|
def _update_mapping_index(self, from_index: int, to_index: int):
|
||||||
|
for seq_ids2index in self.mamba_cache_indices_mapping.values():
|
||||||
|
for seq_id, index in seq_ids2index.items():
|
||||||
|
if from_index == index:
|
||||||
|
seq_ids2index.update({seq_id: to_index})
|
||||||
|
return
|
||||||
|
|
||||||
|
def _release_finished_requests(self,
|
||||||
|
finished_seq_groups_req_ids: List[str]):
|
||||||
|
for req_id in finished_seq_groups_req_ids:
|
||||||
|
if req_id in self.mamba_cache_indices_mapping:
|
||||||
|
self.mamba_cache_indices_mapping.pop(req_id)
|
||||||
|
|
||||||
|
def _first_free_index_in_mamba_cache(
|
||||||
|
self, indices_range: Optional[List[int]] = None) -> int:
|
||||||
|
assert self.mamba_cache is not None
|
||||||
|
if indices_range is None:
|
||||||
|
max_possible_batch_size = self.mamba_cache[0].shape[1]
|
||||||
|
indices_range = list(range(max_possible_batch_size))
|
||||||
|
all_occupied_indices = self._get_all_occupied_indices()
|
||||||
|
for i in indices_range:
|
||||||
|
if i not in all_occupied_indices:
|
||||||
|
return i
|
||||||
|
raise Exception("Couldn't find a free spot in the mamba cache! This"
|
||||||
|
"should never happen")
|
||||||
78
qwen3_6_scripts/patch_model_runner.py
Normal file
78
qwen3_6_scripts/patch_model_runner.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
Fix: prefix_cache_hit stays True for chunked-prefill chunk 2+ even when past cache.
|
||||||
|
|
||||||
|
Root cause:
|
||||||
|
model_runner.py _compute_for_prefix_cache_hit has three cases:
|
||||||
|
Case 1: prefix_cache_len <= context_len → "already past cache, do normal"
|
||||||
|
Case 2: context_len < prefix_cache_len < seq_len → partial hit, correct
|
||||||
|
Case 3: seq_len <= prefix_cache_len → full hit, reduce to 1 token
|
||||||
|
|
||||||
|
Case 1 does nothing (leaves prefix_cache_hit = True). Then in utils.py:
|
||||||
|
if inter_data.prefix_cache_hit:
|
||||||
|
block_table = computed_block_nums ← ONLY the original prefix blocks!
|
||||||
|
|
||||||
|
But context_len > prefix_cache_len means chunk 1 tokens (between prefix_cache_len
|
||||||
|
and context_len) are ALSO in KV cache and need to be in block_table.
|
||||||
|
block_table = computed_block_nums misses all chunk-1 blocks.
|
||||||
|
|
||||||
|
In _forward_prefix_pytorch:
|
||||||
|
num_ctx_blocks = ceil(context_len / block_size) # e.g. 268
|
||||||
|
block_tables.shape[1] = len(computed_block_nums) # e.g. 12 <-- too small!
|
||||||
|
At tile_blk >= 12: blk_ids is empty → k_t shape [..., 0] → amax crash.
|
||||||
|
|
||||||
|
Fix:
|
||||||
|
Set prefix_cache_hit = False for Case 1, so utils.py falls through to:
|
||||||
|
elif chunked_prefill_enabled:
|
||||||
|
block_table = block_tables[seq_id] ← full block table (prefix + chunk1)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
CANDIDATE_PATHS = [
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/vllm/worker/model_runner.py",
|
||||||
|
"/usr/local/corex/lib/python3/dist-packages/vllm/worker/model_runner.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
OLD_BLOCK = """\
|
||||||
|
if prefix_cache_len <= context_len:
|
||||||
|
# We already passed the cache hit region,
|
||||||
|
# so do normal computation.
|
||||||
|
pass"""
|
||||||
|
|
||||||
|
NEW_BLOCK = """\
|
||||||
|
if prefix_cache_len <= context_len:
|
||||||
|
# We already passed the cache hit region,
|
||||||
|
# so do normal computation.
|
||||||
|
# Must clear prefix_cache_hit so _add_seq_group uses the full
|
||||||
|
# block_tables (prefix + previous-chunk blocks) instead of only
|
||||||
|
# computed_block_nums (prefix only). Without this, block_tables
|
||||||
|
# passed to _forward_prefix_pytorch is too narrow for context_len,
|
||||||
|
# causing an empty blk_ids slice and a zero-dim amax() crash.
|
||||||
|
inter_data.prefix_cache_hit = False"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
for path in CANDIDATE_PATHS:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
continue
|
||||||
|
with open(path, "r") as f:
|
||||||
|
src = f.read()
|
||||||
|
if OLD_BLOCK not in src:
|
||||||
|
if NEW_BLOCK in src:
|
||||||
|
print(f"[patch_model_runner] already patched: {path}")
|
||||||
|
patched = True
|
||||||
|
break
|
||||||
|
print(f"[patch_model_runner] WARNING: expected block not found in {path}, skipping")
|
||||||
|
continue
|
||||||
|
patched_src = src.replace(OLD_BLOCK, NEW_BLOCK, 1)
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(patched_src)
|
||||||
|
print(f"[patch_model_runner] patched Case-1 prefix_cache_hit fix in: {path}")
|
||||||
|
patched = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not patched:
|
||||||
|
print("[patch_model_runner] ERROR: could not find model_runner.py at any known path", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
94
qwen3_6_scripts/patch_ops.sh
Executable file
94
qwen3_6_scripts/patch_ops.sh
Executable file
@@ -0,0 +1,94 @@
|
|||||||
|
# BI-V100 patch script for Qwen3.6-27B (Qwen3_5 architecture)
|
||||||
|
#
|
||||||
|
# Triton situation on BI-V100:
|
||||||
|
# - Standard Triton 2.3.1 is already present in the image.
|
||||||
|
# - HAS_TRITON = False (hardcoded in vendor vllm), but Triton is still used
|
||||||
|
# for TP-mode cache management (custom_cache_manager / libentry).
|
||||||
|
# - The vendor's triton_utils/__init__.py, custom_cache_manager.py, libentry.py
|
||||||
|
# are already correct for standard Triton 2.3.1 — do NOT overwrite them.
|
||||||
|
# - DO NOT install BI-V150 corex Triton 2.1.0 (pkgs/triton): that causes
|
||||||
|
# GPU hang on BI-V100 because the Triton CUDA PTX kernels are incompatible.
|
||||||
|
|
||||||
|
# Recommended server start command for TP=4 support 100K, need chunked prefill
|
||||||
|
# CUDA_VISIBLE_DEVICES="4,5,6,7" VLLM_ENGINE_ITERATION_TIMEOUT_S=3600 python3 -m vllm.entrypoints.openai.api_server \
|
||||||
|
# --model /workspace/models/Qwen3.6-27B --port 1111 --served-model-name llm \
|
||||||
|
# --max-model-len 100000 --enforce-eager --trust-remote-code -tp 4 --gpu-memory-utilization 0.95 \
|
||||||
|
# --max-num-seqs 1 --disable-log-requests --disable-frontend-multiprocessing \
|
||||||
|
# --max-num-batched-tokens 4096 --enable-chunked-prefill
|
||||||
|
#
|
||||||
|
# With prefix caching (GDN align-mode, requires chunked prefill):
|
||||||
|
# CUDA_VISIBLE_DEVICES="4,5,6,7" VLLM_ENGINE_ITERATION_TIMEOUT_S=3600 python3 -m vllm.entrypoints.openai.api_server \
|
||||||
|
# --model /workspace/models/Qwen3.6-35B-A3B --port 1111 --served-model-name llm \
|
||||||
|
# --max-model-len 150000 --trust-remote-code -tp 4 --gpu-memory-utilization 0.90 \
|
||||||
|
# --max-num-seqs 1 --disable-log-requests --disable-frontend-multiprocessing \
|
||||||
|
# --max-num-batched-tokens 8192 --enable-chunked-prefill --enable-prefix-caching \
|
||||||
|
# --max-seq-len-to-capture 32768
|
||||||
|
|
||||||
|
# --- paged_attn.py: replace forward_prefix with pure-PyTorch fallback -------
|
||||||
|
# The Triton context_attention_fwd kernel hangs BI-V100 GPUs permanently
|
||||||
|
# (standard Triton 2.3.1 PTX is not supported by the corex runtime either).
|
||||||
|
# Our paged_attn.py bypasses it entirely via _forward_prefix_pytorch, which
|
||||||
|
# utilizes K-tiling techniques, and also have _forward_decode_pytorch to bypass kernel
|
||||||
|
# when context length is high
|
||||||
|
cp ./paged_attn.py /usr/local/corex/lib/python3/dist-packages/vllm/attention/ops/paged_attn.py
|
||||||
|
|
||||||
|
# --- model_runner.py: fix prefix_cache_hit stays True in chunked-prefill chunk 2+ ---
|
||||||
|
# Bug: _compute_for_prefix_cache_hit Case 1 (prefix_cache_len <= context_len)
|
||||||
|
# leaves prefix_cache_hit=True. Then _add_seq_group uses block_table=computed_block_nums
|
||||||
|
# (only the original prefix blocks), ignoring chunk-1 KV cache blocks.
|
||||||
|
# _forward_prefix_pytorch then gets an undersized block_tables and crashes with
|
||||||
|
# "amax(): Expected reduction dim -1 to have non-zero size" on the 2nd tile.
|
||||||
|
# Fix: set prefix_cache_hit=False for Case 1 so the full block_tables is used.
|
||||||
|
python3 ./patch_model_runner.py
|
||||||
|
|
||||||
|
# --- transformers: Qwen3_5 tokenizer / model files --------------------------
|
||||||
|
pip install transformers==4.55.3 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
cp -r ./qwen3_5 /usr/local/lib/python3.10/site-packages/transformers/models/
|
||||||
|
cp -r ./qwen3_5_moe /usr/local/lib/python3.10/site-packages/transformers/models/
|
||||||
|
python3 ./patch_transformers_qwen3_5.py
|
||||||
|
|
||||||
|
# --- vllm model: Qwen3.6-27B (Qwen3_5 arch) --------------------------------
|
||||||
|
cp ./mamba_cache.py /usr/local/corex/lib/python3/dist-packages/vllm/model_executor/models/
|
||||||
|
cp ./qwen3_5.py /usr/local/corex/lib/python3/dist-packages/vllm/model_executor/models/qwen3_5.py
|
||||||
|
python3 ./patch_vllm_qwen3_5.py
|
||||||
|
|
||||||
|
# --- sequence.py: fix completion_tokens inflation under chunked prefill ------
|
||||||
|
# Bug: get_output_token_ids_to_return(delta=True) with num_new_tokens=0
|
||||||
|
# returns _cached_all_token_ids[-0:] == [0:] (the ENTIRE prompt+output list).
|
||||||
|
# Each prefill chunk step adds prompt_len to previous_num_tokens, so a 10K
|
||||||
|
# prompt processed in 3 chunks inflates completion_tokens by ~30K.
|
||||||
|
# Also adds num_cached_tokens field to RequestMetrics for prefix-cache stats.
|
||||||
|
cp ./sequence.py /usr/local/corex/lib/python3/dist-packages/vllm/sequence.py
|
||||||
|
|
||||||
|
# --- scheduler.py: record num_cached_tokens in RequestMetrics ----------------
|
||||||
|
# Sets seq_group.metrics.num_cached_tokens = prefix_cache_len on first prefill
|
||||||
|
# when --enable-prefix-caching is active, so serving_chat.py can report it in
|
||||||
|
# usage.prompt_tokens_details.cached_tokens (OpenAI-compatible API response).
|
||||||
|
cp ./scheduler.py /usr/local/corex/lib/python3/dist-packages/vllm/core/scheduler.py
|
||||||
|
|
||||||
|
# --- xformers: bypass cudnnFlashAttnForward (head_dim=256 > 128 limit) ------
|
||||||
|
# Injects _run_sdpa_fallback (pure matmul+softmax) into xformers.py.
|
||||||
|
# Required because head_dim=256 > 128 and ixformer flash attention either
|
||||||
|
# crashes (is_causal=True) or produces wrong output (attn_mask path).
|
||||||
|
# The fallback uses query_start_loc to derive actual query lengths, so it
|
||||||
|
# works correctly during profiling runs with chunked-prefill-style batches.
|
||||||
|
# also bypasses auto chunked prefill on
|
||||||
|
python3 ./patch_xformers_sdpa_seq.py
|
||||||
|
|
||||||
|
# --- tool parser: Qwen3 XML tool call format ---------------------------------
|
||||||
|
# Registers "qwen3_coder" parser for Qwen3.6 XML-style tool calls:
|
||||||
|
# <tool_call><function=name><parameter=key>\nvalue\n</parameter></function></tool_call>
|
||||||
|
# Use at server start: --tool-call-parser qwen3_coder --enable-auto-tool-choice
|
||||||
|
cp ./qwen3coder_tool_parser.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/tool_parsers/
|
||||||
|
python3 ./patch_vllm_tool_parser.py
|
||||||
|
|
||||||
|
# --- reasoning parser: Qwen3 <think>...</think> split ------------------------
|
||||||
|
# Adds --reasoning-parser qwen3 support.
|
||||||
|
# Routes thinking tokens to reasoning_content, rest to content in the delta.
|
||||||
|
# Works together with --tool-call-parser qwen3_coder (think → tool call flow).
|
||||||
|
cp -r ./reasoning /usr/local/corex/lib/python3/dist-packages/vllm/
|
||||||
|
cp ./protocol.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/protocol.py
|
||||||
|
cp ./cli_args.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/cli_args.py
|
||||||
|
cp ./serving_chat.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/serving_chat.py
|
||||||
|
cp ./api_server.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/api_server.py
|
||||||
|
cp ./chat_utils.py /usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/chat_utils.py
|
||||||
117
qwen3_6_scripts/patch_transformers_qwen3_5.py
Normal file
117
qwen3_6_scripts/patch_transformers_qwen3_5.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Patches transformers 4.55.3 to register qwen3_5 and qwen3_5_moe model types.
|
||||||
|
|
||||||
|
Deploy steps on the remote machine:
|
||||||
|
1. cp -r modified_scripts/qwen3_5 /usr/local/lib/python3.10/site-packages/transformers/models/qwen3_5
|
||||||
|
2. cp -r modified_scripts/qwen3_5_moe /usr/local/lib/python3.10/site-packages/transformers/models/qwen3_5_moe
|
||||||
|
3. python3 modified_scripts/patch_transformers_qwen3_5.py
|
||||||
|
|
||||||
|
Target: pip-installed transformers at /usr/local/lib/python3.10/site-packages/transformers/
|
||||||
|
(Not the corex pre-installed path at /usr/local/corex/lib64/python3/dist-packages/)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
TRANSFORMERS_ROOT = "/usr/local/lib/python3.10/site-packages/transformers"
|
||||||
|
AUTO_CONFIG = f"{TRANSFORMERS_ROOT}/models/auto/configuration_auto.py"
|
||||||
|
MODELS_INIT = f"{TRANSFORMERS_ROOT}/models/__init__.py"
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path, replacements):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
for old, new in replacements:
|
||||||
|
if new in content:
|
||||||
|
print(f" [skip] already patched: {repr(new[:60])}")
|
||||||
|
continue
|
||||||
|
if old not in content:
|
||||||
|
print(f" [warn] anchor not found: {repr(old[:60])}")
|
||||||
|
continue
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
patched = True
|
||||||
|
print(f" [ok] inserted after: {repr(old[:60])}")
|
||||||
|
|
||||||
|
if patched:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"=== Patching {AUTO_CONFIG} ===")
|
||||||
|
patch_file(AUTO_CONFIG, [
|
||||||
|
# CONFIG_MAPPING_NAMES: insert qwen3_5 + qwen3_5_moe right after qwen3
|
||||||
|
(
|
||||||
|
'("qwen3", "Qwen3Config"),',
|
||||||
|
'("qwen3", "Qwen3Config"),\n ("qwen3_5", "Qwen3_5Config"),\n ("qwen3_5_moe", "Qwen3_5MoeConfig"),',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'("qwen3", "Qwen3Config")\n',
|
||||||
|
'("qwen3", "Qwen3Config"),\n ("qwen3_5", "Qwen3_5Config"),\n ("qwen3_5_moe", "Qwen3_5MoeConfig"),\n',
|
||||||
|
),
|
||||||
|
# MODEL_NAMES_MAPPING (model_type -> human readable name)
|
||||||
|
(
|
||||||
|
'("qwen3", "Qwen3"),',
|
||||||
|
'("qwen3", "Qwen3"),\n ("qwen3_5", "Qwen3_5"),\n ("qwen3_5_moe", "Qwen3_5_MoE"),',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'("qwen3", "Qwen3")\n',
|
||||||
|
'("qwen3", "Qwen3"),\n ("qwen3_5", "Qwen3_5"),\n ("qwen3_5_moe", "Qwen3_5_MoE"),\n',
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
print(f"\n=== Patching {MODELS_INIT} ===")
|
||||||
|
patch_file(MODELS_INIT, [
|
||||||
|
(
|
||||||
|
"from .qwen3 import *\n",
|
||||||
|
"from .qwen3 import *\n from .qwen3_5 import *\n from .qwen3_5_moe import *\n",
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
print("\n=== Verification ===")
|
||||||
|
try:
|
||||||
|
import importlib.util, types
|
||||||
|
|
||||||
|
def _load_config_mod(module_name, file_path):
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
mod.__package__ = ".".join(module_name.split(".")[:-1])
|
||||||
|
pkg = sys.modules.setdefault("transformers", types.ModuleType("transformers"))
|
||||||
|
pkg.__path__ = [TRANSFORMERS_ROOT]
|
||||||
|
cu = sys.modules.setdefault(
|
||||||
|
"transformers.configuration_utils", types.ModuleType("transformers.configuration_utils"))
|
||||||
|
class _PC:
|
||||||
|
def __init__(self, **kwargs): pass
|
||||||
|
cu.PretrainedConfig = _PC
|
||||||
|
for sub in ("transformers.models", f"transformers.models.{module_name.split('.')[-2]}"):
|
||||||
|
m = sys.modules.setdefault(sub, types.ModuleType(sub))
|
||||||
|
m.__path__ = [TRANSFORMERS_ROOT]
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
mod27 = _load_config_mod(
|
||||||
|
"transformers.models.qwen3_5.configuration_qwen3_5",
|
||||||
|
f"{TRANSFORMERS_ROOT}/models/qwen3_5/configuration_qwen3_5.py",
|
||||||
|
)
|
||||||
|
cfg = mod27.Qwen3_5Config()
|
||||||
|
print(f" Qwen3_5Config() smoke-test OK (model_type={cfg.model_type})")
|
||||||
|
|
||||||
|
mod35 = _load_config_mod(
|
||||||
|
"transformers.models.qwen3_5_moe.configuration_qwen3_5_moe",
|
||||||
|
f"{TRANSFORMERS_ROOT}/models/qwen3_5_moe/configuration_qwen3_5_moe.py",
|
||||||
|
)
|
||||||
|
moe_cfg = mod35.Qwen3_5MoeConfig()
|
||||||
|
print(f" Qwen3_5MoeConfig() smoke-test OK (model_type={moe_cfg.model_type})")
|
||||||
|
t = moe_cfg.text_config
|
||||||
|
print(f" num_experts={t.num_experts}, top_k={t.num_experts_per_tok}, "
|
||||||
|
f"shared={t.shared_expert_intermediate_size}, layers={t.num_hidden_layers}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [warn] smoke-test failed (may be fine at runtime): {e}")
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
76
qwen3_6_scripts/patch_vllm_qwen3_5.py
Normal file
76
qwen3_6_scripts/patch_vllm_qwen3_5.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Patches the vLLM model registry and deploys the Qwen3_5 model file.
|
||||||
|
|
||||||
|
Deploy steps on the remote machine:
|
||||||
|
1. cp modified_scripts/qwen3_5.py \
|
||||||
|
/usr/local/corex/lib64/python3/dist-packages/vllm/model_executor/models/qwen3_5.py
|
||||||
|
2. python3 modified_scripts/patch_vllm_qwen3_5.py
|
||||||
|
|
||||||
|
Also edit your model config.json to set:
|
||||||
|
"architectures": ["Qwen3_5ForCausalLM"]
|
||||||
|
|
||||||
|
Target: vLLM at /usr/local/corex/lib64/python3/dist-packages/vllm/
|
||||||
|
"""
|
||||||
|
|
||||||
|
VLLM_ROOT = "/usr/local/corex/lib64/python3/dist-packages/vllm"
|
||||||
|
REGISTRY = f"{VLLM_ROOT}/model_executor/models/registry.py"
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path, replacements):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
for old, new in replacements:
|
||||||
|
if new in content:
|
||||||
|
print(f" [skip] already patched: {repr(new[:70])}")
|
||||||
|
continue
|
||||||
|
if old not in content:
|
||||||
|
print(f" [warn] anchor not found: {repr(old[:70])}")
|
||||||
|
continue
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
patched = True
|
||||||
|
print(f" [ok] patched after: {repr(old[:70])}")
|
||||||
|
|
||||||
|
if patched:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"=== Patching {REGISTRY} ===")
|
||||||
|
patch_file(REGISTRY, [
|
||||||
|
(
|
||||||
|
' "Qwen3ForCausalLM": ("qwen3", "Qwen3ForCausalLM"),\n'
|
||||||
|
' "Qwen3MoeForCausalLM": ("qwen3_moe", "Qwen3MoeForCausalLM"),',
|
||||||
|
' "Qwen3ForCausalLM": ("qwen3", "Qwen3ForCausalLM"),\n'
|
||||||
|
' "Qwen3MoeForCausalLM": ("qwen3_moe", "Qwen3MoeForCausalLM"),\n'
|
||||||
|
' "Qwen3_5ForCausalLM": ("qwen3_5", "Qwen3_5ForCausalLM"),\n'
|
||||||
|
' "Qwen3_5MoeForCausalLM": ("qwen3_5", "Qwen3_5MoeForCausalLM"),',
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
print("\n=== Verification ===")
|
||||||
|
try:
|
||||||
|
import importlib.util
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"qwen3_5",
|
||||||
|
f"{VLLM_ROOT}/model_executor/models/qwen3_5.py",
|
||||||
|
)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
# Quick check: does the class exist?
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
cls = mod.Qwen3_5ForCausalLM
|
||||||
|
print(f" Qwen3_5ForCausalLM found: {cls}")
|
||||||
|
cls_moe = mod.Qwen3_5MoeForCausalLM
|
||||||
|
print(f" Qwen3_5MoeForCausalLM found: {cls_moe}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [warn] verification failed (may be OK at runtime): {e}")
|
||||||
|
|
||||||
|
print("\nDone. Remember to:")
|
||||||
|
print(" 1. Set config.json 'architectures': ['Qwen3_5ForCausalLM'] or ['Qwen3_5MoEForCausalLM']")
|
||||||
|
print(" 2. Run patch_transformers_qwen3_5.py if not already done")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
79
qwen3_6_scripts/patch_vllm_tool_parser.py
Normal file
79
qwen3_6_scripts/patch_vllm_tool_parser.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Patches vLLM 0.6.3 to register Qwen3CoderToolParser under the name "qwen3_coder".
|
||||||
|
|
||||||
|
Deploy steps on the remote machine (already called by patch_ops.sh):
|
||||||
|
1. cp qwen3coder_tool_parser.py \
|
||||||
|
/usr/local/corex/lib/python3/dist-packages/vllm/entrypoints/openai/tool_parsers/
|
||||||
|
2. python3 patch_vllm_tool_parser.py
|
||||||
|
|
||||||
|
Usage after patching:
|
||||||
|
--tool-call-parser qwen3_coder --enable-auto-tool-choice
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
VLLM_ROOT = "/usr/local/corex/lib/python3/dist-packages/vllm"
|
||||||
|
TOOL_PARSERS_DIR = f"{VLLM_ROOT}/entrypoints/openai/tool_parsers"
|
||||||
|
INIT_FILE = f"{TOOL_PARSERS_DIR}/__init__.py"
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path, replacements):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
patched = False
|
||||||
|
for old, new in replacements:
|
||||||
|
if new in content:
|
||||||
|
print(f" [skip] already patched: {repr(new[:70])}")
|
||||||
|
continue
|
||||||
|
if old not in content:
|
||||||
|
print(f" [warn] anchor not found: {repr(old[:70])}")
|
||||||
|
continue
|
||||||
|
content = content.replace(old, new, 1)
|
||||||
|
patched = True
|
||||||
|
print(f" [ok] patched: {repr(old[:50])} -> {repr(new[:50])}")
|
||||||
|
|
||||||
|
if patched:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.isdir(TOOL_PARSERS_DIR):
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Tool parsers directory not found: {TOOL_PARSERS_DIR}\n"
|
||||||
|
"Verify the vLLM installation path.")
|
||||||
|
|
||||||
|
print(f"=== Patching {INIT_FILE} ===")
|
||||||
|
patch_file(INIT_FILE, [
|
||||||
|
(
|
||||||
|
"from .mistral_tool_parser import MistralToolParser",
|
||||||
|
"from .mistral_tool_parser import MistralToolParser\n"
|
||||||
|
"from .qwen3coder_tool_parser import Qwen3CoderToolParser",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'"MistralToolParser", "Internlm2ToolParser", "Llama3JsonToolParser"\n]',
|
||||||
|
'"MistralToolParser", "Internlm2ToolParser", "Llama3JsonToolParser",\n'
|
||||||
|
' "Qwen3CoderToolParser"\n]',
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
print("\n=== Verification ===")
|
||||||
|
try:
|
||||||
|
import importlib.util
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"qwen3coder_tool_parser",
|
||||||
|
f"{TOOL_PARSERS_DIR}/qwen3coder_tool_parser.py",
|
||||||
|
)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
print(f" Module spec loaded: {spec.name}")
|
||||||
|
print(" (full import requires torch/vllm runtime — skipping exec)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [warn] spec check failed: {e}")
|
||||||
|
|
||||||
|
print("\nDone. Start vLLM server with:")
|
||||||
|
print(" --tool-call-parser qwen3_coder --enable-auto-tool-choice")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
192
qwen3_6_scripts/patch_xformers_sdpa_batch.py
Normal file
192
qwen3_6_scripts/patch_xformers_sdpa_batch.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
"""
|
||||||
|
策略:批量(block-diagonal)fallback — 纯 PyTorch 数学实现
|
||||||
|
=============================================================
|
||||||
|
构建块对角 causal mask,对整批序列一次 matmul + softmax,
|
||||||
|
完全绕开所有硬件 flash attention kernel。
|
||||||
|
|
||||||
|
背景:
|
||||||
|
ixformer flshattF: head_dim > 128 报错拒绝
|
||||||
|
cudnnFlashAttnForward: 接受 head_dim=256,但数值结果错误(输出全"!")
|
||||||
|
两者大概率是同一硬件单元,ixformer 提前拦截了硬件不支持的配置。
|
||||||
|
纯 matmul 路径完全绕开硬件 flash attention,数值正确。
|
||||||
|
|
||||||
|
优点:
|
||||||
|
数值正确。
|
||||||
|
并发请求 prefill attention 在 GPU 上真正并行(一次大 matmul)。
|
||||||
|
|
||||||
|
缺点:
|
||||||
|
峰值显存 = total_tokens² × H × dtype_size
|
||||||
|
total_tokens 受 --max-num-batched-tokens 控制,max-model-len 控制不住。
|
||||||
|
|
||||||
|
内存参考(fp16,H_local=6,--max-num-batched-tokens=T):
|
||||||
|
T=2048 → 峰值 ~50 MB
|
||||||
|
T=4096 → 峰值 ~200 MB
|
||||||
|
T=8192 → 峰值 ~800 MB
|
||||||
|
T=16384 → 峰值 ~3.2 GB
|
||||||
|
|
||||||
|
Deploy:
|
||||||
|
python3 modified_scripts/patch_xformers_sdpa_batch.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
XFORMERS_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/attention/backends/xformers.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
FALLBACK_METHOD = '''
|
||||||
|
def _run_sdpa_fallback(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: "XFormersMetadata",
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""批量纯数学 attention fallback。
|
||||||
|
|
||||||
|
构建块对角 causal mask(等价于 ixformer BlockDiagonalCausalMask),
|
||||||
|
对整批序列一次 matmul + softmax,GPU 并行处理所有序列。
|
||||||
|
|
||||||
|
块对角 mask 结构(seq1 len=3,seq2 len=2):
|
||||||
|
s1,0 s1,1 s1,2 s2,0 s2,1
|
||||||
|
s1,0 [ 0 -inf -inf -inf -inf ]
|
||||||
|
s1,1 [ 0 0 -inf -inf -inf ]
|
||||||
|
s1,2 [ 0 0 0 -inf -inf ]
|
||||||
|
s2,0 [-inf -inf -inf 0 -inf ]
|
||||||
|
s2,1 [-inf -inf -inf 0 0 ]
|
||||||
|
|
||||||
|
softmax 在 float32 下计算防止 float16 溢出,结果转回原始 dtype。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query : [1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
key : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
value : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
Returns:
|
||||||
|
[1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
"""
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
orig_dtype = query.dtype
|
||||||
|
total_tokens = query.shape[1]
|
||||||
|
|
||||||
|
# ── 构建块对角 causal mask [T, T] ────────────────────────────────
|
||||||
|
# 全部初始化为 -inf,再对每条序列的对角块填入下三角 0
|
||||||
|
mask = torch.full(
|
||||||
|
(total_tokens, total_tokens),
|
||||||
|
float("-inf"),
|
||||||
|
dtype=torch.float32,
|
||||||
|
device=query.device,
|
||||||
|
)
|
||||||
|
start = 0
|
||||||
|
for seq_len in attn_metadata.seq_lens:
|
||||||
|
end = start + seq_len
|
||||||
|
mask[start:end, start:end] = torch.tril(
|
||||||
|
torch.zeros(seq_len, seq_len,
|
||||||
|
dtype=torch.float32, device=query.device)
|
||||||
|
)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
# ── [1, H, T, D],.contiguous() ──────────────────────────────────
|
||||||
|
q_all = query.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
k_all = key.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
v_all = value.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
|
||||||
|
# ── GQA:展开 KV heads ────────────────────────────────────────────
|
||||||
|
if k_all.shape[1] != q_all.shape[1]:
|
||||||
|
n = q_all.shape[1] // k_all.shape[1]
|
||||||
|
k_all = k_all.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
v_all = v_all.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
|
||||||
|
# ── 纯数学 attention(float32 防溢出)────────────────────────────
|
||||||
|
# [1, H, T, T]
|
||||||
|
attn_w = torch.matmul(q_all.float(), k_all.float().transpose(-2, -1))
|
||||||
|
attn_w = attn_w * self.scale
|
||||||
|
attn_w = attn_w + mask # 加法广播:mask [T,T] → [1, H, T, T]
|
||||||
|
attn_w = torch.softmax(attn_w, dim=-1)
|
||||||
|
|
||||||
|
out = torch.matmul(attn_w, v_all.float()).to(orig_dtype)
|
||||||
|
# [1, H, T, D] → [1, T, H, D]
|
||||||
|
return out.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
OLD_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
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,
|
||||||
|
op = self.attn_op
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
# Add the batch dimension.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
if self.head_size > 128:
|
||||||
|
out = self._run_sdpa_fallback(query, key, value, attn_metadata)
|
||||||
|
else:
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=attn_bias[0],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=self.attn_op,
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
INJECT_ANCHOR = " def _run_memory_efficient_xformers_forward("
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "_run_sdpa_fallback" in content:
|
||||||
|
print(" [skip] _run_sdpa_fallback already present")
|
||||||
|
elif INJECT_ANCHOR not in content:
|
||||||
|
print(" [warn] inject anchor not found")
|
||||||
|
else:
|
||||||
|
content = content.replace(INJECT_ANCHOR, FALLBACK_METHOD + INJECT_ANCHOR, 1)
|
||||||
|
print(" [ok] injected _run_sdpa_fallback (batch, pure-math)")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if NEW_XFORMER_BLOCK in content:
|
||||||
|
print(" [skip] dispatch block already patched")
|
||||||
|
elif OLD_XFORMER_BLOCK in content:
|
||||||
|
content = content.replace(OLD_XFORMER_BLOCK, NEW_XFORMER_BLOCK, 1)
|
||||||
|
print(" [ok] patched dispatch block")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] dispatch block anchor not found")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== patch_xformers_sdpa_batch (batch, pure-math) ===")
|
||||||
|
print(f"Target: {XFORMERS_PATH}")
|
||||||
|
patch_file(XFORMERS_PATH)
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
191
qwen3_6_scripts/patch_xformers_sdpa_batch_kernel.py
Normal file
191
qwen3_6_scripts/patch_xformers_sdpa_batch_kernel.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
策略:批量(block-diagonal)— F.scaled_dot_product_attention,可走硬件 kernel
|
||||||
|
=============================================================================
|
||||||
|
构建块对角 causal mask,对整批序列一次 F.scaled_dot_product_attention。
|
||||||
|
与 patch_xformers_sdpa_batch.py(纯 matmul)的区别:
|
||||||
|
SDPA 会根据 PyTorch/驱动能力分发到最优 kernel(Flash Attention /
|
||||||
|
mem-efficient attention / math fallback),而不是固定走 cublas matmul。
|
||||||
|
|
||||||
|
历史说明:
|
||||||
|
该方案最早因输出全"!"而被弃用,后续排查确认"!"由 mamba_cache.py bug
|
||||||
|
引起,与 attention 实现无关。当前恢复此方案用于性能对比测试。
|
||||||
|
|
||||||
|
已知硬件限制(BI-V100):
|
||||||
|
cudnnFlashAttnForward 不支持 is_causal=True(报错)。
|
||||||
|
本实现使用 is_causal=False + 显式块对角 additive mask 规避此限制。
|
||||||
|
若 SDPA 仍分发到有问题的 kernel,回退到 patch_xformers_sdpa_batch.py。
|
||||||
|
|
||||||
|
优点(vs 纯 matmul):
|
||||||
|
SDPA 可分发到 Flash Attention kernel → O(L) 显存、更快的 CUDA kernel。
|
||||||
|
|
||||||
|
缺点:
|
||||||
|
依赖硬件 kernel 行为,若 kernel 有 bug 则数值错误(需与 matmul 版对比验证)。
|
||||||
|
|
||||||
|
Deploy:
|
||||||
|
python3 modified_scripts/patch_xformers_sdpa_batch_kernel.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
XFORMERS_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/attention/backends/xformers.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
FALLBACK_METHOD = '''
|
||||||
|
def _run_sdpa_fallback(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: "XFormersMetadata",
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""批量 F.scaled_dot_product_attention fallback(可走硬件 kernel)。
|
||||||
|
|
||||||
|
构建块对角 causal mask,对整批序列一次 SDPA 调用。
|
||||||
|
SDPA 可分发到 Flash Attention / mem-efficient attention kernel。
|
||||||
|
is_causal=False + 显式 additive mask,规避 cudnnFlashAttnForward
|
||||||
|
不支持 is_causal=True 的限制。
|
||||||
|
|
||||||
|
块对角 mask(seq1 len=3,seq2 len=2):
|
||||||
|
s1,0 s1,1 s1,2 s2,0 s2,1
|
||||||
|
s1,0 [ 0 -inf -inf -inf -inf ]
|
||||||
|
s1,1 [ 0 0 -inf -inf -inf ]
|
||||||
|
s1,2 [ 0 0 0 -inf -inf ]
|
||||||
|
s2,0 [-inf -inf -inf 0 -inf ]
|
||||||
|
s2,1 [-inf -inf -inf 0 0 ]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query : [1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
key : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
value : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
Returns:
|
||||||
|
[1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
"""
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
orig_dtype = query.dtype
|
||||||
|
total_tokens = query.shape[1]
|
||||||
|
|
||||||
|
# ── 块对角 causal mask [T, T] ─────────────────────────────────────
|
||||||
|
mask = torch.full(
|
||||||
|
(total_tokens, total_tokens),
|
||||||
|
float("-inf"),
|
||||||
|
dtype=orig_dtype,
|
||||||
|
device=query.device,
|
||||||
|
)
|
||||||
|
start = 0
|
||||||
|
for seq_len in attn_metadata.seq_lens:
|
||||||
|
end = start + seq_len
|
||||||
|
mask[start:end, start:end] = torch.tril(
|
||||||
|
torch.zeros(seq_len, seq_len, dtype=orig_dtype, device=query.device)
|
||||||
|
)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
# ── [1, H, T, D] ──────────────────────────────────────────────────
|
||||||
|
q_all = query.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
k_all = key.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
v_all = value.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
|
||||||
|
# ── GQA:展开 KV heads ────────────────────────────────────────────
|
||||||
|
if k_all.shape[1] != q_all.shape[1]:
|
||||||
|
n = q_all.shape[1] // k_all.shape[1]
|
||||||
|
k_all = k_all.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
v_all = v_all.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
|
||||||
|
# ── F.scaled_dot_product_attention(可走硬件 kernel)─────────────
|
||||||
|
# is_causal=False:避免 cudnnFlashAttnForward "not support causal mode"
|
||||||
|
# attn_mask 传 additive float mask(非 bool),SDPA 选择 math/kernel 路径
|
||||||
|
out = F.scaled_dot_product_attention(
|
||||||
|
q_all, k_all, v_all,
|
||||||
|
attn_mask=mask,
|
||||||
|
dropout_p=0.0,
|
||||||
|
is_causal=False,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
||||||
|
# [1, H, T, D] → [1, T, H, D]
|
||||||
|
return out.squeeze(0).permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
OLD_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
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,
|
||||||
|
op = self.attn_op
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
# Add the batch dimension.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
if self.head_size > 128:
|
||||||
|
out = self._run_sdpa_fallback(query, key, value, attn_metadata)
|
||||||
|
else:
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=attn_bias[0],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=self.attn_op,
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
INJECT_ANCHOR = " def _run_memory_efficient_xformers_forward("
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "_run_sdpa_fallback" in content:
|
||||||
|
print(" [skip] _run_sdpa_fallback already present")
|
||||||
|
elif INJECT_ANCHOR not in content:
|
||||||
|
print(" [warn] inject anchor not found")
|
||||||
|
else:
|
||||||
|
content = content.replace(INJECT_ANCHOR, FALLBACK_METHOD + INJECT_ANCHOR, 1)
|
||||||
|
print(" [ok] injected _run_sdpa_fallback (batch, F.sdpa kernel)")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if NEW_XFORMER_BLOCK in content:
|
||||||
|
print(" [skip] dispatch block already patched")
|
||||||
|
elif OLD_XFORMER_BLOCK in content:
|
||||||
|
content = content.replace(OLD_XFORMER_BLOCK, NEW_XFORMER_BLOCK, 1)
|
||||||
|
print(" [ok] patched dispatch block")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] dispatch block anchor not found")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== patch_xformers_sdpa_batch_kernel (batch, F.sdpa + kernel dispatch) ===")
|
||||||
|
print(f"Target: {XFORMERS_PATH}")
|
||||||
|
patch_file(XFORMERS_PATH)
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
321
qwen3_6_scripts/patch_xformers_sdpa_seq.py
Normal file
321
qwen3_6_scripts/patch_xformers_sdpa_seq.py
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
"""
|
||||||
|
策略:顺序(per-sequence)fallback — 纯 PyTorch 数学实现
|
||||||
|
==========================================================
|
||||||
|
逐条序列用 matmul + softmax 手写 attention,完全绕开所有硬件
|
||||||
|
flash attention kernel(ixformer / cudnnFlashAttnForward)。
|
||||||
|
|
||||||
|
背景:
|
||||||
|
Iluvatar cudnnFlashAttnForward 存在两个已知问题:
|
||||||
|
1. 不支持 is_causal=True(报错)
|
||||||
|
2. 使用 attn_mask 路径时数值结果不正确(静默错误,输出全为"!")
|
||||||
|
与华为昇腾 910B4 上 llama.cpp --flash-attn off 修复同类问题的原理相同。
|
||||||
|
纯数学路径(matmul + softmax)在任何 PyTorch 后端上结果都正确。
|
||||||
|
|
||||||
|
优点:
|
||||||
|
数值正确,不依赖任何硬件特定 attention kernel。
|
||||||
|
峰值显存 = max(seq_len)² × H × dtype_size,由 --max-model-len 控制。
|
||||||
|
|
||||||
|
缺点:
|
||||||
|
并发请求的 prefill attention 串行执行。
|
||||||
|
O(L²) 显存(无 flash attention 的 O(L) 优化)。
|
||||||
|
|
||||||
|
内存参考(fp16,H_local=6):
|
||||||
|
max-model-len=4096 → 峰值 ~200 MB
|
||||||
|
max-model-len=8192 → 峰值 ~800 MB
|
||||||
|
max-model-len=16384 → 峰值 ~3.2 GB
|
||||||
|
|
||||||
|
额外 patch(arg_utils.py):
|
||||||
|
vllm 0.6.3 在 max_model_len > 32K 时会自动开启 chunked prefill(无命令行
|
||||||
|
关闭选项),原意是防止 profiling OOM。但 _run_sdpa_fallback 已通过 Q-tiling
|
||||||
|
解决了该问题,chunked prefill 反而会把推理路径从 _run_sdpa_fallback 切换到
|
||||||
|
_forward_prefix_pytorch,属于不必要的行为变更,因此一并禁用该自动逻辑。
|
||||||
|
|
||||||
|
Deploy:
|
||||||
|
python3 modified_scripts/patch_xformers_sdpa_seq.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
XFORMERS_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/attention/backends/xformers.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
ARG_UTILS_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/engine/arg_utils.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGITS_PROC_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/model_executor/layers/logits_processor.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
# _apply_logits_processors crashes when seq_groups is None (intermediate
|
||||||
|
# chunked-prefill chunks on the driver rank). Add an early-return guard.
|
||||||
|
_LP_OLD_BLOCK = """\
|
||||||
|
def _apply_logits_processors(
|
||||||
|
logits: torch.Tensor,
|
||||||
|
sampling_metadata: SamplingMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
found_logits_processors = False\
|
||||||
|
"""
|
||||||
|
|
||||||
|
_LP_NEW_BLOCK = """\
|
||||||
|
def _apply_logits_processors(
|
||||||
|
logits: torch.Tensor,
|
||||||
|
sampling_metadata: SamplingMetadata,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if sampling_metadata.seq_groups is None: # intermediate chunked-prefill chunk
|
||||||
|
return logits
|
||||||
|
found_logits_processors = False\
|
||||||
|
"""
|
||||||
|
|
||||||
|
# vllm 0.6.3 自动开启 chunked prefill 的原始块
|
||||||
|
_ARG_OLD_BLOCK = """\
|
||||||
|
if (is_gpu and not use_sliding_window and not use_spec_decode
|
||||||
|
and not self.enable_lora
|
||||||
|
and not self.enable_prompt_adapter):
|
||||||
|
self.enable_chunked_prefill = True
|
||||||
|
logger.warning(
|
||||||
|
"Chunked prefill is enabled by default for models with "
|
||||||
|
"max_model_len > 32K. Currently, chunked prefill might "
|
||||||
|
"not work with some features or models. If you "
|
||||||
|
"encounter any issues, please disable chunked prefill "
|
||||||
|
"by setting --enable-chunked-prefill=False.")\
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ARG_NEW_BLOCK = """\
|
||||||
|
if (is_gpu and not use_sliding_window and not use_spec_decode
|
||||||
|
and not self.enable_lora
|
||||||
|
and not self.enable_prompt_adapter):
|
||||||
|
pass # skip auto-enable: Q-tiling in _run_sdpa_fallback
|
||||||
|
# handles long-context memory without chunked prefill\
|
||||||
|
"""
|
||||||
|
|
||||||
|
FALLBACK_METHOD = '''
|
||||||
|
def _run_sdpa_fallback(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: "XFormersMetadata",
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""纯数学 causal attention fallback,带 Q-tiling 内存优化。
|
||||||
|
|
||||||
|
调用时机:kv_cache.numel()==0(profiling 阶段)。
|
||||||
|
此路径无 KV 缓存前缀,KV 长度 == query 长度。
|
||||||
|
|
||||||
|
内存优化(Q-tiling,与 Flash Attention 同思路):
|
||||||
|
将 Q 分成 _Q_CHUNK 大小的子块逐块计算,每块峰值内存
|
||||||
|
O(_Q_CHUNK × q_len) 而非 O(q_len²)。
|
||||||
|
profiling 阶段序列可能达到 max_model_len(如 20K tokens),
|
||||||
|
不加 Q-tiling 会产生 9.6 GB 矩阵直接 OOM。
|
||||||
|
|
||||||
|
softmax 在 float32 下计算以防止 float16 溢出,结果转回原始 dtype。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query : [1, total_query_tokens, num_heads, head_dim]
|
||||||
|
key : [1, total_query_tokens, num_kv_heads, head_dim]
|
||||||
|
value : [1, total_query_tokens, num_kv_heads, head_dim]
|
||||||
|
Returns:
|
||||||
|
[1, total_query_tokens, num_heads, head_dim]
|
||||||
|
"""
|
||||||
|
_Q_CHUNK = 256 # 与 _forward_prefix_pytorch 的 _ATTN_Q_CHUNK 保持一致
|
||||||
|
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
orig_dtype = query.dtype
|
||||||
|
num_seqs = len(attn_metadata.seq_lens)
|
||||||
|
|
||||||
|
# 推导每条序列的实际 query 长度。
|
||||||
|
# 正常 prefill 时 q_len == seq_len;如果将来遇到 chunked 场景,
|
||||||
|
# query_start_loc 记录的是真实 query token 数(非全序列长度)。
|
||||||
|
if (attn_metadata.query_start_loc is not None
|
||||||
|
and len(attn_metadata.query_start_loc) == num_seqs + 1):
|
||||||
|
q_lens = [
|
||||||
|
int(attn_metadata.query_start_loc[i + 1].item()) -
|
||||||
|
int(attn_metadata.query_start_loc[i].item())
|
||||||
|
for i in range(num_seqs)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
q_lens = list(attn_metadata.seq_lens)
|
||||||
|
|
||||||
|
q_flat = query.squeeze(0) # [T, H, D]
|
||||||
|
k_flat = key.squeeze(0) # [T, Hkv, D]
|
||||||
|
v_flat = value.squeeze(0)
|
||||||
|
|
||||||
|
output = torch.empty_like(q_flat)
|
||||||
|
seq_start = 0
|
||||||
|
for q_len in q_lens:
|
||||||
|
seq_end = seq_start + q_len
|
||||||
|
|
||||||
|
# 当前序列的完整 K/V(此路径无前缀,KV == Q)
|
||||||
|
k_s = k_flat[seq_start:seq_end].permute(1, 0, 2).float() # [Hkv, q_len, D]
|
||||||
|
v_s = v_flat[seq_start:seq_end].permute(1, 0, 2).float() # [Hkv, q_len, D]
|
||||||
|
|
||||||
|
# GQA:展开 KV heads 至与 query heads 一致
|
||||||
|
if k_s.shape[0] != self.num_heads:
|
||||||
|
n = self.num_heads // k_s.shape[0]
|
||||||
|
k_s = k_s.repeat_interleave(n, dim=0).contiguous()
|
||||||
|
v_s = v_s.repeat_interleave(n, dim=0).contiguous()
|
||||||
|
|
||||||
|
# k_pos 用于因果掩码
|
||||||
|
k_pos = torch.arange(q_len, device=query.device)
|
||||||
|
|
||||||
|
# Q-tiling:分块处理 query,峰值内存 O(_Q_CHUNK × q_len)
|
||||||
|
for qc_start in range(0, q_len, _Q_CHUNK):
|
||||||
|
qc_end = min(qc_start + _Q_CHUNK, q_len)
|
||||||
|
|
||||||
|
# [H, qc, D]
|
||||||
|
q_c = q_flat[seq_start + qc_start:seq_start + qc_end] \
|
||||||
|
.permute(1, 0, 2).float()
|
||||||
|
|
||||||
|
# [H, qc, q_len]
|
||||||
|
attn_w = torch.matmul(q_c, k_s.transpose(-2, -1)) * self.scale
|
||||||
|
|
||||||
|
# 因果掩码:q_c 里位置 j 只能看 k_pos <= j(相对位置)
|
||||||
|
qc_q_pos = torch.arange(qc_start, qc_end, device=query.device)
|
||||||
|
mask = k_pos.unsqueeze(0) > qc_q_pos.unsqueeze(1)
|
||||||
|
attn_w = attn_w.masked_fill(mask.unsqueeze(0), float("-inf"))
|
||||||
|
|
||||||
|
attn_w = torch.softmax(attn_w, dim=-1)
|
||||||
|
out_c = torch.matmul(attn_w, v_s).to(orig_dtype) # [H, qc, D]
|
||||||
|
|
||||||
|
output[seq_start + qc_start:seq_start + qc_end] = (
|
||||||
|
out_c.permute(1, 0, 2))
|
||||||
|
|
||||||
|
seq_start = seq_end
|
||||||
|
|
||||||
|
return output.unsqueeze(0) # [1, T, H, D]
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
OLD_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
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,
|
||||||
|
op = self.attn_op
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
# Add the batch dimension.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
if self.head_size > 128:
|
||||||
|
out = self._run_sdpa_fallback(query, key, value, attn_metadata)
|
||||||
|
else:
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=attn_bias[0],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=self.attn_op,
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
INJECT_ANCHOR = " def _run_memory_efficient_xformers_forward("
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "_run_sdpa_fallback" in content:
|
||||||
|
print(" [skip] _run_sdpa_fallback already present")
|
||||||
|
elif INJECT_ANCHOR not in content:
|
||||||
|
print(" [warn] inject anchor not found")
|
||||||
|
else:
|
||||||
|
content = content.replace(INJECT_ANCHOR, FALLBACK_METHOD + INJECT_ANCHOR, 1)
|
||||||
|
print(" [ok] injected _run_sdpa_fallback (sequential, pure-math)")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if NEW_XFORMER_BLOCK in content:
|
||||||
|
print(" [skip] dispatch block already patched")
|
||||||
|
elif OLD_XFORMER_BLOCK in content:
|
||||||
|
content = content.replace(OLD_XFORMER_BLOCK, NEW_XFORMER_BLOCK, 1)
|
||||||
|
print(" [ok] patched dispatch block")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] dispatch block anchor not found")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_arg_utils(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "skip auto-enable: Q-tiling" in content:
|
||||||
|
print(" [skip] chunked-prefill auto-enable already disabled")
|
||||||
|
elif _ARG_OLD_BLOCK in content:
|
||||||
|
content = content.replace(_ARG_OLD_BLOCK, _ARG_NEW_BLOCK, 1)
|
||||||
|
print(" [ok] disabled chunked-prefill auto-enable for 32K+")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] target block not found — check arg_utils.py version")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def patch_logits_processor(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "intermediate chunked-prefill chunk" in content:
|
||||||
|
print(" [skip] seq_groups=None guard already present")
|
||||||
|
elif _LP_OLD_BLOCK in content:
|
||||||
|
content = content.replace(_LP_OLD_BLOCK, _LP_NEW_BLOCK, 1)
|
||||||
|
print(" [ok] added seq_groups=None guard in _apply_logits_processors")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] target block not found — check logits_processor.py version")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== patch_xformers_sdpa_seq (sequential, pure-math) ===")
|
||||||
|
print(f"Target: {XFORMERS_PATH}")
|
||||||
|
patch_file(XFORMERS_PATH)
|
||||||
|
|
||||||
|
print("\n=== patch_arg_utils (disable chunked-prefill auto-enable) ===")
|
||||||
|
print(f"Target: {ARG_UTILS_PATH}")
|
||||||
|
patch_arg_utils(ARG_UTILS_PATH)
|
||||||
|
|
||||||
|
print("\n=== patch_logits_processor (seq_groups=None guard for chunked prefill) ===")
|
||||||
|
print(f"Target: {LOGITS_PROC_PATH}")
|
||||||
|
patch_logits_processor(LOGITS_PROC_PATH)
|
||||||
|
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
181
qwen3_6_scripts/patch_xformers_sdpa_seq_kernel.py
Normal file
181
qwen3_6_scripts/patch_xformers_sdpa_seq_kernel.py
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
"""
|
||||||
|
策略:顺序(per-sequence)— F.scaled_dot_product_attention,可走硬件 kernel
|
||||||
|
=============================================================================
|
||||||
|
逐条序列调用 F.scaled_dot_product_attention,is_causal=False + 显式因果 mask。
|
||||||
|
与 patch_xformers_sdpa_seq.py(纯 matmul)的区别:
|
||||||
|
SDPA 可分发到 Flash Attention / mem-efficient attention kernel,
|
||||||
|
而纯 matmul 固定走 cublas。
|
||||||
|
|
||||||
|
硬件限制(BI-V100):
|
||||||
|
cudnnFlashAttnForward 不支持 is_causal=True(直接报错)。
|
||||||
|
必须使用 is_causal=False + 显式 additive causal mask。
|
||||||
|
每条序列单独构造上三角 -inf mask,peak 显存 = max(seq_len)² × dtype,
|
||||||
|
比 batch 版的 total_tokens² 小得多。
|
||||||
|
|
||||||
|
与 batch_kernel 的对比:
|
||||||
|
seq_kernel: 显存小,peak = max_single_seq²;并发 prefill 串行排队
|
||||||
|
batch_kernel: 显存大,peak = total_tokens²;并发 prefill 一次并行处理,
|
||||||
|
通过 --max-num-batched-tokens 控制 total_tokens 上限
|
||||||
|
|
||||||
|
Deploy:
|
||||||
|
python3 modified_scripts/patch_xformers_sdpa_seq_kernel.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
XFORMERS_PATH = (
|
||||||
|
"/usr/local/corex/lib64/python3/dist-packages/"
|
||||||
|
"vllm/attention/backends/xformers.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
FALLBACK_METHOD = '''
|
||||||
|
def _run_sdpa_fallback(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
attn_metadata: "XFormersMetadata",
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""顺序 F.scaled_dot_product_attention fallback(可走硬件 kernel)。
|
||||||
|
|
||||||
|
逐条序列调用 SDPA,is_causal=False + 显式上三角 additive mask。
|
||||||
|
cudnnFlashAttnForward 不支持 is_causal=True,必须用显式 mask。
|
||||||
|
逐序列构造 mask,peak 显存 = max(seq_len)² × dtype(远小于 batch 版)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query : [1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
key : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
value : [1, total_prefill_tokens, num_kv_heads, head_dim]
|
||||||
|
Returns:
|
||||||
|
[1, total_prefill_tokens, num_heads, head_dim]
|
||||||
|
"""
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
assert attn_metadata.seq_lens is not None
|
||||||
|
orig_dtype = query.dtype
|
||||||
|
|
||||||
|
q_flat = query.squeeze(0) # [T, H, D]
|
||||||
|
k_flat = key.squeeze(0) # [T, Hkv, D]
|
||||||
|
v_flat = value.squeeze(0)
|
||||||
|
|
||||||
|
output = torch.empty_like(q_flat)
|
||||||
|
start = 0
|
||||||
|
for seq_len in attn_metadata.seq_lens:
|
||||||
|
end = start + seq_len
|
||||||
|
# [1, H, L, D]
|
||||||
|
q_s = q_flat[start:end].permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
k_s = k_flat[start:end].permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
v_s = v_flat[start:end].permute(1, 0, 2).contiguous().unsqueeze(0)
|
||||||
|
|
||||||
|
# GQA:展开 KV heads
|
||||||
|
if k_s.shape[1] != q_s.shape[1]:
|
||||||
|
n = q_s.shape[1] // k_s.shape[1]
|
||||||
|
k_s = k_s.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
v_s = v_s.repeat_interleave(n, dim=1).contiguous()
|
||||||
|
|
||||||
|
# 逐序列因果 mask [L, L],上三角 -inf
|
||||||
|
causal_mask = torch.tril(
|
||||||
|
torch.zeros(seq_len, seq_len, dtype=orig_dtype, device=q_s.device)
|
||||||
|
)
|
||||||
|
causal_mask = causal_mask.masked_fill(
|
||||||
|
torch.triu(torch.ones(seq_len, seq_len, dtype=torch.bool,
|
||||||
|
device=q_s.device), diagonal=1),
|
||||||
|
float("-inf"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# is_causal=False + 显式 mask,规避 cudnnFlashAttnForward 不支持 is_causal=True
|
||||||
|
out_s = F.scaled_dot_product_attention(
|
||||||
|
q_s, k_s, v_s,
|
||||||
|
attn_mask=causal_mask,
|
||||||
|
dropout_p=0.0,
|
||||||
|
is_causal=False,
|
||||||
|
scale=self.scale,
|
||||||
|
)
|
||||||
|
# [1, H, L, D] → [L, H, D]
|
||||||
|
output[start:end] = out_s.squeeze(0).permute(1, 0, 2).to(orig_dtype)
|
||||||
|
start = end
|
||||||
|
|
||||||
|
return output.unsqueeze(0) # [1, T, H, D]
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
OLD_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
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,
|
||||||
|
op = self.attn_op
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_XFORMER_BLOCK = """\
|
||||||
|
self.attn_op = xops.fmha.flash.FwOp()
|
||||||
|
if self.alibi_slopes is None:
|
||||||
|
# Add the batch dimension.
|
||||||
|
query = query.unsqueeze(0)
|
||||||
|
key = key.unsqueeze(0)
|
||||||
|
value = value.unsqueeze(0)
|
||||||
|
if self.head_size > 128:
|
||||||
|
out = self._run_sdpa_fallback(query, key, value, attn_metadata)
|
||||||
|
else:
|
||||||
|
out = xops.memory_efficient_attention_forward(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
attn_bias=attn_bias[0],
|
||||||
|
p=0.0,
|
||||||
|
scale=self.scale,
|
||||||
|
op=self.attn_op,
|
||||||
|
)
|
||||||
|
return out.view_as(original_query)\
|
||||||
|
"""
|
||||||
|
|
||||||
|
INJECT_ANCHOR = " def _run_memory_efficient_xformers_forward("
|
||||||
|
|
||||||
|
|
||||||
|
def patch_file(path):
|
||||||
|
with open(path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if "_run_sdpa_fallback" in content:
|
||||||
|
print(" [skip] _run_sdpa_fallback already present")
|
||||||
|
elif INJECT_ANCHOR not in content:
|
||||||
|
print(" [warn] inject anchor not found")
|
||||||
|
else:
|
||||||
|
content = content.replace(INJECT_ANCHOR, FALLBACK_METHOD + INJECT_ANCHOR, 1)
|
||||||
|
print(" [ok] injected _run_sdpa_fallback (seq, F.sdpa kernel)")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if NEW_XFORMER_BLOCK in content:
|
||||||
|
print(" [skip] dispatch block already patched")
|
||||||
|
elif OLD_XFORMER_BLOCK in content:
|
||||||
|
content = content.replace(OLD_XFORMER_BLOCK, NEW_XFORMER_BLOCK, 1)
|
||||||
|
print(" [ok] patched dispatch block")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
print(" [warn] dispatch block anchor not found")
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f" Written: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== patch_xformers_sdpa_seq_kernel (seq, F.sdpa + kernel dispatch) ===")
|
||||||
|
print(f"Target: {XFORMERS_PATH}")
|
||||||
|
patch_file(XFORMERS_PATH)
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1043
qwen3_6_scripts/protocol.py
Normal file
1043
qwen3_6_scripts/protocol.py
Normal file
File diff suppressed because it is too large
Load Diff
1369
qwen3_6_scripts/qwen3_5.py
Normal file
1369
qwen3_6_scripts/qwen3_5.py
Normal file
File diff suppressed because it is too large
Load Diff
3
qwen3_6_scripts/qwen3_5/__init__.py
Normal file
3
qwen3_6_scripts/qwen3_5/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .configuration_qwen3_5 import Qwen3_5Config, Qwen3_5TextConfig, Qwen3_5VisionConfig
|
||||||
|
|
||||||
|
__all__ = ["Qwen3_5Config", "Qwen3_5TextConfig", "Qwen3_5VisionConfig"]
|
||||||
188
qwen3_6_scripts/qwen3_5/configuration_qwen3_5.py
Normal file
188
qwen3_6_scripts/qwen3_5/configuration_qwen3_5.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Adapted from transformers 5.2.0 for compatibility with transformers 4.55.3 + torch 2.1.0
|
||||||
|
# Stubs layer_type_validation and RopeParameters which do not exist in 4.55.3
|
||||||
|
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from ...configuration_utils import PretrainedConfig as PreTrainedConfig
|
||||||
|
|
||||||
|
# --- Local stubs for APIs not present in transformers 4.55.3 ---
|
||||||
|
# Always use these definitions; do NOT import from the older transformers
|
||||||
|
# as same-named functions there have incompatible signatures.
|
||||||
|
|
||||||
|
def layer_type_validation(layer_types, num_hidden_layers=None, attention=True):
|
||||||
|
allowed = {"full_attention", "linear_attention"}
|
||||||
|
if not all(lt in allowed for lt in layer_types):
|
||||||
|
raise ValueError(f"layer_types entries must be in {allowed}, got {layer_types}")
|
||||||
|
if num_hidden_layers is not None and num_hidden_layers != len(layer_types):
|
||||||
|
raise ValueError(
|
||||||
|
f"num_hidden_layers ({num_hidden_layers}) != len(layer_types) ({len(layer_types)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import TypedDict
|
||||||
|
class RopeParameters(TypedDict, total=False):
|
||||||
|
rope_theta: float
|
||||||
|
rope_type: str
|
||||||
|
partial_rotary_factor: float
|
||||||
|
factor: float
|
||||||
|
except Exception:
|
||||||
|
RopeParameters = dict
|
||||||
|
|
||||||
|
# --- End stubs ---
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5TextConfig(PreTrainedConfig):
|
||||||
|
r"""
|
||||||
|
Configuration for the text backbone of Qwen3.5 / Qwen3.6-27B models.
|
||||||
|
model_type is "qwen3_5_text" (used internally by the nested config).
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_type = "qwen3_5_text"
|
||||||
|
keys_to_ignore_at_inference = ["past_key_values"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vocab_size=248320,
|
||||||
|
hidden_size=4096,
|
||||||
|
intermediate_size=12288,
|
||||||
|
num_hidden_layers=32,
|
||||||
|
num_attention_heads=16,
|
||||||
|
num_key_value_heads=4,
|
||||||
|
hidden_act="silu",
|
||||||
|
max_position_embeddings=32768,
|
||||||
|
initializer_range=0.02,
|
||||||
|
rms_norm_eps=1e-6,
|
||||||
|
use_cache=True,
|
||||||
|
tie_word_embeddings=False,
|
||||||
|
rope_parameters=None,
|
||||||
|
attention_bias=False,
|
||||||
|
attention_dropout=0.0,
|
||||||
|
head_dim=256,
|
||||||
|
linear_conv_kernel_dim=4,
|
||||||
|
linear_key_head_dim=128,
|
||||||
|
linear_value_head_dim=128,
|
||||||
|
linear_num_key_heads=16,
|
||||||
|
linear_num_value_heads=32,
|
||||||
|
layer_types=None,
|
||||||
|
pad_token_id=None,
|
||||||
|
bos_token_id=None,
|
||||||
|
eos_token_id=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.pad_token_id = pad_token_id
|
||||||
|
self.bos_token_id = bos_token_id
|
||||||
|
self.eos_token_id = eos_token_id
|
||||||
|
self.tie_word_embeddings = tie_word_embeddings
|
||||||
|
self.vocab_size = vocab_size
|
||||||
|
self.max_position_embeddings = max_position_embeddings
|
||||||
|
self.hidden_size = hidden_size
|
||||||
|
self.intermediate_size = intermediate_size
|
||||||
|
self.num_hidden_layers = num_hidden_layers
|
||||||
|
self.num_attention_heads = num_attention_heads
|
||||||
|
self.num_key_value_heads = num_key_value_heads
|
||||||
|
self.hidden_act = hidden_act
|
||||||
|
self.initializer_range = initializer_range
|
||||||
|
self.rms_norm_eps = rms_norm_eps
|
||||||
|
self.use_cache = use_cache
|
||||||
|
self.attention_bias = attention_bias
|
||||||
|
self.attention_dropout = attention_dropout
|
||||||
|
self.head_dim = head_dim
|
||||||
|
self.rope_parameters = rope_parameters
|
||||||
|
kwargs.setdefault("partial_rotary_factor", 0.25)
|
||||||
|
|
||||||
|
self.layer_types = layer_types
|
||||||
|
if self.layer_types is None:
|
||||||
|
interval_pattern = kwargs.get("full_attention_interval", 4)
|
||||||
|
self.layer_types = [
|
||||||
|
"linear_attention" if bool((i + 1) % interval_pattern) else "full_attention"
|
||||||
|
for i in range(self.num_hidden_layers)
|
||||||
|
]
|
||||||
|
layer_type_validation(self.layer_types, self.num_hidden_layers)
|
||||||
|
|
||||||
|
self.linear_conv_kernel_dim = linear_conv_kernel_dim
|
||||||
|
self.linear_key_head_dim = linear_key_head_dim
|
||||||
|
self.linear_value_head_dim = linear_value_head_dim
|
||||||
|
self.linear_num_key_heads = linear_num_key_heads
|
||||||
|
self.linear_num_value_heads = linear_num_value_heads
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5VisionConfig(PreTrainedConfig):
|
||||||
|
model_type = "qwen3_5_vision"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
depth=27,
|
||||||
|
hidden_size=1152,
|
||||||
|
hidden_act="gelu_pytorch_tanh",
|
||||||
|
intermediate_size=4304,
|
||||||
|
num_heads=16,
|
||||||
|
in_channels=3,
|
||||||
|
patch_size=16,
|
||||||
|
spatial_merge_size=2,
|
||||||
|
temporal_patch_size=2,
|
||||||
|
out_hidden_size=3584,
|
||||||
|
num_position_embeddings=2304,
|
||||||
|
initializer_range=0.02,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.depth = depth
|
||||||
|
self.hidden_size = hidden_size
|
||||||
|
self.hidden_act = hidden_act
|
||||||
|
self.intermediate_size = intermediate_size
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.in_channels = in_channels
|
||||||
|
self.patch_size = patch_size
|
||||||
|
self.spatial_merge_size = spatial_merge_size
|
||||||
|
self.temporal_patch_size = temporal_patch_size
|
||||||
|
self.out_hidden_size = out_hidden_size
|
||||||
|
self.num_position_embeddings = num_position_embeddings
|
||||||
|
self.initializer_range = initializer_range
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5Config(PreTrainedConfig):
|
||||||
|
r"""
|
||||||
|
Top-level configuration for Qwen3.5 / Qwen3.6-27B.
|
||||||
|
model_type = "qwen3_5" matches the model card / config.json.
|
||||||
|
Wraps Qwen3_5TextConfig (and optionally Qwen3_5VisionConfig for multimodal use).
|
||||||
|
For vLLM text-only inference only text_config is consumed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_type = "qwen3_5"
|
||||||
|
keys_to_ignore_at_inference = ["past_key_values"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
text_config=None,
|
||||||
|
vision_config=None,
|
||||||
|
image_token_id=248056,
|
||||||
|
video_token_id=248057,
|
||||||
|
vision_start_token_id=248053,
|
||||||
|
vision_end_token_id=248054,
|
||||||
|
tie_word_embeddings=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if isinstance(text_config, dict):
|
||||||
|
self.text_config = Qwen3_5TextConfig(**text_config)
|
||||||
|
elif text_config is None:
|
||||||
|
self.text_config = Qwen3_5TextConfig()
|
||||||
|
else:
|
||||||
|
self.text_config = text_config
|
||||||
|
|
||||||
|
if isinstance(vision_config, dict):
|
||||||
|
self.vision_config = Qwen3_5VisionConfig(**vision_config)
|
||||||
|
elif vision_config is None:
|
||||||
|
self.vision_config = Qwen3_5VisionConfig()
|
||||||
|
else:
|
||||||
|
self.vision_config = vision_config
|
||||||
|
|
||||||
|
self.image_token_id = image_token_id
|
||||||
|
self.video_token_id = video_token_id
|
||||||
|
self.vision_start_token_id = vision_start_token_id
|
||||||
|
self.vision_end_token_id = vision_end_token_id
|
||||||
|
self.tie_word_embeddings = tie_word_embeddings
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Qwen3_5Config", "Qwen3_5TextConfig", "Qwen3_5VisionConfig"]
|
||||||
3
qwen3_6_scripts/qwen3_5_moe/__init__.py
Normal file
3
qwen3_6_scripts/qwen3_5_moe/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .configuration_qwen3_5_moe import Qwen3_5MoeConfig, Qwen3_5MoeTextConfig
|
||||||
|
|
||||||
|
__all__ = ["Qwen3_5MoeConfig", "Qwen3_5MoeTextConfig"]
|
||||||
198
qwen3_6_scripts/qwen3_5_moe/configuration_qwen3_5_moe.py
Normal file
198
qwen3_6_scripts/qwen3_5_moe/configuration_qwen3_5_moe.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
# Adapted from transformers 5.2.0 for compatibility with transformers 4.55.3 + torch 2.1.0
|
||||||
|
# Source: transformers/models/qwen3_5_moe/configuration_qwen3_5_moe.py
|
||||||
|
# Stubs layer_type_validation and RopeParameters which do not exist in 4.55.3
|
||||||
|
# Removes ignore_keys_at_rope_validation / base_model_tp_plan / base_model_pp_plan
|
||||||
|
# which are 5.x-only and irrelevant for vLLM inference.
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ...configuration_utils import PretrainedConfig as PreTrainedConfig
|
||||||
|
|
||||||
|
# --- Local stubs for APIs not present in transformers 4.55.3 ---
|
||||||
|
def layer_type_validation(layer_types, num_hidden_layers=None, attention=True):
|
||||||
|
allowed = {"full_attention", "linear_attention"}
|
||||||
|
if not all(lt in allowed for lt in layer_types):
|
||||||
|
raise ValueError(f"layer_types entries must be in {allowed}, got {layer_types}")
|
||||||
|
if num_hidden_layers is not None and num_hidden_layers != len(layer_types):
|
||||||
|
raise ValueError(
|
||||||
|
f"num_hidden_layers ({num_hidden_layers}) != len(layer_types) ({len(layer_types)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import TypedDict
|
||||||
|
class RopeParameters(TypedDict, total=False):
|
||||||
|
rope_theta: float
|
||||||
|
rope_type: str
|
||||||
|
partial_rotary_factor: float
|
||||||
|
factor: float
|
||||||
|
except Exception:
|
||||||
|
RopeParameters = dict
|
||||||
|
|
||||||
|
# --- End stubs ---
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5MoeTextConfig(PreTrainedConfig):
|
||||||
|
r"""
|
||||||
|
Configuration for the text backbone of Qwen3.5-MoE / Qwen3.6-35B-A3B models.
|
||||||
|
model_type is "qwen3_5_moe_text" (used internally by the nested config).
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_type = "qwen3_5_moe_text"
|
||||||
|
keys_to_ignore_at_inference = ["past_key_values"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
vocab_size=248320,
|
||||||
|
hidden_size=2048,
|
||||||
|
num_hidden_layers=40,
|
||||||
|
num_attention_heads=16,
|
||||||
|
num_key_value_heads=2,
|
||||||
|
hidden_act="silu",
|
||||||
|
max_position_embeddings=32768,
|
||||||
|
initializer_range=0.02,
|
||||||
|
rms_norm_eps=1e-6,
|
||||||
|
use_cache=True,
|
||||||
|
tie_word_embeddings=False,
|
||||||
|
rope_parameters=None,
|
||||||
|
attention_bias=False,
|
||||||
|
attention_dropout=0.0,
|
||||||
|
head_dim=256,
|
||||||
|
linear_conv_kernel_dim=4,
|
||||||
|
linear_key_head_dim=128,
|
||||||
|
linear_value_head_dim=128,
|
||||||
|
linear_num_key_heads=16,
|
||||||
|
linear_num_value_heads=32,
|
||||||
|
moe_intermediate_size=512,
|
||||||
|
shared_expert_intermediate_size=512,
|
||||||
|
num_experts_per_tok=8,
|
||||||
|
num_experts=256,
|
||||||
|
output_router_logits=False,
|
||||||
|
router_aux_loss_coef=0.001,
|
||||||
|
layer_types=None,
|
||||||
|
pad_token_id=None,
|
||||||
|
bos_token_id=None,
|
||||||
|
eos_token_id=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.pad_token_id = pad_token_id
|
||||||
|
self.bos_token_id = bos_token_id
|
||||||
|
self.eos_token_id = eos_token_id
|
||||||
|
self.tie_word_embeddings = tie_word_embeddings
|
||||||
|
self.vocab_size = vocab_size
|
||||||
|
self.max_position_embeddings = max_position_embeddings
|
||||||
|
self.hidden_size = hidden_size
|
||||||
|
self.num_hidden_layers = num_hidden_layers
|
||||||
|
self.num_attention_heads = num_attention_heads
|
||||||
|
self.num_key_value_heads = num_key_value_heads
|
||||||
|
self.hidden_act = hidden_act
|
||||||
|
self.initializer_range = initializer_range
|
||||||
|
self.rms_norm_eps = rms_norm_eps
|
||||||
|
self.use_cache = use_cache
|
||||||
|
self.attention_bias = attention_bias
|
||||||
|
self.attention_dropout = attention_dropout
|
||||||
|
self.head_dim = head_dim
|
||||||
|
self.rope_parameters = rope_parameters
|
||||||
|
kwargs.setdefault("partial_rotary_factor", 0.25)
|
||||||
|
|
||||||
|
self.layer_types = layer_types
|
||||||
|
if self.layer_types is None:
|
||||||
|
interval_pattern = kwargs.get("full_attention_interval", 4)
|
||||||
|
self.layer_types = [
|
||||||
|
"linear_attention" if bool((i + 1) % interval_pattern) else "full_attention"
|
||||||
|
for i in range(self.num_hidden_layers)
|
||||||
|
]
|
||||||
|
layer_type_validation(self.layer_types, self.num_hidden_layers)
|
||||||
|
|
||||||
|
self.linear_conv_kernel_dim = linear_conv_kernel_dim
|
||||||
|
self.linear_key_head_dim = linear_key_head_dim
|
||||||
|
self.linear_value_head_dim = linear_value_head_dim
|
||||||
|
self.linear_num_key_heads = linear_num_key_heads
|
||||||
|
self.linear_num_value_heads = linear_num_value_heads
|
||||||
|
self.moe_intermediate_size = moe_intermediate_size
|
||||||
|
self.shared_expert_intermediate_size = shared_expert_intermediate_size
|
||||||
|
self.num_experts_per_tok = num_experts_per_tok
|
||||||
|
self.num_experts = num_experts
|
||||||
|
self.output_router_logits = output_router_logits
|
||||||
|
self.router_aux_loss_coef = router_aux_loss_coef
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5MoeVisionConfig(PreTrainedConfig):
|
||||||
|
model_type = "qwen3_5_moe"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
depth=27,
|
||||||
|
hidden_size=1152,
|
||||||
|
hidden_act="gelu_pytorch_tanh",
|
||||||
|
intermediate_size=4304,
|
||||||
|
num_heads=16,
|
||||||
|
in_channels=3,
|
||||||
|
patch_size=16,
|
||||||
|
spatial_merge_size=2,
|
||||||
|
temporal_patch_size=2,
|
||||||
|
out_hidden_size=3584,
|
||||||
|
num_position_embeddings=2304,
|
||||||
|
initializer_range=0.02,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.depth = depth
|
||||||
|
self.hidden_size = hidden_size
|
||||||
|
self.hidden_act = hidden_act
|
||||||
|
self.intermediate_size = intermediate_size
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.in_channels = in_channels
|
||||||
|
self.patch_size = patch_size
|
||||||
|
self.spatial_merge_size = spatial_merge_size
|
||||||
|
self.temporal_patch_size = temporal_patch_size
|
||||||
|
self.out_hidden_size = out_hidden_size
|
||||||
|
self.num_position_embeddings = num_position_embeddings
|
||||||
|
self.initializer_range = initializer_range
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3_5MoeConfig(PreTrainedConfig):
|
||||||
|
r"""
|
||||||
|
Top-level configuration for Qwen3.5-MoE / Qwen3.6-35B-A3B.
|
||||||
|
model_type = "qwen3_5_moe" matches the model card / config.json.
|
||||||
|
Wraps Qwen3_5MoeTextConfig (and optionally Qwen3_5MoeVisionConfig).
|
||||||
|
For vLLM text-only inference only text_config is consumed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_type = "qwen3_5_moe"
|
||||||
|
keys_to_ignore_at_inference = ["past_key_values"]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
text_config=None,
|
||||||
|
vision_config=None,
|
||||||
|
image_token_id=248056,
|
||||||
|
video_token_id=248057,
|
||||||
|
vision_start_token_id=248053,
|
||||||
|
vision_end_token_id=248054,
|
||||||
|
tie_word_embeddings=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
if isinstance(text_config, dict):
|
||||||
|
self.text_config = Qwen3_5MoeTextConfig(**text_config)
|
||||||
|
elif text_config is None:
|
||||||
|
self.text_config = Qwen3_5MoeTextConfig()
|
||||||
|
else:
|
||||||
|
self.text_config = text_config
|
||||||
|
|
||||||
|
if isinstance(vision_config, dict):
|
||||||
|
self.vision_config = Qwen3_5MoeVisionConfig(**vision_config)
|
||||||
|
elif vision_config is None:
|
||||||
|
self.vision_config = Qwen3_5MoeVisionConfig()
|
||||||
|
else:
|
||||||
|
self.vision_config = vision_config
|
||||||
|
|
||||||
|
self.image_token_id = image_token_id
|
||||||
|
self.video_token_id = video_token_id
|
||||||
|
self.vision_start_token_id = vision_start_token_id
|
||||||
|
self.vision_end_token_id = vision_end_token_id
|
||||||
|
self.tie_word_embeddings = tie_word_embeddings
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Qwen3_5MoeConfig", "Qwen3_5MoeTextConfig"]
|
||||||
509
qwen3_6_scripts/qwen3coder_tool_parser.py
Normal file
509
qwen3_6_scripts/qwen3coder_tool_parser.py
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from typing import Any, Dict, List, Optional, Sequence, Union
|
||||||
|
|
||||||
|
import regex as re
|
||||||
|
|
||||||
|
from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
|
||||||
|
ChatCompletionToolsParam,
|
||||||
|
DeltaFunctionCall, DeltaMessage,
|
||||||
|
DeltaToolCall,
|
||||||
|
ExtractedToolCallInformation,
|
||||||
|
FunctionCall, ToolCall)
|
||||||
|
from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (
|
||||||
|
ToolParser, ToolParserManager)
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
from vllm.transformers_utils.tokenizer import AnyTokenizer
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ToolParserManager.register_module("qwen3_coder")
|
||||||
|
class Qwen3CoderToolParser(ToolParser):
|
||||||
|
"""
|
||||||
|
Tool parser for Qwen3 models using XML-style tool call format:
|
||||||
|
<tool_call><function=name><parameter=key>
|
||||||
|
value
|
||||||
|
</parameter></function></tool_call>
|
||||||
|
|
||||||
|
Port of vllm-original qwen3coder_tool_parser.py to vllm 0.6.3 API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tokenizer: AnyTokenizer):
|
||||||
|
super().__init__(tokenizer)
|
||||||
|
|
||||||
|
self.current_tool_name_sent: bool = False
|
||||||
|
self.prev_tool_call_arr: List[Dict] = []
|
||||||
|
# Base class uses int; we override with string IDs
|
||||||
|
self.current_tool_id: Optional[str] = None # type: ignore[assignment]
|
||||||
|
self.streamed_args_for_tool: List[str] = []
|
||||||
|
|
||||||
|
self.tool_call_start_token: str = "<tool_call>"
|
||||||
|
self.tool_call_end_token: str = "</tool_call>"
|
||||||
|
self.tool_call_prefix: str = "<function="
|
||||||
|
self.function_end_token: str = "</function>"
|
||||||
|
self.parameter_prefix: str = "<parameter="
|
||||||
|
self.parameter_end_token: str = "</parameter>"
|
||||||
|
self.is_tool_call_started: bool = False
|
||||||
|
|
||||||
|
self._reset_streaming_state()
|
||||||
|
|
||||||
|
self.tool_call_complete_regex = re.compile(
|
||||||
|
r"<tool_call>(.*?)</tool_call>", re.DOTALL)
|
||||||
|
self.tool_call_regex = re.compile(
|
||||||
|
r"<tool_call>(.*?)</tool_call>|<tool_call>(.*?)$", re.DOTALL)
|
||||||
|
self.tool_call_function_regex = re.compile(
|
||||||
|
r"<function=(.*?)</function>|<function=(.*)$", re.DOTALL)
|
||||||
|
self.tool_call_parameter_regex = re.compile(
|
||||||
|
r"<parameter=(.*?)(?:</parameter>|(?=<parameter=)|(?=</function>)|$)",
|
||||||
|
re.DOTALL)
|
||||||
|
|
||||||
|
if not self.model_tokenizer:
|
||||||
|
raise ValueError(
|
||||||
|
"The model tokenizer must be passed to the ToolParser "
|
||||||
|
"constructor during construction.")
|
||||||
|
|
||||||
|
self.tool_call_start_token_id = self.vocab.get(
|
||||||
|
self.tool_call_start_token)
|
||||||
|
self.tool_call_end_token_id = self.vocab.get(self.tool_call_end_token)
|
||||||
|
|
||||||
|
if (self.tool_call_start_token_id is None
|
||||||
|
or self.tool_call_end_token_id is None):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Qwen3 XML Tool parser could not locate tool call start/end "
|
||||||
|
"tokens in the tokenizer!")
|
||||||
|
|
||||||
|
logger.debug("vLLM Successfully imported tool parser %s !",
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_tool_call_id(self) -> str:
|
||||||
|
return f"call_{uuid.uuid4().hex[:24]}"
|
||||||
|
|
||||||
|
def _reset_streaming_state(self) -> None:
|
||||||
|
self.current_tool_index = 0
|
||||||
|
self.is_tool_call_started = False
|
||||||
|
self.header_sent = False
|
||||||
|
self.current_tool_id = None
|
||||||
|
self.current_function_name: Optional[str] = None
|
||||||
|
self.current_param_name: Optional[str] = None
|
||||||
|
self.current_param_value: str = ""
|
||||||
|
self.param_count = 0
|
||||||
|
self.in_param = False
|
||||||
|
self.in_function = False
|
||||||
|
self.accumulated_text: str = ""
|
||||||
|
self.json_started = False
|
||||||
|
self.json_closed = False
|
||||||
|
self.accumulated_params: Dict[str, Any] = {}
|
||||||
|
self.streaming_request: Optional[ChatCompletionRequest] = None
|
||||||
|
|
||||||
|
def _get_arguments_config(
|
||||||
|
self, func_name: str,
|
||||||
|
tools: Optional[List[ChatCompletionToolsParam]]) -> Dict:
|
||||||
|
if tools is None:
|
||||||
|
return {}
|
||||||
|
for config in tools:
|
||||||
|
if not hasattr(config, "type") or not (
|
||||||
|
hasattr(config, "function")
|
||||||
|
and hasattr(config.function, "name")):
|
||||||
|
continue
|
||||||
|
if config.type == "function" and config.function.name == func_name:
|
||||||
|
if not hasattr(config.function, "parameters"):
|
||||||
|
return {}
|
||||||
|
params = config.function.parameters
|
||||||
|
if isinstance(params, dict) and "properties" in params:
|
||||||
|
return params["properties"]
|
||||||
|
elif isinstance(params, dict):
|
||||||
|
return params
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
logger.debug("Tool '%s' is not defined in the tools list.", func_name)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _convert_param_value(self, param_value: str, param_name: str,
|
||||||
|
param_config: Dict, func_name: str) -> Any:
|
||||||
|
if param_value.lower() == "null":
|
||||||
|
return None
|
||||||
|
|
||||||
|
if param_name not in param_config:
|
||||||
|
if param_config != {}:
|
||||||
|
logger.debug(
|
||||||
|
"Parsed parameter '%s' is not defined in tool '%s', "
|
||||||
|
"returning string value.", param_name, func_name)
|
||||||
|
return param_value
|
||||||
|
|
||||||
|
if (isinstance(param_config[param_name], dict)
|
||||||
|
and "type" in param_config[param_name]):
|
||||||
|
param_type = str(
|
||||||
|
param_config[param_name]["type"]).strip().lower()
|
||||||
|
else:
|
||||||
|
param_type = "string"
|
||||||
|
|
||||||
|
if param_type in ["string", "str", "text", "varchar", "char", "enum"]:
|
||||||
|
return param_value
|
||||||
|
elif (param_type.startswith("int") or param_type.startswith("uint")
|
||||||
|
or param_type.startswith("long")
|
||||||
|
or param_type.startswith("short")
|
||||||
|
or param_type.startswith("unsigned")):
|
||||||
|
try:
|
||||||
|
return int(param_value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return param_value
|
||||||
|
elif param_type.startswith("num") or param_type.startswith("float"):
|
||||||
|
try:
|
||||||
|
v = float(param_value)
|
||||||
|
return int(v) if v - int(v) == 0 else v
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return param_value
|
||||||
|
elif param_type in ["boolean", "bool", "binary"]:
|
||||||
|
lower = param_value.lower()
|
||||||
|
if lower not in ["true", "false"]:
|
||||||
|
logger.debug(
|
||||||
|
"Parameter '%s' value '%s' is not boolean in tool '%s'.",
|
||||||
|
param_name, param_value, func_name)
|
||||||
|
return lower == "true"
|
||||||
|
else:
|
||||||
|
if (param_type in ["object", "array", "arr"]
|
||||||
|
or param_type.startswith("dict")
|
||||||
|
or param_type.startswith("list")):
|
||||||
|
try:
|
||||||
|
return json.loads(param_value)
|
||||||
|
except (json.JSONDecodeError, TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return ast.literal_eval(param_value)
|
||||||
|
except (ValueError, SyntaxError, TypeError):
|
||||||
|
pass
|
||||||
|
return param_value
|
||||||
|
|
||||||
|
def _parse_xml_function_call(
|
||||||
|
self, function_call_str: str,
|
||||||
|
tools: Optional[List[ChatCompletionToolsParam]]) -> ToolCall:
|
||||||
|
end_index = function_call_str.index(">")
|
||||||
|
function_name = function_call_str[:end_index]
|
||||||
|
param_config = self._get_arguments_config(function_name, tools)
|
||||||
|
parameters = function_call_str[end_index + 1:]
|
||||||
|
param_dict: Dict[str, Any] = {}
|
||||||
|
for match_text in self.tool_call_parameter_regex.findall(parameters):
|
||||||
|
idx = match_text.index(">")
|
||||||
|
param_name = match_text[:idx]
|
||||||
|
param_value = str(match_text[idx + 1:])
|
||||||
|
if param_value.startswith("\n"):
|
||||||
|
param_value = param_value[1:]
|
||||||
|
if param_value.endswith("\n"):
|
||||||
|
param_value = param_value[:-1]
|
||||||
|
param_dict[param_name] = self._convert_param_value(
|
||||||
|
param_value, param_name, param_config, function_name)
|
||||||
|
return ToolCall(
|
||||||
|
type="function",
|
||||||
|
function=FunctionCall(
|
||||||
|
name=function_name,
|
||||||
|
arguments=json.dumps(param_dict, ensure_ascii=False)))
|
||||||
|
|
||||||
|
def _get_function_calls(self, model_output: str) -> List[str]:
|
||||||
|
matched_ranges = self.tool_call_regex.findall(model_output)
|
||||||
|
raw_tool_calls = [
|
||||||
|
match[0] if match[0] else match[1] for match in matched_ranges
|
||||||
|
]
|
||||||
|
if not raw_tool_calls:
|
||||||
|
raw_tool_calls = [model_output]
|
||||||
|
raw_function_calls: List[tuple] = []
|
||||||
|
for tool_call in raw_tool_calls:
|
||||||
|
raw_function_calls.extend(
|
||||||
|
self.tool_call_function_regex.findall(tool_call))
|
||||||
|
return [match[0] if match[0] else match[1]
|
||||||
|
for match in raw_function_calls]
|
||||||
|
|
||||||
|
def extract_tool_calls(
|
||||||
|
self, model_output: str,
|
||||||
|
request: ChatCompletionRequest) -> ExtractedToolCallInformation:
|
||||||
|
if self.tool_call_prefix not in model_output:
|
||||||
|
return ExtractedToolCallInformation(tools_called=False,
|
||||||
|
tool_calls=[],
|
||||||
|
content=model_output)
|
||||||
|
try:
|
||||||
|
function_calls = self._get_function_calls(model_output)
|
||||||
|
if not function_calls:
|
||||||
|
return ExtractedToolCallInformation(tools_called=False,
|
||||||
|
tool_calls=[],
|
||||||
|
content=model_output)
|
||||||
|
|
||||||
|
tool_calls = [
|
||||||
|
self._parse_xml_function_call(fc, request.tools)
|
||||||
|
for fc in function_calls
|
||||||
|
]
|
||||||
|
|
||||||
|
self.prev_tool_call_arr.clear()
|
||||||
|
for tc in tool_calls:
|
||||||
|
self.prev_tool_call_arr.append({
|
||||||
|
"name": tc.function.name,
|
||||||
|
"arguments": tc.function.arguments,
|
||||||
|
})
|
||||||
|
|
||||||
|
content_index = model_output.find(self.tool_call_start_token)
|
||||||
|
idx = model_output.find(self.tool_call_prefix)
|
||||||
|
content_index = content_index if content_index >= 0 else idx
|
||||||
|
content = model_output[:content_index]
|
||||||
|
|
||||||
|
return ExtractedToolCallInformation(
|
||||||
|
tools_called=bool(tool_calls),
|
||||||
|
tool_calls=tool_calls,
|
||||||
|
content=content if content else None,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Error extracting tool call from response.")
|
||||||
|
return ExtractedToolCallInformation(tools_called=False,
|
||||||
|
tool_calls=[],
|
||||||
|
content=model_output)
|
||||||
|
|
||||||
|
def extract_tool_calls_streaming(
|
||||||
|
self,
|
||||||
|
previous_text: str,
|
||||||
|
current_text: str,
|
||||||
|
delta_text: str,
|
||||||
|
previous_token_ids: Sequence[int],
|
||||||
|
current_token_ids: Sequence[int],
|
||||||
|
delta_token_ids: Sequence[int],
|
||||||
|
request: ChatCompletionRequest,
|
||||||
|
) -> Union[DeltaMessage, None]:
|
||||||
|
if not previous_text:
|
||||||
|
self._reset_streaming_state()
|
||||||
|
self.streaming_request = request
|
||||||
|
|
||||||
|
if not delta_text:
|
||||||
|
if delta_token_ids and self.tool_call_end_token_id not in delta_token_ids:
|
||||||
|
complete_calls = len(
|
||||||
|
self.tool_call_complete_regex.findall(current_text))
|
||||||
|
if complete_calls > 0 and self.prev_tool_call_arr:
|
||||||
|
open_calls = (
|
||||||
|
current_text.count(self.tool_call_start_token) -
|
||||||
|
current_text.count(self.tool_call_end_token))
|
||||||
|
if open_calls == 0:
|
||||||
|
return DeltaMessage(content="")
|
||||||
|
elif not self.is_tool_call_started and current_text:
|
||||||
|
return DeltaMessage(content="")
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.accumulated_text = current_text
|
||||||
|
|
||||||
|
if self.json_closed and not self.in_function:
|
||||||
|
tool_ends = current_text.count(self.tool_call_end_token)
|
||||||
|
if tool_ends > self.current_tool_index:
|
||||||
|
self.current_tool_index += 1
|
||||||
|
self.header_sent = False
|
||||||
|
self.param_count = 0
|
||||||
|
self.json_started = False
|
||||||
|
self.json_closed = False
|
||||||
|
self.accumulated_params = {}
|
||||||
|
tool_starts = current_text.count(self.tool_call_start_token)
|
||||||
|
if self.current_tool_index >= tool_starts:
|
||||||
|
self.is_tool_call_started = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self.is_tool_call_started:
|
||||||
|
if (self.tool_call_start_token_id in delta_token_ids
|
||||||
|
or self.tool_call_start_token in delta_text):
|
||||||
|
self.is_tool_call_started = True
|
||||||
|
if self.tool_call_start_token in delta_text:
|
||||||
|
content_before = delta_text[:delta_text.index(
|
||||||
|
self.tool_call_start_token)]
|
||||||
|
if content_before:
|
||||||
|
return DeltaMessage(content=content_before)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if (current_text.rstrip().endswith(self.tool_call_end_token)
|
||||||
|
and delta_text.strip() == ""):
|
||||||
|
return None
|
||||||
|
return DeltaMessage(content=delta_text)
|
||||||
|
|
||||||
|
tool_starts_count = current_text.count(self.tool_call_start_token)
|
||||||
|
if self.current_tool_index >= tool_starts_count:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Locate the current tool call's text slice
|
||||||
|
tool_start_positions: List[int] = []
|
||||||
|
search = 0
|
||||||
|
while True:
|
||||||
|
search = current_text.find(self.tool_call_start_token, search)
|
||||||
|
if search == -1:
|
||||||
|
break
|
||||||
|
tool_start_positions.append(search)
|
||||||
|
search += len(self.tool_call_start_token)
|
||||||
|
|
||||||
|
if self.current_tool_index >= len(tool_start_positions):
|
||||||
|
return None
|
||||||
|
|
||||||
|
tool_start_idx = tool_start_positions[self.current_tool_index]
|
||||||
|
tool_end_idx = current_text.find(self.tool_call_end_token,
|
||||||
|
tool_start_idx)
|
||||||
|
if tool_end_idx == -1:
|
||||||
|
tool_text = current_text[tool_start_idx:]
|
||||||
|
else:
|
||||||
|
tool_text = current_text[tool_start_idx:tool_end_idx +
|
||||||
|
len(self.tool_call_end_token)]
|
||||||
|
|
||||||
|
if not self.header_sent:
|
||||||
|
if self.tool_call_prefix in tool_text:
|
||||||
|
func_start = (tool_text.find(self.tool_call_prefix) +
|
||||||
|
len(self.tool_call_prefix))
|
||||||
|
func_end = tool_text.find(">", func_start)
|
||||||
|
if func_end != -1:
|
||||||
|
self.current_function_name = tool_text[func_start:func_end]
|
||||||
|
self.current_tool_id = self._generate_tool_call_id()
|
||||||
|
self.header_sent = True
|
||||||
|
self.in_function = True
|
||||||
|
self.prev_tool_call_arr.append({
|
||||||
|
"name": self.current_function_name,
|
||||||
|
"arguments": "{}",
|
||||||
|
})
|
||||||
|
self.streamed_args_for_tool.append("")
|
||||||
|
return DeltaMessage(tool_calls=[
|
||||||
|
DeltaToolCall(
|
||||||
|
index=self.current_tool_index,
|
||||||
|
id=self.current_tool_id,
|
||||||
|
function=DeltaFunctionCall(
|
||||||
|
name=self.current_function_name,
|
||||||
|
arguments=""),
|
||||||
|
type="function",
|
||||||
|
)
|
||||||
|
])
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.in_function:
|
||||||
|
if not self.json_started:
|
||||||
|
self.json_started = True
|
||||||
|
self.streamed_args_for_tool[self.current_tool_index] += "{"
|
||||||
|
return DeltaMessage(tool_calls=[
|
||||||
|
DeltaToolCall(
|
||||||
|
index=self.current_tool_index,
|
||||||
|
function=DeltaFunctionCall(arguments="{"),
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Collect all complete parameters in one pass (speculative-decode safe)
|
||||||
|
param_starts: List[int] = []
|
||||||
|
search = 0
|
||||||
|
while True:
|
||||||
|
search = tool_text.find(self.parameter_prefix, search)
|
||||||
|
if search == -1:
|
||||||
|
break
|
||||||
|
param_starts.append(search)
|
||||||
|
search += len(self.parameter_prefix)
|
||||||
|
|
||||||
|
json_fragments: List[str] = []
|
||||||
|
while not self.in_param and self.param_count < len(param_starts):
|
||||||
|
param_idx = param_starts[self.param_count]
|
||||||
|
param_start = param_idx + len(self.parameter_prefix)
|
||||||
|
remaining = tool_text[param_start:]
|
||||||
|
|
||||||
|
if ">" not in remaining:
|
||||||
|
break
|
||||||
|
|
||||||
|
name_end = remaining.find(">")
|
||||||
|
current_param_name = remaining[:name_end]
|
||||||
|
value_start = param_start + name_end + 1
|
||||||
|
value_text = tool_text[value_start:]
|
||||||
|
if value_text.startswith("\n"):
|
||||||
|
value_text = value_text[1:]
|
||||||
|
|
||||||
|
param_end_idx = value_text.find(self.parameter_end_token)
|
||||||
|
if param_end_idx == -1:
|
||||||
|
next_param = value_text.find(self.parameter_prefix)
|
||||||
|
func_end = value_text.find(self.function_end_token)
|
||||||
|
if next_param != -1 and (func_end == -1
|
||||||
|
or next_param < func_end):
|
||||||
|
param_end_idx = next_param
|
||||||
|
elif func_end != -1:
|
||||||
|
param_end_idx = func_end
|
||||||
|
else:
|
||||||
|
tool_end_in_value = value_text.find(
|
||||||
|
self.tool_call_end_token)
|
||||||
|
if tool_end_in_value != -1:
|
||||||
|
param_end_idx = tool_end_in_value
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if param_end_idx == -1:
|
||||||
|
break
|
||||||
|
|
||||||
|
param_value = value_text[:param_end_idx]
|
||||||
|
if param_value.endswith("\n"):
|
||||||
|
param_value = param_value[:-1]
|
||||||
|
|
||||||
|
self.accumulated_params[current_param_name] = param_value
|
||||||
|
param_config = self._get_arguments_config(
|
||||||
|
self.current_function_name or "",
|
||||||
|
self.streaming_request.tools
|
||||||
|
if self.streaming_request else None)
|
||||||
|
converted = self._convert_param_value(
|
||||||
|
param_value, current_param_name, param_config,
|
||||||
|
self.current_function_name or "")
|
||||||
|
serialized = json.dumps(converted, ensure_ascii=False)
|
||||||
|
|
||||||
|
sep = "" if self.param_count == 0 else ", "
|
||||||
|
json_fragments.append(
|
||||||
|
f'{sep}"{current_param_name}": {serialized}')
|
||||||
|
self.param_count += 1
|
||||||
|
|
||||||
|
if json_fragments:
|
||||||
|
combined = "".join(json_fragments)
|
||||||
|
if self.current_tool_index < len(self.streamed_args_for_tool):
|
||||||
|
self.streamed_args_for_tool[
|
||||||
|
self.current_tool_index] += combined
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"streamed_args_for_tool out of sync: index=%d len=%d",
|
||||||
|
self.current_tool_index,
|
||||||
|
len(self.streamed_args_for_tool))
|
||||||
|
return DeltaMessage(tool_calls=[
|
||||||
|
DeltaToolCall(
|
||||||
|
index=self.current_tool_index,
|
||||||
|
function=DeltaFunctionCall(arguments=combined),
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Emit closing brace when </function> is seen (after params are done)
|
||||||
|
if not self.json_closed and self.function_end_token in tool_text:
|
||||||
|
self.json_closed = True
|
||||||
|
func_start = (tool_text.find(self.tool_call_prefix) +
|
||||||
|
len(self.tool_call_prefix))
|
||||||
|
func_content_end = tool_text.find(self.function_end_token,
|
||||||
|
func_start)
|
||||||
|
if func_content_end != -1:
|
||||||
|
try:
|
||||||
|
parsed_tool = self._parse_xml_function_call(
|
||||||
|
tool_text[func_start:func_content_end],
|
||||||
|
self.streaming_request.tools
|
||||||
|
if self.streaming_request else None)
|
||||||
|
if self.current_tool_index < len(
|
||||||
|
self.prev_tool_call_arr):
|
||||||
|
self.prev_tool_call_arr[
|
||||||
|
self.current_tool_index]["arguments"] = (
|
||||||
|
parsed_tool.function.arguments)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Failed to parse tool call during "
|
||||||
|
"streaming: %s",
|
||||||
|
tool_text,
|
||||||
|
exc_info=True)
|
||||||
|
|
||||||
|
if self.current_tool_index < len(self.streamed_args_for_tool):
|
||||||
|
self.streamed_args_for_tool[
|
||||||
|
self.current_tool_index] += "}"
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"streamed_args_for_tool out of sync: index=%d len=%d",
|
||||||
|
self.current_tool_index,
|
||||||
|
len(self.streamed_args_for_tool))
|
||||||
|
|
||||||
|
result = DeltaMessage(tool_calls=[
|
||||||
|
DeltaToolCall(
|
||||||
|
index=self.current_tool_index,
|
||||||
|
function=DeltaFunctionCall(arguments="}"),
|
||||||
|
)
|
||||||
|
])
|
||||||
|
self.in_function = False
|
||||||
|
self.accumulated_params = {}
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
16
qwen3_6_scripts/reasoning/__init__.py
Normal file
16
qwen3_6_scripts/reasoning/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Reasoning parser module for vLLM 0.6.3 (BI-V100 / Qwen3.6-27B adaptation).
|
||||||
|
|
||||||
|
Usage: --reasoning-parser qwen3
|
||||||
|
"""
|
||||||
|
|
||||||
|
from vllm.reasoning.abs_reasoning_parsers import ReasoningParser, ReasoningParserManager
|
||||||
|
|
||||||
|
__all__ = ["ReasoningParser", "ReasoningParserManager"]
|
||||||
|
|
||||||
|
# Lazy-register Qwen3 parser; imported on first get_reasoning_parser("qwen3").
|
||||||
|
ReasoningParserManager.register_lazy(
|
||||||
|
"qwen3",
|
||||||
|
"vllm.reasoning.qwen3_reasoning_parser",
|
||||||
|
"Qwen3ReasoningParser",
|
||||||
|
)
|
||||||
243
qwen3_6_scripts/reasoning/abs_reasoning_parsers.py
Normal file
243
qwen3_6_scripts/reasoning/abs_reasoning_parsers.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
"""
|
||||||
|
Abstract reasoning parser base classes for vLLM 0.6.3.
|
||||||
|
Adapted from vllm-original/vllm/reasoning/abs_reasoning_parsers.py:
|
||||||
|
- Removed vllm.entrypoints.mcp, vllm.utils.collection_utils, import_utils
|
||||||
|
- DeltaMessage from vllm 0.6.3 protocol path
|
||||||
|
- TokenizerLike -> AnyTokenizer
|
||||||
|
- ReasoningParserManager: simplified eager + lazy registration
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
from abc import abstractmethod
|
||||||
|
from collections.abc import Iterable, Sequence
|
||||||
|
from functools import cached_property
|
||||||
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.entrypoints.openai.protocol import DeltaMessage
|
||||||
|
from vllm.transformers_utils.tokenizer import AnyTokenizer
|
||||||
|
else:
|
||||||
|
DeltaMessage = Any
|
||||||
|
AnyTokenizer = Any
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningParser:
|
||||||
|
"""Abstract base for all reasoning parsers."""
|
||||||
|
|
||||||
|
def __init__(self, tokenizer: "AnyTokenizer", *args, **kwargs):
|
||||||
|
self.model_tokenizer = tokenizer
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def vocab(self) -> dict:
|
||||||
|
return self.model_tokenizer.get_vocab()
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
|
||||||
|
"""Return True once the reasoning block has closed in input_ids."""
|
||||||
|
|
||||||
|
def is_reasoning_end_streaming(
|
||||||
|
self, input_ids: Sequence[int], delta_ids: Iterable[int]
|
||||||
|
) -> bool:
|
||||||
|
return self.is_reasoning_end(input_ids)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_content_ids(self, input_ids: list) -> list:
|
||||||
|
"""Return token ids that belong to the content (post-reasoning) part."""
|
||||||
|
|
||||||
|
def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_reasoning(
|
||||||
|
self, model_output: str, request: Any
|
||||||
|
) -> "tuple[Optional[str], Optional[str]]":
|
||||||
|
"""
|
||||||
|
Split a complete model output into (reasoning_text, content_text).
|
||||||
|
Either part may be None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def extract_reasoning_streaming(
|
||||||
|
self,
|
||||||
|
previous_text: str,
|
||||||
|
current_text: str,
|
||||||
|
delta_text: str,
|
||||||
|
previous_token_ids: Sequence[int],
|
||||||
|
current_token_ids: Sequence[int],
|
||||||
|
delta_token_ids: Sequence[int],
|
||||||
|
) -> Optional["DeltaMessage"]:
|
||||||
|
"""
|
||||||
|
Extract reasoning from a streaming delta.
|
||||||
|
Returns a DeltaMessage with reasoning_content and/or content set,
|
||||||
|
or None if this delta should be suppressed (control token).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class BaseThinkingReasoningParser(ReasoningParser):
|
||||||
|
"""
|
||||||
|
Base for parsers that use <start_token>...</end_token> delimiters.
|
||||||
|
Subclasses define start_token / end_token properties.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def start_token(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def end_token(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __init__(self, tokenizer: "AnyTokenizer", *args, **kwargs):
|
||||||
|
super().__init__(tokenizer, *args, **kwargs)
|
||||||
|
|
||||||
|
if not self.model_tokenizer:
|
||||||
|
raise ValueError("Tokenizer must be passed to ReasoningParser.")
|
||||||
|
if not self.start_token or not self.end_token:
|
||||||
|
raise ValueError("start_token and end_token must be defined.")
|
||||||
|
|
||||||
|
self.start_token_id: Optional[int] = self.vocab.get(self.start_token)
|
||||||
|
self.end_token_id: Optional[int] = self.vocab.get(self.end_token)
|
||||||
|
if self.start_token_id is None or self.end_token_id is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{self.__class__.__name__}: could not find think tokens "
|
||||||
|
f"'{self.start_token}'/'{self.end_token}' in tokenizer vocab."
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_reasoning_end(self, input_ids: Sequence[int]) -> bool:
|
||||||
|
for token_id in reversed(input_ids):
|
||||||
|
if token_id == self.start_token_id:
|
||||||
|
return False
|
||||||
|
if token_id == self.end_token_id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_reasoning_end_streaming(
|
||||||
|
self, input_ids: Sequence[int], delta_ids: Iterable[int]
|
||||||
|
) -> bool:
|
||||||
|
return self.end_token_id in delta_ids
|
||||||
|
|
||||||
|
def extract_content_ids(self, input_ids: list) -> list:
|
||||||
|
if self.end_token_id not in input_ids[:-1]:
|
||||||
|
return []
|
||||||
|
return input_ids[input_ids.index(self.end_token_id) + 1:]
|
||||||
|
|
||||||
|
def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
|
||||||
|
count = 0
|
||||||
|
depth = 0
|
||||||
|
for tid in token_ids:
|
||||||
|
if tid == self.start_token_id:
|
||||||
|
depth += 1
|
||||||
|
elif tid == self.end_token_id:
|
||||||
|
if depth > 0:
|
||||||
|
depth -= 1
|
||||||
|
elif depth > 0:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def extract_reasoning(
|
||||||
|
self, model_output: str, request: Any
|
||||||
|
) -> "tuple[Optional[str], Optional[str]]":
|
||||||
|
# Strip <think> if the model generated it (old-style template).
|
||||||
|
parts = model_output.partition(self.start_token)
|
||||||
|
model_output = parts[2] if parts[1] else parts[0]
|
||||||
|
|
||||||
|
if self.end_token not in model_output:
|
||||||
|
return model_output, None
|
||||||
|
reasoning, _, content = model_output.partition(self.end_token)
|
||||||
|
return reasoning, content or None
|
||||||
|
|
||||||
|
def extract_reasoning_streaming(
|
||||||
|
self,
|
||||||
|
previous_text: str,
|
||||||
|
current_text: str,
|
||||||
|
delta_text: str,
|
||||||
|
previous_token_ids: Sequence[int],
|
||||||
|
current_token_ids: Sequence[int],
|
||||||
|
delta_token_ids: Sequence[int],
|
||||||
|
) -> Optional["DeltaMessage"]:
|
||||||
|
from vllm.entrypoints.openai.protocol import DeltaMessage as _DeltaMessage
|
||||||
|
|
||||||
|
# Suppress lone control tokens.
|
||||||
|
if len(delta_token_ids) == 1 and delta_token_ids[0] in (
|
||||||
|
self.start_token_id, self.end_token_id
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
start_in_prev = self.start_token_id in previous_token_ids
|
||||||
|
start_in_delta = self.start_token_id in delta_token_ids
|
||||||
|
end_in_prev = self.end_token_id in previous_token_ids
|
||||||
|
end_in_delta = self.end_token_id in delta_token_ids
|
||||||
|
|
||||||
|
if start_in_prev:
|
||||||
|
if end_in_delta:
|
||||||
|
end_idx = delta_text.find(self.end_token)
|
||||||
|
reasoning = delta_text[:end_idx] if end_idx >= 0 else ""
|
||||||
|
content = delta_text[end_idx + len(self.end_token):] if end_idx >= 0 else None
|
||||||
|
return _DeltaMessage(
|
||||||
|
reasoning_content=reasoning or None,
|
||||||
|
content=content or None,
|
||||||
|
)
|
||||||
|
elif end_in_prev:
|
||||||
|
return _DeltaMessage(content=delta_text)
|
||||||
|
else:
|
||||||
|
return _DeltaMessage(reasoning_content=delta_text)
|
||||||
|
|
||||||
|
elif start_in_delta:
|
||||||
|
if end_in_delta:
|
||||||
|
start_idx = delta_text.find(self.start_token)
|
||||||
|
end_idx = delta_text.find(self.end_token)
|
||||||
|
reasoning = delta_text[start_idx + len(self.start_token):end_idx]
|
||||||
|
content = delta_text[end_idx + len(self.end_token):]
|
||||||
|
return _DeltaMessage(
|
||||||
|
reasoning_content=reasoning or None,
|
||||||
|
content=content or None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return _DeltaMessage(reasoning_content=delta_text)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return _DeltaMessage(content=delta_text)
|
||||||
|
|
||||||
|
|
||||||
|
class ReasoningParserManager:
|
||||||
|
"""
|
||||||
|
Registry for ReasoningParser implementations.
|
||||||
|
Supports eager and lazy registration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_parsers: dict = {} # name -> class (eager)
|
||||||
|
_lazy: dict = {} # name -> (module_path, class_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_module(cls, name: str, parser_cls: type) -> None:
|
||||||
|
"""Eagerly register a ReasoningParser class."""
|
||||||
|
if not issubclass(parser_cls, ReasoningParser):
|
||||||
|
raise TypeError(f"{parser_cls} is not a ReasoningParser subclass.")
|
||||||
|
cls._parsers[name] = parser_cls
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_lazy(cls, name: str, module_path: str, class_name: str) -> None:
|
||||||
|
"""Register a parser for deferred import."""
|
||||||
|
cls._lazy[name] = (module_path, class_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_reasoning_parser(cls, name: str) -> type:
|
||||||
|
if name in cls._parsers:
|
||||||
|
return cls._parsers[name]
|
||||||
|
if name in cls._lazy:
|
||||||
|
module_path, class_name = cls._lazy[name]
|
||||||
|
mod = importlib.import_module(module_path)
|
||||||
|
parser_cls = getattr(mod, class_name)
|
||||||
|
cls._parsers[name] = parser_cls
|
||||||
|
return parser_cls
|
||||||
|
registered = sorted(set(cls._parsers) | set(cls._lazy))
|
||||||
|
raise KeyError(
|
||||||
|
f"Reasoning parser '{name}' not found. "
|
||||||
|
f"Available: {registered}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_registered(cls) -> list:
|
||||||
|
return sorted(set(cls._parsers) | set(cls._lazy))
|
||||||
108
qwen3_6_scripts/reasoning/qwen3_reasoning_parser.py
Normal file
108
qwen3_6_scripts/reasoning/qwen3_reasoning_parser.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Reasoning parser for Qwen3 / Qwen3.5 / Qwen3.6 model family.
|
||||||
|
Adapted from vllm-original/vllm/reasoning/qwen3_reasoning_parser.py.
|
||||||
|
|
||||||
|
The model uses <think>...</think> to wrap chain-of-thought output.
|
||||||
|
For Qwen3.5+ the chat template injects <think> into the prompt, so only
|
||||||
|
</think> appears in the generated tokens; older templates generate <think>
|
||||||
|
themselves. Both styles are handled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Sequence, Any
|
||||||
|
|
||||||
|
from vllm.reasoning.abs_reasoning_parsers import (
|
||||||
|
BaseThinkingReasoningParser,
|
||||||
|
ReasoningParserManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Qwen3ReasoningParser(BaseThinkingReasoningParser):
|
||||||
|
|
||||||
|
def __init__(self, tokenizer: Any, *args, **kwargs):
|
||||||
|
super().__init__(tokenizer, *args, **kwargs)
|
||||||
|
chat_kwargs = kwargs.get("chat_template_kwargs", {}) or {}
|
||||||
|
self.thinking_enabled = chat_kwargs.get("enable_thinking", True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_token(self) -> str:
|
||||||
|
return "<think>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end_token(self) -> str:
|
||||||
|
return "</think>"
|
||||||
|
|
||||||
|
def extract_reasoning(
|
||||||
|
self, model_output: str, request: Any
|
||||||
|
) -> "tuple[Optional[str], Optional[str]]":
|
||||||
|
# Strip <think> if the model generated it (old template / edge case).
|
||||||
|
parts = model_output.partition(self.start_token)
|
||||||
|
model_output = parts[2] if parts[1] else parts[0]
|
||||||
|
|
||||||
|
if self.end_token not in model_output:
|
||||||
|
if not self.thinking_enabled:
|
||||||
|
return None, model_output
|
||||||
|
# Thinking enabled but output truncated before </think>.
|
||||||
|
return model_output, None
|
||||||
|
|
||||||
|
reasoning, _, content = model_output.partition(self.end_token)
|
||||||
|
return reasoning, content or None
|
||||||
|
|
||||||
|
def count_reasoning_tokens(self, token_ids: Sequence[int]) -> int:
|
||||||
|
token_ids = list(token_ids)
|
||||||
|
if self.start_token_id in token_ids:
|
||||||
|
# Old-style template: model generates <think> itself.
|
||||||
|
# Use depth-counting from the base class.
|
||||||
|
return super().count_reasoning_tokens(token_ids)
|
||||||
|
elif self.end_token_id in token_ids:
|
||||||
|
# New-style template (Qwen3.5+): <think> is injected into the
|
||||||
|
# prompt, so output starts already inside the thinking block.
|
||||||
|
# Every token before </think> is a reasoning token.
|
||||||
|
return token_ids.index(self.end_token_id)
|
||||||
|
else:
|
||||||
|
# No </think> in output: either truncated (all reasoning)
|
||||||
|
# or thinking disabled (none).
|
||||||
|
return len(token_ids) if self.thinking_enabled else 0
|
||||||
|
|
||||||
|
def extract_reasoning_streaming(
|
||||||
|
self,
|
||||||
|
previous_text: str,
|
||||||
|
current_text: str,
|
||||||
|
delta_text: str,
|
||||||
|
previous_token_ids: Sequence[int],
|
||||||
|
current_token_ids: Sequence[int],
|
||||||
|
delta_token_ids: Sequence[int],
|
||||||
|
):
|
||||||
|
from vllm.entrypoints.openai.protocol import DeltaMessage
|
||||||
|
|
||||||
|
if not self.thinking_enabled:
|
||||||
|
return DeltaMessage(content=delta_text) if delta_text else None
|
||||||
|
|
||||||
|
# Strip <think> from delta if the model generates it itself.
|
||||||
|
if self.start_token_id in delta_token_ids:
|
||||||
|
start_idx = delta_text.find(self.start_token)
|
||||||
|
if start_idx >= 0:
|
||||||
|
delta_text = delta_text[start_idx + len(self.start_token):]
|
||||||
|
|
||||||
|
if self.end_token_id in delta_token_ids:
|
||||||
|
end_idx = delta_text.find(self.end_token)
|
||||||
|
if end_idx >= 0:
|
||||||
|
reasoning = delta_text[:end_idx]
|
||||||
|
content = delta_text[end_idx + len(self.end_token):]
|
||||||
|
if not reasoning and not content:
|
||||||
|
return None
|
||||||
|
return DeltaMessage(
|
||||||
|
reasoning_content=reasoning or None,
|
||||||
|
content=content or None,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not delta_text:
|
||||||
|
return None
|
||||||
|
elif self.end_token_id in previous_token_ids:
|
||||||
|
return DeltaMessage(content=delta_text)
|
||||||
|
else:
|
||||||
|
return DeltaMessage(reasoning_content=delta_text)
|
||||||
|
|
||||||
|
|
||||||
|
# Register immediately when this module is imported.
|
||||||
|
ReasoningParserManager.register_module("qwen3", Qwen3ReasoningParser)
|
||||||
1656
qwen3_6_scripts/scheduler.py
Normal file
1656
qwen3_6_scripts/scheduler.py
Normal file
File diff suppressed because it is too large
Load Diff
1386
qwen3_6_scripts/sequence.py
Normal file
1386
qwen3_6_scripts/sequence.py
Normal file
File diff suppressed because it is too large
Load Diff
1090
qwen3_6_scripts/serving_chat.py
Normal file
1090
qwen3_6_scripts/serving_chat.py
Normal file
File diff suppressed because it is too large
Load Diff
36
vllm/__init__.py
Normal file
36
vllm/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""vLLM: a high-throughput and memory-efficient inference engine for LLMs"""
|
||||||
|
|
||||||
|
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 (CompletionOutput, EmbeddingOutput,
|
||||||
|
EmbeddingRequestOutput, RequestOutput)
|
||||||
|
from vllm.pooling_params import PoolingParams
|
||||||
|
from vllm.sampling_params import SamplingParams
|
||||||
|
|
||||||
|
from .version import __version__, __version_tuple__
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"__version_tuple__",
|
||||||
|
"LLM",
|
||||||
|
"ModelRegistry",
|
||||||
|
"PromptType",
|
||||||
|
"TextPrompt",
|
||||||
|
"TokensPrompt",
|
||||||
|
"SamplingParams",
|
||||||
|
"RequestOutput",
|
||||||
|
"CompletionOutput",
|
||||||
|
"EmbeddingOutput",
|
||||||
|
"EmbeddingRequestOutput",
|
||||||
|
"LLMEngine",
|
||||||
|
"EngineArgs",
|
||||||
|
"AsyncLLMEngine",
|
||||||
|
"AsyncEngineArgs",
|
||||||
|
"initialize_ray_cluster",
|
||||||
|
"PoolingParams",
|
||||||
|
]
|
||||||
BIN
vllm/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/_core_ext.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/_core_ext.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/_custom_ops.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/_custom_ops.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/_ipex_ops.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/_ipex_ops.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/beam_search.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/beam_search.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/block.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/block.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/config.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/config.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/connections.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/connections.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/envs.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/envs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/forward_context.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/forward_context.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/logger.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/logger.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/outputs.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/outputs.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/pooling_params.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/pooling_params.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/sampling_params.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/sampling_params.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/scalar_type.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/scalar_type.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/scripts.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/scripts.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/sequence.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/sequence.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/tracing.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/tracing.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/utils.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/__pycache__/version.cpython-310.pyc
Normal file
BIN
vllm/__pycache__/version.cpython-310.pyc
Normal file
Binary file not shown.
278
vllm/_core_ext.py
Normal file
278
vllm/_core_ext.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import importlib.util
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.logger import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
core_C_available = importlib.util.find_spec('._core_C', 'vllm') is not None
|
||||||
|
|
||||||
|
|
||||||
|
# Mirrors enum in `core/scalar_type.hpp`
|
||||||
|
class NanRepr(Enum):
|
||||||
|
NONE = 0 # nans are not supported
|
||||||
|
IEEE_754 = 1 # nans are: Exp all 1s, mantissa not all 0s
|
||||||
|
EXTD_RANGE_MAX_MIN = 2 # nans are: Exp all 1s, mantissa all 1s
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING or not core_C_available:
|
||||||
|
# On platforms were we cannot use/build the C++ core extension (i.e. namely
|
||||||
|
# neuron and tpu), we define the mock ScalarType class here that partially
|
||||||
|
# mimics the C++ ScalarType class.
|
||||||
|
#
|
||||||
|
# We also use this provide type signatures to the Python LSP for the methods
|
||||||
|
# in the C++ ScalarType class. So these type signatures should be kept
|
||||||
|
# in sync with csrc/core/scalar_type.hpp
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScalarType:
|
||||||
|
"""
|
||||||
|
ScalarType can represent a wide range of floating point and integer
|
||||||
|
types, in particular it can be used to represent sub-byte data types
|
||||||
|
(something that torch.dtype currently does not support). It is also
|
||||||
|
capable of representing types with a bias, i.e.:
|
||||||
|
`stored_value = value + bias`,
|
||||||
|
this is useful for quantized types (e.g. standard GPTQ 4bit uses a bias
|
||||||
|
of 8). The implementation for this class can be found in
|
||||||
|
csrc/core/scalar_type.hpp, these type signatures should be kept in sync
|
||||||
|
with that file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
exponent: int
|
||||||
|
"""
|
||||||
|
Number of bits in the exponent if this is a floating point type
|
||||||
|
(zero if this an integer type)
|
||||||
|
"""
|
||||||
|
|
||||||
|
mantissa: int
|
||||||
|
"""
|
||||||
|
Number of bits in the mantissa if this is a floating point type,
|
||||||
|
or the number bits representing an integer excluding the sign bit if
|
||||||
|
this an integer type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bias: int
|
||||||
|
"""
|
||||||
|
bias used to encode the values in this scalar type
|
||||||
|
(value = stored_value - bias, default 0) for example if we store the
|
||||||
|
type as an unsigned integer with a bias of 128 then the value 0 will be
|
||||||
|
stored as 128 and -1 will be stored as 127 and 1 will be stored as 129.
|
||||||
|
"""
|
||||||
|
|
||||||
|
signed: bool
|
||||||
|
"If the type is signed (i.e. has a sign bit)"
|
||||||
|
|
||||||
|
_finite_values_only: bool = False
|
||||||
|
"""
|
||||||
|
Private: if NANs are supported, used `has_infs()` instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
nan_repr: int = NanRepr.IEEE_754.value
|
||||||
|
"""
|
||||||
|
How NaNs are represent in this scalar type, returns NanRepr value.
|
||||||
|
(not applicable for integer types)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_bits(self):
|
||||||
|
return self.exponent + self.mantissa + int(self.signed)
|
||||||
|
|
||||||
|
def min(self) -> Union[int, float]:
|
||||||
|
"""
|
||||||
|
Min representable value for this scalar type.
|
||||||
|
(accounting for bias if there is one)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def max(self) -> Union[int, float]:
|
||||||
|
"""
|
||||||
|
Max representable value for this scalar type.
|
||||||
|
(accounting for bias if there is one)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def is_signed(self) -> bool:
|
||||||
|
"""
|
||||||
|
If the type is signed (i.e. has a sign bit), same as `signed`
|
||||||
|
added for consistency with:
|
||||||
|
https://pytorch.org/docs/stable/generated/torch.Tensor.is_signed.html
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def is_floating_point(self) -> bool:
|
||||||
|
"If the type is a floating point type"
|
||||||
|
return self.exponent != 0
|
||||||
|
|
||||||
|
def is_integer(self) -> bool:
|
||||||
|
"If the type is an integer type"
|
||||||
|
return self.exponent == 0
|
||||||
|
|
||||||
|
def has_bias(self) -> bool:
|
||||||
|
"If the type has a non-zero bias"
|
||||||
|
return self.bias != 0
|
||||||
|
|
||||||
|
def has_infs(self) -> bool:
|
||||||
|
"If the type is floating point and supports infinity"
|
||||||
|
return not self._finite_values_only
|
||||||
|
|
||||||
|
def has_nans(self) -> bool:
|
||||||
|
return self.nan_repr != NanRepr.NONE.value
|
||||||
|
|
||||||
|
def is_ieee_754(self) -> bool:
|
||||||
|
"""
|
||||||
|
If the type is a floating point type that follows IEEE 754
|
||||||
|
conventions
|
||||||
|
"""
|
||||||
|
return self.nan_repr == NanRepr.IEEE_754.value and \
|
||||||
|
not self._finite_values_only
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# __len__ needs to be defined (and has to throw TypeError) for pytorch's
|
||||||
|
# opcheck to work.
|
||||||
|
def __len__(self) -> int:
|
||||||
|
raise TypeError
|
||||||
|
|
||||||
|
#
|
||||||
|
# Convenience Constructors
|
||||||
|
#
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def int_(cls, size_bits: int, bias: Optional[int]) -> 'ScalarType':
|
||||||
|
"Create a signed integer scalar type (size_bits includes sign-bit)."
|
||||||
|
return cls(size_bits - 1, size_bits, bias if bias else 0, True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def uint(cls, size_bits: int, bias: Optional[int]) -> 'ScalarType':
|
||||||
|
"""Create a unsigned integer scalar type."""
|
||||||
|
return cls(size_bits, size_bits, bias if bias else 0, False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def float_IEEE754(cls, exponent: int, mantissa: int) -> 'ScalarType':
|
||||||
|
"""
|
||||||
|
Create a standard floating point type
|
||||||
|
(i.e. follows IEEE 754 conventions).
|
||||||
|
"""
|
||||||
|
return cls(exponent, mantissa, 0, True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def float_(cls, exponent: int, mantissa: int, finite_values_only: bool,
|
||||||
|
nan_repr: int) -> 'ScalarType':
|
||||||
|
"""
|
||||||
|
Create a non-standard floating point type
|
||||||
|
(i.e. does not follow IEEE 754 conventions).
|
||||||
|
"""
|
||||||
|
return cls(exponent, mantissa, 0, True, finite_values_only,
|
||||||
|
nan_repr)
|
||||||
|
|
||||||
|
elif core_C_available:
|
||||||
|
try:
|
||||||
|
import vllm._core_C # noqa: F401
|
||||||
|
except ImportError as e:
|
||||||
|
logger.warning("Failed to import from vllm._core_C with %r", e)
|
||||||
|
|
||||||
|
ScalarType = torch.classes._core_C.ScalarType
|
||||||
|
|
||||||
|
if (hasattr(torch, "_library")
|
||||||
|
and hasattr(torch._library, "register_fake_class")):
|
||||||
|
# Needed for dynamo support of ScalarType.
|
||||||
|
@torch._library.register_fake_class("_core_C::ScalarType")
|
||||||
|
class FakeScalarType:
|
||||||
|
|
||||||
|
def __init__(self, scalar_type):
|
||||||
|
self.ScalarType = scalar_type
|
||||||
|
|
||||||
|
def bias_getter(self) -> int:
|
||||||
|
return self.ScalarType.bias
|
||||||
|
|
||||||
|
def exponent_getter(self) -> int:
|
||||||
|
return self.ScalarType.exponent
|
||||||
|
|
||||||
|
def mantissa_getter(self) -> int:
|
||||||
|
return self.ScalarType.mantissa
|
||||||
|
|
||||||
|
def signed_getter(self) -> bool:
|
||||||
|
return self.ScalarType.signed
|
||||||
|
|
||||||
|
def size_bits_getter(self) -> int:
|
||||||
|
return self.ScalarType.size_bits
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size_bits(self) -> int:
|
||||||
|
return self.ScalarType.size_bits
|
||||||
|
|
||||||
|
def min(self) -> Union[int, float]:
|
||||||
|
return self.ScalarType.min()
|
||||||
|
|
||||||
|
def max(self) -> Union[int, float]:
|
||||||
|
return self.ScalarType.max()
|
||||||
|
|
||||||
|
def is_signed(self) -> bool:
|
||||||
|
return self.ScalarType.is_signed()
|
||||||
|
|
||||||
|
def is_floating_point(self) -> bool:
|
||||||
|
return self.ScalarType.is_floating_point()
|
||||||
|
|
||||||
|
def is_integer(self) -> bool:
|
||||||
|
return self.ScalarType.is_integer()
|
||||||
|
|
||||||
|
def has_bias(self) -> bool:
|
||||||
|
return self.ScalarType.has_bias()
|
||||||
|
|
||||||
|
def has_infs(self) -> bool:
|
||||||
|
return self.ScalarType.has_infs()
|
||||||
|
|
||||||
|
def has_nans(self) -> bool:
|
||||||
|
return self.ScalarType.has_nans()
|
||||||
|
|
||||||
|
def is_ieee_754(self) -> bool:
|
||||||
|
return self.ScalarType.is_ieee_754()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.ScalarType.__str__()
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.ScalarType.__repr__()
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return self.ScalarType.__len__()
|
||||||
|
|
||||||
|
def __obj_flatten__(self) -> Tuple[Tuple[str, Any], ...]:
|
||||||
|
return torch.classes._core_C.ScalarType.__obj_flatten__(
|
||||||
|
self.ScalarType)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __obj_unflatten__(
|
||||||
|
cls, flat_type: Tuple[Tuple[str, Any],
|
||||||
|
...]) -> 'ScalarType':
|
||||||
|
return cls(
|
||||||
|
torch.classes._core_C.ScalarType.__obj_unflatten__(
|
||||||
|
flat_type))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def int_(cls, size_bits: int, bias: Optional[int]) -> 'ScalarType':
|
||||||
|
return ScalarType.int_(size_bits, bias)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def uint(cls, size_bits: int, bias: Optional[int]) -> 'ScalarType':
|
||||||
|
return ScalarType.uint(size_bits, bias)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def float_IEEE754(cls, exponent: int,
|
||||||
|
mantissa: int) -> 'ScalarType':
|
||||||
|
return ScalarType.float_IEEE754(exponent, mantissa)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def float_(cls, exponent: int, mantissa: int,
|
||||||
|
finite_values_only: bool,
|
||||||
|
nan_repr: int) -> 'ScalarType':
|
||||||
|
return ScalarType.float_(exponent, mantissa,
|
||||||
|
finite_values_only, nan_repr)
|
||||||
1105
vllm/_custom_ops.py
Normal file
1105
vllm/_custom_ops.py
Normal file
File diff suppressed because it is too large
Load Diff
243
vllm/_ipex_ops.py
Normal file
243
vllm/_ipex_ops.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
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
|
||||||
|
head_mapping = torch.arange(
|
||||||
|
0,
|
||||||
|
num_kv_heads,
|
||||||
|
device=query.device,
|
||||||
|
dtype=torch.int32,
|
||||||
|
).view(num_kv_heads,
|
||||||
|
1).repeat_interleave(num_queries_per_tokens).flatten()
|
||||||
|
# todo: ipex will refactor namespace
|
||||||
|
torch.xpu.paged_attention_v1( # type: ignore
|
||||||
|
out,
|
||||||
|
query.contiguous(),
|
||||||
|
key_cache.view_as(value_cache),
|
||||||
|
value_cache,
|
||||||
|
head_mapping,
|
||||||
|
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
|
||||||
|
head_mapping = torch.arange(
|
||||||
|
0,
|
||||||
|
num_kv_heads,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=query.device,
|
||||||
|
).view(num_kv_heads,
|
||||||
|
1).repeat_interleave(num_queries_per_tokens).flatten()
|
||||||
|
# todo: ipex will refactor namespace
|
||||||
|
torch.xpu.paged_attention_v2( # type: ignore
|
||||||
|
out,
|
||||||
|
exp_sum,
|
||||||
|
max_logits,
|
||||||
|
tmp_out,
|
||||||
|
query.contiguous(),
|
||||||
|
key_cache.view_as(value_cache),
|
||||||
|
value_cache,
|
||||||
|
head_mapping,
|
||||||
|
block_tables,
|
||||||
|
context_lens,
|
||||||
|
scale,
|
||||||
|
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,
|
||||||
|
) -> None:
|
||||||
|
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_)
|
||||||
|
|
||||||
|
@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
BIN
vllm/adapter_commons/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/adapter_commons/__pycache__/layers.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/layers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/adapter_commons/__pycache__/models.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/adapter_commons/__pycache__/request.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/request.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/adapter_commons/__pycache__/utils.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/adapter_commons/__pycache__/worker_manager.cpython-310.pyc
Normal file
BIN
vllm/adapter_commons/__pycache__/worker_manager.cpython-310.pyc
Normal file
Binary file not shown.
14
vllm/adapter_commons/layers.py
Normal file
14
vllm/adapter_commons/layers.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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)
|
||||||
104
vllm/adapter_commons/models.py
Normal file
104
vllm/adapter_commons/models.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Callable, Dict, Hashable, 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[T]):
|
||||||
|
|
||||||
|
def __init__(self, capacity: int, deactivate_fn: Callable[[Hashable],
|
||||||
|
None]):
|
||||||
|
super().__init__(capacity)
|
||||||
|
self.deactivate_fn = deactivate_fn
|
||||||
|
|
||||||
|
def _on_remove(self, key: Hashable, 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
|
||||||
23
vllm/adapter_commons/request.py
Normal file
23
vllm/adapter_commons/request.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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)
|
||||||
90
vllm/adapter_commons/utils.py
Normal file
90
vllm/adapter_commons/utils.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
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())
|
||||||
36
vllm/adapter_commons/worker_manager.py
Normal file
36
vllm/adapter_commons/worker_manager.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
BIN
vllm/assets/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
vllm/assets/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/assets/__pycache__/audio.cpython-310.pyc
Normal file
BIN
vllm/assets/__pycache__/audio.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/assets/__pycache__/base.cpython-310.pyc
Normal file
BIN
vllm/assets/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/assets/__pycache__/image.cpython-310.pyc
Normal file
BIN
vllm/assets/__pycache__/image.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/assets/__pycache__/video.cpython-310.pyc
Normal file
BIN
vllm/assets/__pycache__/video.cpython-310.pyc
Normal file
Binary file not shown.
28
vllm/assets/audio.py
Normal file
28
vllm/assets/audio.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal, Tuple
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import librosa
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from vllm.assets.base import get_vllm_public_assets, vLLM_S3_BUCKET_URL
|
||||||
|
|
||||||
|
ASSET_DIR = "multimodal_asset"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AudioAsset:
|
||||||
|
name: Literal["winning_call", "mary_had_lamb"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def audio_and_sample_rate(self) -> Tuple[np.ndarray, int]:
|
||||||
|
|
||||||
|
audio_path = get_vllm_public_assets(filename=f"{self.name}.ogg",
|
||||||
|
s3_prefix=ASSET_DIR)
|
||||||
|
y, sr = librosa.load(audio_path, sr=None)
|
||||||
|
assert isinstance(sr, int)
|
||||||
|
return y, sr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return urljoin(vLLM_S3_BUCKET_URL, f"{ASSET_DIR}/{self.name}.ogg")
|
||||||
39
vllm/assets/base.py
Normal file
39
vllm/assets/base.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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
|
||||||
|
from vllm.envs import VLLM_IMAGE_FETCH_TIMEOUT
|
||||||
|
|
||||||
|
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=VLLM_IMAGE_FETCH_TIMEOUT)
|
||||||
|
|
||||||
|
return asset_path
|
||||||
30
vllm/assets/image.py
Normal file
30
vllm/assets/image.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from vllm.assets.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, weights_only=True)
|
||||||
85
vllm/assets/video.py
Normal file
85
vllm/assets/video.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import List, Literal
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
from huggingface_hub import hf_hub_download
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from vllm.multimodal.utils import (sample_frames_from_video,
|
||||||
|
try_import_video_packages)
|
||||||
|
|
||||||
|
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-eample-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:
|
||||||
|
cv2 = try_import_video_packages()
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
for i in range(total_frames):
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if ret:
|
||||||
|
frames.append(frame)
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
frames = np.stack(frames)
|
||||||
|
frames = sample_frames_from_video(frames, num_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]:
|
||||||
|
cv2 = try_import_video_packages()
|
||||||
|
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
|
||||||
17
vllm/attention/__init__.py
Normal file
17
vllm/attention/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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",
|
||||||
|
]
|
||||||
BIN
vllm/attention/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
vllm/attention/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/__pycache__/layer.cpython-310.pyc
Normal file
BIN
vllm/attention/__pycache__/layer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/__pycache__/selector.cpython-310.pyc
Normal file
BIN
vllm/attention/__pycache__/selector.cpython-310.pyc
Normal file
Binary file not shown.
0
vllm/attention/backends/__init__.py
Normal file
0
vllm/attention/backends/__init__.py
Normal file
BIN
vllm/attention/backends/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/abstract.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/abstract.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/flash_attn.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/flash_attn.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/flashinfer.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/flashinfer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/ipex_attn.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/ipex_attn.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/openvino.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/openvino.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/pallas.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/pallas.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/torch_sdpa.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/torch_sdpa.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/utils.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
vllm/attention/backends/__pycache__/xformers.cpython-310.pyc
Normal file
BIN
vllm/attention/backends/__pycache__/xformers.cpython-310.pyc
Normal file
Binary file not shown.
232
vllm/attention/backends/abstract.py
Normal file
232
vllm/attention/backends/abstract.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import (TYPE_CHECKING, Any, Dict, Generic, List, Optional, Set,
|
||||||
|
Tuple, Type, TypeVar)
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner_base import (ModelRunnerBase,
|
||||||
|
ModelRunnerInputBase,
|
||||||
|
ModelRunnerInputBuilderBase)
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionType(Enum):
|
||||||
|
DECODER = auto() # Decoder attention between previous layer Q/K/V
|
||||||
|
ENCODER = auto() # Encoder attention between previous layer Q/K/V
|
||||||
|
ENCODER_DECODER = auto() # Attention between dec. Q and enc. K/V
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionBackend(ABC):
|
||||||
|
"""Abstract class for attention backends."""
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_metadata_builder(cls, *args,
|
||||||
|
**kwargs) -> "AttentionMetadataBuilder":
|
||||||
|
return cls.get_builder_cls()(*args, **kwargs)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@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:
|
||||||
|
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 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,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: T,
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
raise NotImplementedError
|
||||||
444
vllm/attention/backends/blocksparse_attn.py
Normal file
444
vllm/attention/backends/blocksparse_attn.py
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
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_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],
|
||||||
|
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:],
|
||||||
|
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,
|
||||||
|
) -> 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
|
||||||
|
|
||||||
|
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.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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: BlocksparseFlashAttentionMetadata,
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
) -> 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]
|
||||||
|
"""
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"BlocksparseFlashAttentionImpl")
|
||||||
|
|
||||||
|
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,
|
||||||
|
k_scale,
|
||||||
|
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,
|
||||||
|
k_scale,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reshape the output tensor.
|
||||||
|
return output.view(num_tokens, hidden_size)
|
||||||
951
vllm/attention/backends/flash_attn.py
Normal file
951
vllm/attention/backends/flash_attn.py
Normal file
@@ -0,0 +1,951 @@
|
|||||||
|
|
||||||
|
"""Attention layer with FlashAttention."""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionType)
|
||||||
|
from vllm.attention.backends.utils import (PAD_SLOT_ID, CommonAttentionState,
|
||||||
|
compute_slot_mapping,
|
||||||
|
compute_slot_mapping_start_idx,
|
||||||
|
is_block_tables_empty)
|
||||||
|
from vllm.forward_context import get_forward_context
|
||||||
|
from vllm.utils import async_tensor_h2d, make_tensor_with_pad
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import (ModelInputForGPUBuilder,
|
||||||
|
ModelInputForGPUWithSamplingMetadata)
|
||||||
|
|
||||||
|
from ixformer.contrib.vllm_flash_attn import flash_attn_varlen_func as _flash_attn_varlen_func
|
||||||
|
from ixformer.contrib.vllm_flash_attn import flash_attn_with_kvcache as _flash_attn_with_kvcache
|
||||||
|
|
||||||
|
|
||||||
|
def flash_attn_varlen_func(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k: torch.Tensor,
|
||||||
|
v: torch.Tensor,
|
||||||
|
cu_seqlens_q: torch.Tensor,
|
||||||
|
cu_seqlens_k: torch.Tensor,
|
||||||
|
max_seqlen_q: int,
|
||||||
|
max_seqlen_k: int,
|
||||||
|
softmax_scale: Optional[float] = None,
|
||||||
|
causal: bool = False,
|
||||||
|
window_size: Optional[List[int]] = None,
|
||||||
|
softcap: float = 0.0,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
block_table: Optional[torch.Tensor] = None,
|
||||||
|
out: Optional[torch.Tensor] = None,
|
||||||
|
use_sqrt_alibi: Optional[bool] = False
|
||||||
|
) -> torch.Tensor:
|
||||||
|
# custom op does not support tuple input
|
||||||
|
real_window_size: Tuple[int, int]
|
||||||
|
if window_size is None:
|
||||||
|
real_window_size = (-1, -1)
|
||||||
|
else:
|
||||||
|
assert len(window_size) == 2
|
||||||
|
real_window_size = (window_size[0], window_size[1])
|
||||||
|
return _flash_attn_varlen_func(
|
||||||
|
q=q,
|
||||||
|
k=k,
|
||||||
|
v=v,
|
||||||
|
cu_seqlens_q=cu_seqlens_q,
|
||||||
|
cu_seqlens_k=cu_seqlens_k,
|
||||||
|
max_seqlen_q=max_seqlen_q,
|
||||||
|
max_seqlen_k=max_seqlen_k,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=causal,
|
||||||
|
window_size=real_window_size,
|
||||||
|
softcap=softcap,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
block_table=block_table,
|
||||||
|
out=out,
|
||||||
|
sqrt_alibi=use_sqrt_alibi,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def flash_attn_with_kvcache(
|
||||||
|
decode_query: torch.Tensor,
|
||||||
|
key_cache: torch.Tensor,
|
||||||
|
value_cache: torch.Tensor,
|
||||||
|
seq_lens_cpu_tensors: torch.Tensor,
|
||||||
|
max_context_len: int,
|
||||||
|
cache_seqlens: Optional[torch.Tensor] = None,
|
||||||
|
block_table: Optional[torch.Tensor] = None,
|
||||||
|
softmax_scale: Optional[float] = None,
|
||||||
|
causal: bool = False,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
softcap: float = 0.0,
|
||||||
|
out: Optional[torch.Tensor] = None,
|
||||||
|
use_sqrt_alibi: bool = False
|
||||||
|
) -> torch.Tensor:
|
||||||
|
return _flash_attn_with_kvcache(
|
||||||
|
decode_query,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
cache_seqlens=cache_seqlens,
|
||||||
|
block_table=block_table,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=causal,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=softcap,
|
||||||
|
max_context_len=max_context_len,
|
||||||
|
cache_seqlens_cpu=seq_lens_cpu_tensors,
|
||||||
|
out=out,
|
||||||
|
use_sqrt_alibi=use_sqrt_alibi
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reshape_and_cache_flash(
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
slot_mapping: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
) -> None:
|
||||||
|
"""Inductor cannot deal with inplace operations on views.
|
||||||
|
See https://github.com/pytorch/pytorch/issues/131192
|
||||||
|
and https://github.com/pytorch/pytorch/issues/130174
|
||||||
|
This is a workaround to hide the view operation from the inductor.
|
||||||
|
"""
|
||||||
|
return ops.reshape_and_cache_flash(
|
||||||
|
key, value, kv_cache[0], kv_cache[1], slot_mapping, kv_cache_dtype,
|
||||||
|
k_scale, v_scale)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashAttentionBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [32, 64, 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 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]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
_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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# (batch_size + 1,). The cumulative sequence lengths of the sequences in
|
||||||
|
# the batch, used to index into sequence on encoder-decoder
|
||||||
|
encoder_seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# Our impl need info fields...
|
||||||
|
seq_lens_cpu_tensors: Optional[torch.Tensor] = None
|
||||||
|
encoder_seq_lens_cpu_tensor: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
@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
|
||||||
|
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 = FlashAttentionMetadata(
|
||||||
|
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],
|
||||||
|
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_query_len=0,
|
||||||
|
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,
|
||||||
|
# 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.block_tables is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
|
||||||
|
seq_lens_cpu_tensors = torch.tensor(self.seq_lens[self.num_prefills:],dtype=torch.int32,device="cpu")
|
||||||
|
encoder_seq_lens_cpu_tensor = torch.tensor(self.encoder_seq_lens,dtype=torch.int32,device="cpu") if self.encoder_seq_lens is not None else None
|
||||||
|
max_seq_len = self.seq_lens_tensor.max().item()
|
||||||
|
|
||||||
|
self._cached_decode_metadata = FlashAttentionMetadata(
|
||||||
|
num_prefills=0,
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=self.num_decode_tokens,
|
||||||
|
slot_mapping=self.slot_mapping[self.num_prefill_tokens:],
|
||||||
|
seq_lens=None,
|
||||||
|
seq_lens_tensor=self.seq_lens_tensor[self.num_prefills:],
|
||||||
|
max_decode_query_len=self.max_decode_query_len,
|
||||||
|
max_query_len=max_seq_len,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
max_decode_seq_len=self.max_decode_seq_len,
|
||||||
|
query_start_loc=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=self.block_tables[self.num_prefills:],
|
||||||
|
use_cuda_graph=self.use_cuda_graph,
|
||||||
|
seq_lens_cpu_tensors=seq_lens_cpu_tensors,
|
||||||
|
# Begin encoder & cross attn fields below...
|
||||||
|
encoder_seq_lens=self.encoder_seq_lens,
|
||||||
|
encoder_seq_lens_tensor=self.encoder_seq_lens_tensor,
|
||||||
|
encoder_seq_lens_cpu_tensor=encoder_seq_lens_cpu_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_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.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.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.num_decode_tokens = 0
|
||||||
|
self.has_prefix_cache_hit = False
|
||||||
|
|
||||||
|
self.input_builder = input_builder
|
||||||
|
self.runner = input_builder.runner
|
||||||
|
self.sliding_window = input_builder.sliding_window
|
||||||
|
self.block_size = input_builder.block_size
|
||||||
|
self.use_v2_block_manager = (
|
||||||
|
input_builder.scheduler_config.use_v2_block_manager)
|
||||||
|
|
||||||
|
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:
|
||||||
|
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,
|
||||||
|
self.use_v2_block_manager)
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
query_lens_tensor = async_tensor_h2d(query_lens, torch.long, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
slot_mapping_tensor = async_tensor_h2d(self.slot_mapping, torch.long,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
query_start_loc = torch.zeros(query_lens_tensor.shape[0] + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device)
|
||||||
|
seq_start_loc = torch.zeros(seq_lens_tensor.shape[0] + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device)
|
||||||
|
torch.cumsum(seq_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=seq_start_loc.dtype,
|
||||||
|
out=seq_start_loc[1:])
|
||||||
|
torch.cumsum(query_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=query_start_loc.dtype,
|
||||||
|
out=query_start_loc[1:])
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
seq_start_loc=seq_start_loc,
|
||||||
|
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,
|
||||||
|
use_sqrt_alibi: bool = None
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"FlashAttention does not support block-sparse attention yet, we will support soon")
|
||||||
|
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.use_sqrt_alibi = use_sqrt_alibi
|
||||||
|
self.sliding_window = ((sliding_window, sliding_window)
|
||||||
|
if sliding_window is not None else (-1, -1))
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
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
|
||||||
|
|
||||||
|
if sliding_window is not None:
|
||||||
|
# NOTE(woosuk): flash-attn's sliding window does not work with
|
||||||
|
# paged KV cache.
|
||||||
|
# TODO will support on next week.
|
||||||
|
self.sliding_window = None
|
||||||
|
|
||||||
|
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}.")
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: FlashAttentionMetadata,
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
) -> 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]
|
||||||
|
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]
|
||||||
|
"""
|
||||||
|
# NOTE(woosuk): FlashAttention does not support FP8 KV cache.
|
||||||
|
assert k_scale == 1.0 and v_scale == 1.0, (
|
||||||
|
"key/v_scale is not supported in FlashAttention.")
|
||||||
|
|
||||||
|
output = unified_flash_attention(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
self.num_heads,
|
||||||
|
self.head_size,
|
||||||
|
self.num_kv_heads,
|
||||||
|
kv_cache,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
self.scale,
|
||||||
|
self.sliding_window,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.logits_soft_cap,
|
||||||
|
attn_type=attn_type,
|
||||||
|
use_sqrt_alibi=self.use_sqrt_alibi,
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def unified_flash_attention(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
softmax_scale: float,
|
||||||
|
window_size: Optional[List[int]] = None,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
use_sqrt_alibi: bool = False
|
||||||
|
) -> torch.Tensor:
|
||||||
|
|
||||||
|
current_metadata = get_forward_context()
|
||||||
|
assert current_metadata is not None
|
||||||
|
assert isinstance(current_metadata, FlashAttentionMetadata)
|
||||||
|
attn_metadata: FlashAttentionMetadata = current_metadata
|
||||||
|
|
||||||
|
# Reshape the query, key, and value tensors.
|
||||||
|
query = query.view(-1, num_heads, head_size)
|
||||||
|
if key is not None:
|
||||||
|
assert value is not None
|
||||||
|
key = key.view(-1, num_kv_heads, head_size)
|
||||||
|
value = value.view(-1, num_kv_heads, head_size)
|
||||||
|
else:
|
||||||
|
assert value is None
|
||||||
|
|
||||||
|
if attn_type != AttentionType.ENCODER and kv_cache.numel() > 0:
|
||||||
|
key_cache = kv_cache[0]
|
||||||
|
value_cache = kv_cache[1]
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
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.flatten()
|
||||||
|
else:
|
||||||
|
# Update self-attention KV cache (prefill/decode)
|
||||||
|
updated_slot_mapping = attn_metadata.slot_mapping.flatten()
|
||||||
|
ops.reshape_and_cache_flash(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
updated_slot_mapping,
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
if attn_type == AttentionType.ENCODER:
|
||||||
|
# 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_encoder_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
num_decode_tokens = 0
|
||||||
|
elif attn_type == AttentionType.DECODER:
|
||||||
|
# Decoder self-attention supports chunked prefill.
|
||||||
|
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_encoder_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||||
|
assert key.shape[0] == num_prefill_tokens + num_decode_tokens, \
|
||||||
|
f"key : {key.shape} : #prefill tokens {num_prefill_tokens} : #decode tokens {num_decode_tokens}" # noqa
|
||||||
|
assert value.shape[0] == num_prefill_tokens + num_decode_tokens, \
|
||||||
|
f"value : {value.shape} : #prefill toks {num_prefill_tokens} : #decode toks {num_decode_tokens}" # noqa
|
||||||
|
else: # attn_type == AttentionType.ENCODER_DECODER
|
||||||
|
# Encoder/decoder cross-attention requires no chunked
|
||||||
|
# prefill (100% prefill or 100% decode tokens, no mix)
|
||||||
|
num_prefill_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
if attn_metadata.num_encoder_tokens is not None:
|
||||||
|
num_encoder_tokens = attn_metadata.num_encoder_tokens
|
||||||
|
else:
|
||||||
|
num_encoder_tokens = attn_metadata.num_prefill_tokens
|
||||||
|
num_decode_tokens = attn_metadata.num_decode_tokens
|
||||||
|
|
||||||
|
# Query for decode. KV is not needed because it is already cached.
|
||||||
|
output = torch.empty_like(query)
|
||||||
|
decode_query = query[num_prefill_tokens:]
|
||||||
|
# QKV for prefill.
|
||||||
|
query = query[:num_prefill_tokens]
|
||||||
|
if key is not None and value is not None:
|
||||||
|
key = key[:num_encoder_tokens]
|
||||||
|
value = value[:num_encoder_tokens]
|
||||||
|
|
||||||
|
assert query.shape[0] == num_prefill_tokens
|
||||||
|
assert decode_query.shape[0] == num_decode_tokens
|
||||||
|
|
||||||
|
# prefill_output: Optional[torch.Tensor] = None
|
||||||
|
# decode_output: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
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.
|
||||||
|
flash_attn_varlen_func(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
cu_seqlens_q=prefill_meta.encoder_seq_start_loc if attn_type == AttentionType.ENCODER else prefill_meta.seq_start_loc,
|
||||||
|
cu_seqlens_k=prefill_meta.seq_start_loc if attn_type == AttentionType.DECODER else prefill_meta.encoder_seq_start_loc,
|
||||||
|
max_seqlen_q=prefill_meta.max_encoder_seq_len if attn_type == AttentionType.ENCODER else prefill_meta.max_prefill_seq_len,
|
||||||
|
max_seqlen_k=prefill_meta.max_prefill_seq_len if attn_type == AttentionType.DECODER else prefill_meta.max_encoder_seq_len,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=attn_type == AttentionType.DECODER,
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
out=output[:num_prefill_tokens],
|
||||||
|
use_sqrt_alibi=use_sqrt_alibi
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# prefix-enabled attention
|
||||||
|
assert prefill_meta.seq_lens is not None
|
||||||
|
max_seq_len = max(prefill_meta.seq_lens)
|
||||||
|
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=attn_type == AttentionType.DECODER,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
block_table=prefill_meta.block_tables,
|
||||||
|
softcap=logits_soft_cap,
|
||||||
|
out=output[:num_prefill_tokens],
|
||||||
|
use_sqrt_alibi=use_sqrt_alibi
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
if decode_meta.max_decode_query_len > 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,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=0.0,
|
||||||
|
block_table=decode_meta.block_tables,
|
||||||
|
out=output[num_prefill_tokens:],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Use flash_attn_with_kvcache for normal decoding.
|
||||||
|
flash_attn_with_kvcache(
|
||||||
|
decode_query.unsqueeze(1),
|
||||||
|
key_cache,
|
||||||
|
value_cache,
|
||||||
|
seq_lens_cpu_tensors=decode_meta.seq_lens_cpu_tensors if attn_type == AttentionType.DECODER else decode_meta.encoder_seq_lens_cpu_tensor,
|
||||||
|
max_context_len=decode_meta.max_query_len if attn_type == AttentionType.DECODER else decode_meta.max_encoder_seq_len,
|
||||||
|
block_table=decode_meta.block_tables if attn_type == AttentionType.DECODER else decode_meta.cross_block_tables,
|
||||||
|
cache_seqlens=decode_meta.seq_lens_tensor if attn_type == AttentionType.DECODER else decode_meta.encoder_seq_lens_tensor,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=True,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
softcap=0.0,
|
||||||
|
out=output[num_prefill_tokens:].unsqueeze(1),
|
||||||
|
use_sqrt_alibi=use_sqrt_alibi
|
||||||
|
).squeeze(1)
|
||||||
|
|
||||||
|
# TODO mv this to flash_attn_with_kvcache when supported.
|
||||||
|
if logits_soft_cap != 0.0:
|
||||||
|
output[num_prefill_tokens:] = logits_soft_cap * torch.tanh(output[num_prefill_tokens:] / logits_soft_cap)
|
||||||
|
|
||||||
|
return output.view(-1, num_heads * head_size)
|
||||||
|
|
||||||
|
|
||||||
|
# @unified_flash_attention.register_fake
|
||||||
|
def _(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
softmax_scale: float,
|
||||||
|
window_size: Optional[List[int]] = None,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
return torch.empty_like(query)
|
||||||
901
vllm/attention/backends/flashinfer.py
Normal file
901
vllm/attention/backends/flashinfer.py
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Type
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flashinfer import BatchDecodeWithPagedKVCacheWrapper
|
||||||
|
from flashinfer.decode import CUDAGraphBatchDecodeWithPagedKVCacheWrapper
|
||||||
|
from flashinfer.prefill import BatchPrefillWithPagedKVCacheWrapper
|
||||||
|
|
||||||
|
from ixformer.contrib.vllm_flash_attn import flash_attn_varlen_func
|
||||||
|
FLASHINFER_WORKSPACE_BUFFER_SIZE = 256 * 1024 * 1024
|
||||||
|
except ImportError:
|
||||||
|
BatchDecodeWithPagedKVCacheWrapper = None
|
||||||
|
CUDAGraphBatchDecodeWithPagedKVCacheWrapper = None
|
||||||
|
BatchPrefillWithPagedKVCacheWrapper = None
|
||||||
|
FLASHINFER_WORKSPACE_BUFFER_SIZE = 0
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from vllm import _custom_ops as ops
|
||||||
|
from vllm.attention.backends.abstract import (AttentionBackend, AttentionImpl,
|
||||||
|
AttentionMetadata,
|
||||||
|
AttentionMetadataBuilder,
|
||||||
|
AttentionState, AttentionType)
|
||||||
|
from vllm.attention.backends.utils import (PAD_SLOT_ID, compute_slot_mapping,
|
||||||
|
compute_slot_mapping_start_idx,
|
||||||
|
is_block_tables_empty)
|
||||||
|
from vllm.attention.ops.paged_attn import PagedAttention
|
||||||
|
from vllm.forward_context import get_forward_context
|
||||||
|
from vllm.utils import (async_tensor_h2d, get_kv_cache_torch_dtype,
|
||||||
|
make_tensor_with_pad)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.worker.model_runner import (ModelInputForGPUBuilder,
|
||||||
|
ModelInputForGPUWithSamplingMetadata)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashInferBackend(AttentionBackend):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_name() -> str:
|
||||||
|
return "flashinfer"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_impl_cls() -> Type["FlashInferImpl"]:
|
||||||
|
return FlashInferImpl
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata_cls() -> Type["AttentionMetadata"]:
|
||||||
|
return FlashInferMetadata
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_builder_cls() -> Type["FlashInferMetadataBuilder"]:
|
||||||
|
return FlashInferMetadataBuilder
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_state_cls() -> Type["FlashInferState"]:
|
||||||
|
return FlashInferState
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_kv_cache_shape(
|
||||||
|
num_blocks: int,
|
||||||
|
block_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
) -> Tuple[int, ...]:
|
||||||
|
return (num_blocks, 2, 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)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_supported_head_sizes() -> List[int]:
|
||||||
|
return [64, 128, 256]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_fp8_dtype_for_flashinfer(kv_cache_dtype: str) -> torch.dtype:
|
||||||
|
if kv_cache_dtype in ("fp8", "fp8_e4m3"):
|
||||||
|
return torch.float8_e4m3fn
|
||||||
|
elif kv_cache_dtype == "fp8_e5m2":
|
||||||
|
return torch.float8_e5m2
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unrecognized FP8 dtype: {kv_cache_dtype}")
|
||||||
|
|
||||||
|
|
||||||
|
class FlashInferState(AttentionState):
|
||||||
|
|
||||||
|
def __init__(self, runner):
|
||||||
|
self.runner = runner
|
||||||
|
self._is_graph_capturing = False
|
||||||
|
self._workspace_buffer = None
|
||||||
|
self._decode_wrapper = None
|
||||||
|
self._prefill_wrapper = None
|
||||||
|
|
||||||
|
def _get_workspace_buffer(self):
|
||||||
|
if self._workspace_buffer is None:
|
||||||
|
self._workspace_buffer = torch.empty(
|
||||||
|
FLASHINFER_WORKSPACE_BUFFER_SIZE,
|
||||||
|
dtype=torch.uint8,
|
||||||
|
device=self.runner.device)
|
||||||
|
return self._workspace_buffer
|
||||||
|
|
||||||
|
def _get_prefill_wrapper(self):
|
||||||
|
if self._prefill_wrapper is None:
|
||||||
|
self._prefill_wrapper = None
|
||||||
|
# Unsupported BatchPrefillWithPagedKVCacheWrapper Now
|
||||||
|
# self._prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(
|
||||||
|
# self._get_workspace_buffer(), "NHD")
|
||||||
|
return self._prefill_wrapper
|
||||||
|
|
||||||
|
def _get_decode_wrapper(self):
|
||||||
|
if self._decode_wrapper is None:
|
||||||
|
num_qo_heads = (self.runner.model_config.get_num_attention_heads(
|
||||||
|
self.runner.parallel_config))
|
||||||
|
num_kv_heads = self.runner.model_config.get_num_kv_heads(
|
||||||
|
self.runner.parallel_config)
|
||||||
|
use_tensor_cores = num_qo_heads // num_kv_heads > 4
|
||||||
|
self._decode_wrapper = BatchDecodeWithPagedKVCacheWrapper(
|
||||||
|
self._get_workspace_buffer(),
|
||||||
|
"NHD",
|
||||||
|
use_tensor_cores=use_tensor_cores)
|
||||||
|
return self._decode_wrapper
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def graph_capture(self, max_batch_size: int):
|
||||||
|
self._is_graph_capturing = True
|
||||||
|
self._graph_decode_wrapper = None
|
||||||
|
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)
|
||||||
|
self._graph_decode_workspace_buffer = self._get_workspace_buffer()
|
||||||
|
self._graph_indices_buffer = torch.empty(
|
||||||
|
max_batch_size * self.runner.cache_config.num_gpu_blocks,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=self.runner.device)
|
||||||
|
self._graph_indptr_buffer = torch.empty(max_batch_size + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=self.runner.device)
|
||||||
|
self._graph_last_page_len_buffer = torch.empty(
|
||||||
|
max_batch_size, dtype=torch.int32, 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
|
||||||
|
del self._graph_decode_workspace_buffer
|
||||||
|
del self._graph_indices_buffer
|
||||||
|
del self._graph_indptr_buffer
|
||||||
|
del self._graph_last_page_len_buffer
|
||||||
|
del self._graph_decode_wrapper
|
||||||
|
|
||||||
|
def graph_clone(self, batch_size: int):
|
||||||
|
assert self._is_graph_capturing
|
||||||
|
state = self.__class__(self.runner)
|
||||||
|
state._workspace_buffer = self._graph_decode_workspace_buffer
|
||||||
|
state._decode_wrapper = self._graph_decode_wrapper
|
||||||
|
state._prefill_wrapper = self._get_prefill_wrapper()
|
||||||
|
return state
|
||||||
|
|
||||||
|
def graph_capture_get_metadata_for_batch(
|
||||||
|
self, batch_size: int, is_encoder_decoder_model: bool = False):
|
||||||
|
assert self._is_graph_capturing
|
||||||
|
_indptr_buffer = self._graph_indptr_buffer[:batch_size + 1]
|
||||||
|
_last_page_len_buffer = self._graph_last_page_len_buffer[:batch_size]
|
||||||
|
|
||||||
|
num_qo_heads = (self.runner.model_config.get_num_attention_heads(
|
||||||
|
self.runner.parallel_config))
|
||||||
|
num_kv_heads = self.runner.model_config.get_num_kv_heads(
|
||||||
|
self.runner.parallel_config)
|
||||||
|
use_tensor_cores = num_qo_heads // num_kv_heads > 4
|
||||||
|
self._graph_decode_wrapper = \
|
||||||
|
CUDAGraphBatchDecodeWithPagedKVCacheWrapper(
|
||||||
|
self._graph_decode_workspace_buffer, _indptr_buffer,
|
||||||
|
self._graph_indices_buffer, _last_page_len_buffer, "NHD",
|
||||||
|
use_tensor_cores)
|
||||||
|
if self.runner.kv_cache_dtype.startswith("fp8"):
|
||||||
|
kv_cache_dtype = FlashInferBackend.get_fp8_dtype_for_flashinfer(
|
||||||
|
self.runner.kv_cache_dtype)
|
||||||
|
else:
|
||||||
|
kv_cache_dtype = get_kv_cache_torch_dtype(
|
||||||
|
self.runner.kv_cache_dtype, self.runner.model_config.dtype)
|
||||||
|
|
||||||
|
paged_kv_indptr_tensor_host = torch.arange(0,
|
||||||
|
batch_size + 1,
|
||||||
|
dtype=torch.int32)
|
||||||
|
paged_kv_indices_tensor_host = torch.arange(0,
|
||||||
|
batch_size,
|
||||||
|
dtype=torch.int32)
|
||||||
|
paged_kv_last_page_len_tensor_host = torch.full((batch_size, ),
|
||||||
|
self.runner.block_size,
|
||||||
|
dtype=torch.int32)
|
||||||
|
query_start_loc_host = torch.arange(0,
|
||||||
|
batch_size + 1,
|
||||||
|
dtype=torch.int32)
|
||||||
|
|
||||||
|
attn_metadata = self.runner.attn_backend.make_metadata(
|
||||||
|
num_prefills=0,
|
||||||
|
slot_mapping=self._graph_slot_mapping[:batch_size],
|
||||||
|
num_prefill_tokens=0,
|
||||||
|
num_decode_tokens=batch_size,
|
||||||
|
max_prefill_seq_len=0,
|
||||||
|
block_tables=self._graph_block_tables,
|
||||||
|
paged_kv_indptr=paged_kv_indptr_tensor_host,
|
||||||
|
paged_kv_indices=paged_kv_indices_tensor_host,
|
||||||
|
paged_kv_last_page_len=paged_kv_last_page_len_tensor_host,
|
||||||
|
num_qo_heads=num_qo_heads,
|
||||||
|
num_kv_heads=num_kv_heads,
|
||||||
|
head_dim=self.runner.model_config.get_head_size(),
|
||||||
|
page_size=self.runner.block_size,
|
||||||
|
seq_start_loc=None,
|
||||||
|
query_start_loc=query_start_loc_host,
|
||||||
|
device=self.runner.device,
|
||||||
|
data_type=kv_cache_dtype,
|
||||||
|
q_data_type=self.runner.model_config.dtype,
|
||||||
|
use_cuda_graph=True,
|
||||||
|
decode_wrapper=self._graph_decode_wrapper,
|
||||||
|
prefill_wrapper=None)
|
||||||
|
attn_metadata.begin_forward()
|
||||||
|
return attn_metadata
|
||||||
|
|
||||||
|
def get_graph_input_buffers(self,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False):
|
||||||
|
return {
|
||||||
|
"slot_mapping": attn_metadata.slot_mapping,
|
||||||
|
}
|
||||||
|
|
||||||
|
def prepare_graph_input_buffers(self,
|
||||||
|
input_buffers,
|
||||||
|
attn_metadata,
|
||||||
|
is_encoder_decoder_model: bool = False):
|
||||||
|
return
|
||||||
|
|
||||||
|
def begin_forward(self, model_input):
|
||||||
|
assert not self._is_graph_capturing
|
||||||
|
state = self
|
||||||
|
if model_input.attn_metadata.use_cuda_graph:
|
||||||
|
batch_size = model_input.input_tokens.shape[0]
|
||||||
|
state = (self.runner.graph_runners[model_input.virtual_engine]
|
||||||
|
[batch_size].attn_state)
|
||||||
|
model_input.attn_metadata.prefill_wrapper = state._get_prefill_wrapper(
|
||||||
|
)
|
||||||
|
model_input.attn_metadata.decode_wrapper = state._get_decode_wrapper()
|
||||||
|
model_input.attn_metadata.begin_forward()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FlashInferMetadata(AttentionMetadata):
|
||||||
|
# Maximum sequence length among prefill batch. 0 if there are decoding
|
||||||
|
# requests only.
|
||||||
|
max_prefill_seq_len: int
|
||||||
|
max_decode_seq_len: int
|
||||||
|
|
||||||
|
use_cuda_graph: bool = True
|
||||||
|
|
||||||
|
prefill_wrapper: Optional[BatchPrefillWithPagedKVCacheWrapper] = None
|
||||||
|
decode_wrapper: Optional[BatchDecodeWithPagedKVCacheWrapper] = None
|
||||||
|
|
||||||
|
# Metadata for the prefill stage
|
||||||
|
seq_start_loc: Optional[torch.Tensor] = None
|
||||||
|
query_start_loc: Optional[torch.Tensor] = None
|
||||||
|
block_tables: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# used for GPU in-place advance_step
|
||||||
|
seq_lens_tensor: Optional[torch.Tensor] = None
|
||||||
|
block_table_bound: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
# An example for paged_kv_indices, paged_kv_indptr:
|
||||||
|
# request 1, page indices [0, 5, 8]
|
||||||
|
# request 2, page indices [1, 6, 7]
|
||||||
|
# request 3, page indices [3, 4]
|
||||||
|
# paged_kv_indices is a concatenation of page indices of all requests:
|
||||||
|
# [0, 5, 8, 1, 6, 7, 3, 4]
|
||||||
|
# paged_kv_indptr is used to index into paged_kv_indices:
|
||||||
|
# [0, 3, 6, 8]
|
||||||
|
# The indptr of the paged kv cache, shape: [batch_size + 1]
|
||||||
|
paged_kv_indptr: Optional[torch.Tensor] = None
|
||||||
|
# The page indices of the paged kv cache
|
||||||
|
paged_kv_indices: Optional[torch.Tensor] = None
|
||||||
|
# The number of entries in the last page of each request in
|
||||||
|
# the paged kv cache, shape: [batch_size]
|
||||||
|
paged_kv_last_page_len: Optional[torch.Tensor] = None
|
||||||
|
# The number of query/output heads
|
||||||
|
num_qo_heads: Optional[int] = None
|
||||||
|
# The number of key/value heads
|
||||||
|
num_kv_heads: Optional[int] = None
|
||||||
|
# The dimension of the attention heads
|
||||||
|
head_dim: Optional[int] = None
|
||||||
|
# Block size of vllm
|
||||||
|
page_size: Optional[int] = None
|
||||||
|
# The data type of the paged kv cache
|
||||||
|
data_type: torch.dtype = None
|
||||||
|
# The data type of the query
|
||||||
|
q_data_type: torch.dtype = None
|
||||||
|
device: torch.device = torch.device("cuda")
|
||||||
|
is_profile_run: bool = False
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# Refer to
|
||||||
|
# https://github.com/flashinfer-ai/flashinfer/blob/3d55c71a62052c590c130897d3a3db49b14fcc34/include/flashinfer/utils.cuh#L157
|
||||||
|
supported_head_sizes = FlashInferBackend.get_supported_head_sizes()
|
||||||
|
if self.head_dim is not None and self.head_dim \
|
||||||
|
not in supported_head_sizes:
|
||||||
|
raise ValueError(
|
||||||
|
f"Only {supported_head_sizes} are supported for head_dim,",
|
||||||
|
f"received {self.head_dim}.")
|
||||||
|
|
||||||
|
def begin_forward(self):
|
||||||
|
if self.num_prefill_tokens > 0:
|
||||||
|
if self.paged_kv_indices is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# assert self.prefill_wrapper is not None
|
||||||
|
assert self.query_start_loc is not None
|
||||||
|
assert self.paged_kv_indices is not None
|
||||||
|
assert self.paged_kv_indptr is not None
|
||||||
|
assert self.paged_kv_last_page_len is not None
|
||||||
|
assert self.block_table_bound is not None
|
||||||
|
assert self.seq_lens_tensor is not None
|
||||||
|
batch_size = self.query_start_loc.shape[0] - 1
|
||||||
|
assert batch_size >= 0
|
||||||
|
# We will use flash attention for profiling to
|
||||||
|
# determine the number of blocks. Therefore,
|
||||||
|
# we don't need to prepare the input for flashinfer for profile run.
|
||||||
|
if not self.is_profile_run:
|
||||||
|
self.paged_kv_indptr = self.paged_kv_indptr.to(self.device)
|
||||||
|
self.paged_kv_last_page_len = self.paged_kv_last_page_len.to(
|
||||||
|
self.device)
|
||||||
|
self.block_table_bound = self.block_table_bound.to(self.device)
|
||||||
|
self.seq_lens_tensor = self.seq_lens_tensor.to(self.device)
|
||||||
|
self.paged_kv_indices = self.paged_kv_indices.to(self.device)
|
||||||
|
|
||||||
|
# Unsupported BatchPrefillWithPagedKVCacheWrapper Now
|
||||||
|
# self.prefill_wrapper.end_forward()
|
||||||
|
# self.prefill_wrapper.begin_forward(
|
||||||
|
# self.query_start_loc, self.paged_kv_indptr,
|
||||||
|
# self.paged_kv_indices, self.paged_kv_last_page_len,
|
||||||
|
# self.num_qo_heads, self.num_kv_heads, self.head_dim,
|
||||||
|
# self.page_size)
|
||||||
|
else:
|
||||||
|
assert self.paged_kv_indices is not None
|
||||||
|
assert self.paged_kv_indptr is not None
|
||||||
|
assert self.paged_kv_last_page_len is not None
|
||||||
|
self.paged_kv_indices = self.paged_kv_indices.to(self.device)
|
||||||
|
self.paged_kv_indptr = self.paged_kv_indptr.to(self.device)
|
||||||
|
self.paged_kv_last_page_len = self.paged_kv_last_page_len.to(
|
||||||
|
self.device)
|
||||||
|
# handle model warmup path
|
||||||
|
if self.block_table_bound is not None:
|
||||||
|
self.block_table_bound = self.block_table_bound.to(self.device)
|
||||||
|
if self.seq_lens_tensor is not None:
|
||||||
|
self.seq_lens_tensor = self.seq_lens_tensor.to(self.device)
|
||||||
|
|
||||||
|
assert self.decode_wrapper is not None
|
||||||
|
self.decode_wrapper.end_forward()
|
||||||
|
self.decode_wrapper.begin_forward(
|
||||||
|
self.paged_kv_indptr,
|
||||||
|
self.paged_kv_indices,
|
||||||
|
self.paged_kv_last_page_len,
|
||||||
|
self.num_qo_heads,
|
||||||
|
self.num_kv_heads,
|
||||||
|
self.head_dim,
|
||||||
|
self.page_size,
|
||||||
|
# Disable flashinfer's pos encoding and use vllm's rope.
|
||||||
|
pos_encoding_mode="NONE",
|
||||||
|
# kv-cache data type.
|
||||||
|
data_type=self.data_type,
|
||||||
|
# query data type.
|
||||||
|
q_data_type=self.q_data_type)
|
||||||
|
|
||||||
|
def asdict_zerocopy(self,
|
||||||
|
skip_fields: Optional[Set[str]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
if skip_fields is None:
|
||||||
|
skip_fields = set()
|
||||||
|
# We need to skip the prefill/decode_wrapper field since it cannot be
|
||||||
|
# broadcasted with nccl when TP is enabled.
|
||||||
|
skip_fields.add('prefill_wrapper')
|
||||||
|
skip_fields.add('decode_wrapper')
|
||||||
|
return super().asdict_zerocopy(skip_fields)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefill_metadata(self) -> Optional["FlashInferMetadata"]:
|
||||||
|
# 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["FlashInferMetadata"]:
|
||||||
|
# Currently chunked prefill is not supported
|
||||||
|
if self.num_prefills > 0:
|
||||||
|
assert self.num_decode_tokens == 0, (
|
||||||
|
"Chunked prefill is not supported with flashinfer yet.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
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 flashinfer yet."
|
||||||
|
"turn_prefills_into_decodes is a Multi-Step + Chunked-Prefill "
|
||||||
|
"specific parameter.")
|
||||||
|
|
||||||
|
assert num_seqs > 0
|
||||||
|
assert num_queries > 0
|
||||||
|
assert model_input.attn_metadata is not None
|
||||||
|
assert sampled_token_ids is not None
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
model_input.input_tokens[:num_queries] = sampled_token_ids.flatten()
|
||||||
|
|
||||||
|
# Update GPU tensors
|
||||||
|
ops.advance_step_flashinfer(
|
||||||
|
num_seqs=num_seqs,
|
||||||
|
num_queries=num_queries,
|
||||||
|
block_size=block_size,
|
||||||
|
input_tokens=model_input.input_tokens,
|
||||||
|
sampled_token_ids=model_input.input_tokens,
|
||||||
|
input_positions=model_input.input_positions,
|
||||||
|
seq_lens=self.seq_lens_tensor,
|
||||||
|
slot_mapping=self.slot_mapping,
|
||||||
|
block_tables=self.block_tables,
|
||||||
|
paged_kv_indices=self.paged_kv_indices,
|
||||||
|
paged_kv_indptr=self.paged_kv_indptr,
|
||||||
|
paged_kv_last_page_len=self.paged_kv_last_page_len,
|
||||||
|
block_table_bound=self.block_table_bound)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashInferMetadataBuilder(AttentionMetadataBuilder[FlashInferMetadata]):
|
||||||
|
|
||||||
|
def __init__(self, input_builder: "ModelInputForGPUBuilder"):
|
||||||
|
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.num_prefills = 0
|
||||||
|
self.num_prefill_tokens = 0
|
||||||
|
self.num_decode_tokens = 0
|
||||||
|
|
||||||
|
self.input_builder = input_builder
|
||||||
|
self.runner = input_builder.runner
|
||||||
|
|
||||||
|
self.sliding_window = input_builder.sliding_window
|
||||||
|
self.block_size = input_builder.block_size
|
||||||
|
self.use_v2_block_manager = (
|
||||||
|
input_builder.scheduler_config.use_v2_block_manager)
|
||||||
|
|
||||||
|
# Please follow https://docs.flashinfer.ai/tutorials/kv_layout.html#page-layout
|
||||||
|
# for the precise definition of the following fields.
|
||||||
|
# An example:
|
||||||
|
# request 1, page indices [0, 5, 8]
|
||||||
|
# request 2, page indices [1, 6, 7]
|
||||||
|
# request 3, page indices [3, 4]
|
||||||
|
# paged_kv_indices is a concatenation of page indices of all requests:
|
||||||
|
# [0, 5, 8, 1, 6, 7, 3, 4]
|
||||||
|
# paged_kv_indptr is used to index into paged_kv_indices:
|
||||||
|
# [0, 3, 6, 8]
|
||||||
|
self.paged_kv_indices: List[int] = []
|
||||||
|
# 0 at the beginning of paged_kv_indptr indicates the start of the
|
||||||
|
# first request’s page indices in the paged_kv_indices list.
|
||||||
|
self.paged_kv_indptr: List[int] = [0]
|
||||||
|
# paged_kv_last_page_len is the length of the last page of each request
|
||||||
|
self.paged_kv_last_page_len: List[int] = []
|
||||||
|
self.total_blocks = 0
|
||||||
|
self.is_profile_run: bool = False
|
||||||
|
|
||||||
|
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.
|
||||||
|
2. block table.
|
||||||
|
3. slot mapping.
|
||||||
|
"""
|
||||||
|
is_prompt = inter_data.is_prompt
|
||||||
|
block_tables = inter_data.block_tables
|
||||||
|
computed_block_nums = inter_data.computed_block_nums
|
||||||
|
|
||||||
|
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:
|
||||||
|
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 = computed_block_nums
|
||||||
|
elif ((chunked_prefill_enabled or not is_prompt)
|
||||||
|
and block_tables is not None):
|
||||||
|
block_table = block_tables[seq_id][-curr_sliding_window_block:]
|
||||||
|
self.block_tables.append(block_table)
|
||||||
|
|
||||||
|
is_profile_run = is_block_tables_empty(block_tables)
|
||||||
|
|
||||||
|
# Compute slot mapping.
|
||||||
|
start_idx = compute_slot_mapping_start_idx(
|
||||||
|
is_prompt, query_len, context_len, self.sliding_window,
|
||||||
|
self.use_v2_block_manager)
|
||||||
|
compute_slot_mapping(is_profile_run, self.slot_mapping, seq_id,
|
||||||
|
seq_len, context_len, start_idx,
|
||||||
|
self.block_size, inter_data.block_tables)
|
||||||
|
|
||||||
|
# It is not necessary to add paged_kv_indices, paged_kv_indptr,
|
||||||
|
# and paged_kv_last_page_len for profile run because we will
|
||||||
|
# create dummy inputs.
|
||||||
|
if is_profile_run:
|
||||||
|
self.is_profile_run = is_profile_run
|
||||||
|
return
|
||||||
|
|
||||||
|
block_table = block_tables[seq_id]
|
||||||
|
self._update_paged_kv_tensors(block_table, seq_len)
|
||||||
|
|
||||||
|
def _update_paged_kv_tensors(self, block_table: List[int], seq_len: int):
|
||||||
|
# Get the number of valid blocks based on sequence length.
|
||||||
|
# If seq_len = 16, block_size = 16,
|
||||||
|
# block_table_bound is 1 with 1 valid block.
|
||||||
|
# If seq_len = 15, block_size = 16,
|
||||||
|
# block_table_bound is 0 + 1 with 1 valid block.
|
||||||
|
self.total_blocks += len(block_table)
|
||||||
|
block_table_bound = seq_len // self.block_size + 1 \
|
||||||
|
if seq_len % self.block_size != 0 \
|
||||||
|
else seq_len // self.block_size
|
||||||
|
self.paged_kv_indices.extend(block_table[:block_table_bound])
|
||||||
|
self.paged_kv_indptr.append(self.paged_kv_indptr[-1] +
|
||||||
|
block_table_bound)
|
||||||
|
|
||||||
|
last_page_len = seq_len % self.block_size
|
||||||
|
if last_page_len == 0:
|
||||||
|
last_page_len = self.block_size
|
||||||
|
self.paged_kv_last_page_len.append(last_page_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.
|
||||||
|
"""
|
||||||
|
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_prefill_seq_len = max(self.prefill_seq_lens, default=0)
|
||||||
|
num_decode_tokens = self.num_decode_tokens
|
||||||
|
|
||||||
|
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]
|
||||||
|
max_blocks = input_block_tables.shape[1]
|
||||||
|
for i, block_table in enumerate(self.block_tables):
|
||||||
|
if block_table:
|
||||||
|
num_blocks = len(block_table)
|
||||||
|
if num_blocks <= max_blocks:
|
||||||
|
input_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.
|
||||||
|
input_block_tables[
|
||||||
|
i, :max_blocks] = block_table[:max_blocks]
|
||||||
|
|
||||||
|
block_tables = torch.from_numpy(input_block_tables).to(
|
||||||
|
device, non_blocking=True)
|
||||||
|
|
||||||
|
last_paged_kv_indptr = self.paged_kv_indptr[-1]
|
||||||
|
self.paged_kv_indptr.extend([last_paged_kv_indptr] *
|
||||||
|
cuda_graph_pad_size)
|
||||||
|
self.paged_kv_last_page_len.extend([0] * cuda_graph_pad_size)
|
||||||
|
else:
|
||||||
|
block_tables = make_tensor_with_pad(
|
||||||
|
self.block_tables,
|
||||||
|
pad=0,
|
||||||
|
dtype=torch.int,
|
||||||
|
device=device,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert device is not None
|
||||||
|
seq_lens_tensor = async_tensor_h2d(seq_lens, torch.int, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
query_lens_tensor = async_tensor_h2d(query_lens, torch.long, device,
|
||||||
|
self.runner.pin_memory)
|
||||||
|
slot_mapping_tensor = async_tensor_h2d(self.slot_mapping, torch.long,
|
||||||
|
device, self.runner.pin_memory)
|
||||||
|
query_start_loc = torch.zeros(query_lens_tensor.shape[0] + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device)
|
||||||
|
seq_start_loc = torch.zeros(seq_lens_tensor.shape[0] + 1,
|
||||||
|
dtype=torch.int32,
|
||||||
|
device=device)
|
||||||
|
torch.cumsum(seq_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=seq_start_loc.dtype,
|
||||||
|
out=seq_start_loc[1:])
|
||||||
|
torch.cumsum(query_lens_tensor,
|
||||||
|
dim=0,
|
||||||
|
dtype=query_start_loc.dtype,
|
||||||
|
out=query_start_loc[1:])
|
||||||
|
|
||||||
|
if len(self.paged_kv_indptr) > 0:
|
||||||
|
# extend to the maximum number of blocks as returned by the
|
||||||
|
# scheduler
|
||||||
|
self.paged_kv_indices.extend(
|
||||||
|
[0] * (self.total_blocks - len(self.paged_kv_indices)))
|
||||||
|
paged_kv_indices_tensor = torch.tensor(self.paged_kv_indices,
|
||||||
|
device="cpu",
|
||||||
|
dtype=torch.int)
|
||||||
|
paged_kv_indptr_tensor = torch.tensor(self.paged_kv_indptr,
|
||||||
|
device="cpu",
|
||||||
|
dtype=torch.int)
|
||||||
|
paged_kv_last_page_len_tensor = torch.tensor(
|
||||||
|
self.paged_kv_last_page_len, device="cpu", dtype=torch.int)
|
||||||
|
block_table_bound_tensor = torch.zeros(len(self.paged_kv_indptr) -
|
||||||
|
1,
|
||||||
|
device="cpu",
|
||||||
|
dtype=torch.int)
|
||||||
|
else:
|
||||||
|
paged_kv_indices_tensor = None
|
||||||
|
paged_kv_indptr_tensor = None
|
||||||
|
paged_kv_last_page_len_tensor = None
|
||||||
|
block_table_bound_tensor = None
|
||||||
|
|
||||||
|
if self.runner.kv_cache_dtype.startswith("fp8"):
|
||||||
|
kv_cache_dtype = FlashInferBackend.get_fp8_dtype_for_flashinfer(
|
||||||
|
self.runner.kv_cache_dtype)
|
||||||
|
else:
|
||||||
|
kv_cache_dtype = get_kv_cache_torch_dtype(
|
||||||
|
self.runner.kv_cache_dtype, self.runner.model_config.dtype)
|
||||||
|
|
||||||
|
return FlashInferMetadata(
|
||||||
|
num_prefills=self.num_prefills,
|
||||||
|
slot_mapping=slot_mapping_tensor,
|
||||||
|
num_prefill_tokens=self.num_prefill_tokens,
|
||||||
|
num_decode_tokens=num_decode_tokens,
|
||||||
|
max_prefill_seq_len=max_prefill_seq_len,
|
||||||
|
max_decode_seq_len=max(seq_lens),
|
||||||
|
block_tables=block_tables,
|
||||||
|
paged_kv_indptr=paged_kv_indptr_tensor,
|
||||||
|
paged_kv_indices=paged_kv_indices_tensor,
|
||||||
|
paged_kv_last_page_len=paged_kv_last_page_len_tensor,
|
||||||
|
block_table_bound=block_table_bound_tensor,
|
||||||
|
seq_lens_tensor=seq_lens_tensor,
|
||||||
|
num_qo_heads=self.runner.model_config.get_num_attention_heads(
|
||||||
|
self.runner.parallel_config),
|
||||||
|
num_kv_heads=self.runner.model_config.get_num_kv_heads(
|
||||||
|
self.runner.parallel_config),
|
||||||
|
head_dim=self.runner.model_config.get_head_size(),
|
||||||
|
page_size=self.block_size,
|
||||||
|
seq_start_loc=seq_start_loc,
|
||||||
|
query_start_loc=query_start_loc,
|
||||||
|
device=device,
|
||||||
|
data_type=kv_cache_dtype,
|
||||||
|
q_data_type=self.runner.model_config.dtype,
|
||||||
|
use_cuda_graph=use_captured_graph,
|
||||||
|
is_profile_run=self.is_profile_run)
|
||||||
|
|
||||||
|
|
||||||
|
class FlashInferImpl(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,
|
||||||
|
) -> None:
|
||||||
|
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
|
||||||
|
if sliding_window is not None:
|
||||||
|
raise ValueError("Sliding window is not supported in FlashInfer.")
|
||||||
|
self.sliding_window = (-1, -1)
|
||||||
|
self.kv_cache_dtype = kv_cache_dtype
|
||||||
|
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
|
||||||
|
|
||||||
|
def forward(
|
||||||
|
self,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: FlashInferMetadata,
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
assert k_scale == 1.0 and v_scale == 1.0, (
|
||||||
|
"key/v_scale is not supported in FlashInfer.")
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"FlashInferImpl")
|
||||||
|
|
||||||
|
# return torch.ops.vllm.unified_flash_infer(
|
||||||
|
return unified_flash_infer(
|
||||||
|
query,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
self.num_heads,
|
||||||
|
self.head_size,
|
||||||
|
self.num_kv_heads,
|
||||||
|
kv_cache,
|
||||||
|
self.kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
self.scale,
|
||||||
|
self.sliding_window,
|
||||||
|
self.alibi_slopes,
|
||||||
|
self.logits_soft_cap,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# @torch.library.custom_op("vllm::unified_flash_infer",
|
||||||
|
# mutates_args=["kv_cache"])
|
||||||
|
def unified_flash_infer(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
softmax_scale: float,
|
||||||
|
window_size: Optional[List[int]] = None,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
|
||||||
|
current_metadata = get_forward_context()
|
||||||
|
assert current_metadata is not None
|
||||||
|
assert isinstance(current_metadata, FlashInferMetadata)
|
||||||
|
attn_metadata: FlashInferMetadata = current_metadata
|
||||||
|
|
||||||
|
num_tokens, hidden_size = query.shape
|
||||||
|
query = query.view(-1, num_heads, head_size)
|
||||||
|
key = key.view(-1, num_kv_heads, head_size)
|
||||||
|
value = value.view(-1, num_kv_heads, head_size)
|
||||||
|
|
||||||
|
if attn_metadata.num_prefill_tokens > 0:
|
||||||
|
assert attn_metadata.num_decode_tokens == 0, (
|
||||||
|
"Chunked prefill is not supported with flashinfer yet.")
|
||||||
|
if attn_metadata.num_decode_tokens > 0:
|
||||||
|
assert attn_metadata.num_prefill_tokens == 0, (
|
||||||
|
"Chunked prefill is not supported with flashinfer yet.")
|
||||||
|
if kv_cache.numel() > 0:
|
||||||
|
# Use the same reshape and cache kernel as flash attention.
|
||||||
|
ops.reshape_and_cache_flashinfer(
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
kv_cache[:, 0],
|
||||||
|
kv_cache[:, 1],
|
||||||
|
attn_metadata.slot_mapping.flatten(),
|
||||||
|
kv_cache_dtype,
|
||||||
|
k_scale,
|
||||||
|
v_scale,
|
||||||
|
)
|
||||||
|
# The FlashInfer api requires data to be in fp8_e4m3 or fp8_e5m2
|
||||||
|
# to process the cache when the kv_cache_dtype is fp8
|
||||||
|
if kv_cache_dtype.startswith("fp8"):
|
||||||
|
torch_dtype = FlashInferBackend.get_fp8_dtype_for_flashinfer(
|
||||||
|
kv_cache_dtype)
|
||||||
|
kv_cache = kv_cache.view(torch_dtype)
|
||||||
|
|
||||||
|
query = query.contiguous() # Flashinfer requires query to be contiguous
|
||||||
|
if prefill_meta := attn_metadata.prefill_metadata:
|
||||||
|
# We will use flash attention for prefill
|
||||||
|
# when kv_cache is not provided.
|
||||||
|
# This happens when vllm runs the profiling to
|
||||||
|
# determine the number of blocks.
|
||||||
|
use_infer_inferface = True
|
||||||
|
if use_infer_inferface or kv_cache.numel() == 0:
|
||||||
|
output = flash_attn_varlen_func(
|
||||||
|
q=query,
|
||||||
|
k=key,
|
||||||
|
v=value,
|
||||||
|
cu_seqlens_q=prefill_meta.seq_start_loc,
|
||||||
|
cu_seqlens_k=prefill_meta.seq_start_loc,
|
||||||
|
max_seqlen_q=prefill_meta.max_prefill_seq_len,
|
||||||
|
max_seqlen_k=prefill_meta.max_prefill_seq_len,
|
||||||
|
softmax_scale=softmax_scale,
|
||||||
|
causal=True,
|
||||||
|
window_size=window_size,
|
||||||
|
alibi_slopes=alibi_slopes,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert prefill_meta is not None
|
||||||
|
assert prefill_meta.prefill_wrapper is not None
|
||||||
|
output = prefill_meta.prefill_wrapper.forward(
|
||||||
|
query, kv_cache, logits_soft_cap=logits_soft_cap, causal=True)
|
||||||
|
else:
|
||||||
|
assert attn_metadata.decode_metadata is not None
|
||||||
|
assert attn_metadata.decode_metadata.decode_wrapper is not None
|
||||||
|
output = attn_metadata.decode_metadata.decode_wrapper.forward(
|
||||||
|
query,
|
||||||
|
kv_cache,
|
||||||
|
sm_scale=softmax_scale,
|
||||||
|
logits_soft_cap=logits_soft_cap,
|
||||||
|
k_scale=k_scale,
|
||||||
|
v_scale=v_scale)
|
||||||
|
return output.view(num_tokens, hidden_size)
|
||||||
|
|
||||||
|
|
||||||
|
# @unified_flash_infer.register_fake
|
||||||
|
def _(
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
num_heads: int,
|
||||||
|
head_size: int,
|
||||||
|
num_kv_heads: int,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
kv_cache_dtype: str,
|
||||||
|
k_scale: float,
|
||||||
|
v_scale: float,
|
||||||
|
softmax_scale: float,
|
||||||
|
window_size: Optional[List[int]] = None,
|
||||||
|
alibi_slopes: Optional[torch.Tensor] = None,
|
||||||
|
logits_soft_cap: Optional[float] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
return torch.empty_like(query).contiguous()
|
||||||
381
vllm/attention/backends/ipex_attn.py
Normal file
381
vllm/attention/backends/ipex_attn.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
""" 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,
|
||||||
|
AttentionMetadata, AttentionType)
|
||||||
|
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-attn"
|
||||||
|
|
||||||
|
@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,
|
||||||
|
) -> None:
|
||||||
|
if blocksparse_params is not None:
|
||||||
|
raise ValueError(
|
||||||
|
"IPEX backend does not support block-sparse attention.")
|
||||||
|
if logits_soft_cap is not None:
|
||||||
|
raise ValueError("IPEX backend does not support logits_soft_cap.")
|
||||||
|
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 kv_cache_dtype != "auto":
|
||||||
|
raise NotImplementedError(
|
||||||
|
"IPEX backend does not support FP8 KV cache. "
|
||||||
|
"Please use xFormers backend instead.")
|
||||||
|
|
||||||
|
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,
|
||||||
|
query: torch.Tensor,
|
||||||
|
key: torch.Tensor,
|
||||||
|
value: torch.Tensor,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
attn_metadata: IpexAttnMetadata, # type: ignore
|
||||||
|
k_scale: float = 1.0,
|
||||||
|
v_scale: float = 1.0,
|
||||||
|
attn_type: AttentionType = AttentionType.DECODER,
|
||||||
|
) -> 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 k_scale == 1.0 and v_scale == 1.0
|
||||||
|
if attn_type != AttentionType.DECODER:
|
||||||
|
raise NotImplementedError("Encoder self-attention and "
|
||||||
|
"encoder/decoder cross-attention "
|
||||||
|
"are not implemented for "
|
||||||
|
"IpexAttnBackendImpl")
|
||||||
|
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,
|
||||||
|
k_scale,
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
k_scale,
|
||||||
|
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,
|
||||||
|
k_scale,
|
||||||
|
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user