150 lines
9.0 KiB
Python
150 lines
9.0 KiB
Python
import torch
|
|
import torch.nn.functional as F
|
|
import triton
|
|
import triton.language as tl
|
|
|
|
@triton.autotune(
|
|
configs=[
|
|
triton.Config({"BLOCK_N": BLOCK_N}, num_warps=num_warps, num_stages=1)
|
|
for num_warps in [1, 2, 4, 8]
|
|
for BLOCK_N in [32, 64]
|
|
],
|
|
key=['local_block_num', 'head_dim'],
|
|
)
|
|
@triton.jit
|
|
def block_attn_decoding_kernel(
|
|
q, # pointer to float32, shape [batch_size, n_head * gqa_size, head_dim]
|
|
k_min, # pointer to float32, shape [batch_size, num_blocks, n_head, head_dim]
|
|
k_max, # pointer to float32, shape [batch_size, num_blocks, n_head, head_dim]
|
|
num_blocks, # pointer to int32, shape [batch_size]
|
|
local_block_num: tl.constexpr, # int32, number of local blocks per query
|
|
head_dim: tl.constexpr, # int32, dimension per head
|
|
stride_qb, stride_qh, stride_qd,
|
|
stride_kb, stride_kt, stride_kh, stride_kd,
|
|
stride_ab, stride_ah, stride_at,
|
|
attn_score, # pointer to int32, shape [batch_size, n_head, num_blocks]
|
|
BLOCK_N: tl.constexpr, # int32, number of queries per block
|
|
):
|
|
batch_id = tl.program_id(0)
|
|
head_idx = tl.program_id(1)
|
|
start_block_idx = tl.program_id(2) * BLOCK_N
|
|
base_q_ptr = tl.make_block_ptr(q + batch_id * stride_qb + head_idx * stride_qh, shape=(head_dim,), block_shape=(head_dim,), strides=(stride_qd,), offsets=(0,), order=(0,))
|
|
mean_q = tl.load(base_q_ptr).to(tl.float32)
|
|
POS_INF = 1e38
|
|
|
|
total_n_blocks = tl.load(num_blocks + batch_id)
|
|
block_block_idx = start_block_idx + tl.arange(0, BLOCK_N)
|
|
k_min_ptr = tl.make_block_ptr(k_min + batch_id * stride_kb + head_idx * stride_kh, shape=(total_n_blocks, head_dim), block_shape=(BLOCK_N, head_dim), strides=(stride_kt, stride_kd), offsets=(start_block_idx, 0), order=(0, 1))
|
|
k_max_ptr = tl.make_block_ptr(k_max + batch_id * stride_kb + head_idx * stride_kh, shape=(total_n_blocks, head_dim), block_shape=(BLOCK_N, head_dim), strides=(stride_kt, stride_kd), offsets=(start_block_idx, 0), order=(0, 1))
|
|
k_min_val = tl.load(k_min_ptr, boundary_check=(0, 1)).to(tl.float32)
|
|
k_max_val = tl.load(k_max_ptr, boundary_check=(0, 1)).to(tl.float32)
|
|
score = tl.maximum(k_min_val * mean_q, k_max_val * mean_q)
|
|
score = tl.sum(score, axis=1)
|
|
score = tl.where(block_block_idx >= total_n_blocks - local_block_num, POS_INF, score)
|
|
attn_score_ptr = tl.make_block_ptr(attn_score + batch_id * stride_ab + head_idx * stride_ah, shape=(total_n_blocks,), block_shape=(BLOCK_N,), strides=(stride_at,), offsets=(start_block_idx,), order=(0,))
|
|
tl.store(attn_score_ptr, score, boundary_check=(0,))
|
|
|
|
def block_attn_decoding(q, k_min, k_max, num_blocks, local_block_num):
|
|
assert num_blocks.max().item() <= k_min.shape[1] , "num_blocks should be less than or equal to k_min.shape[1]"
|
|
min_val = torch.finfo(torch.float32).min
|
|
batch, n_kv_heads, head_dim = q.shape[0], k_min.shape[2], k_min.shape[3]
|
|
attn_score = torch.full((batch, n_kv_heads, num_blocks.max().item()), fill_value=min_val, device=q.device, dtype=torch.float32)
|
|
grid = lambda META: (batch, n_kv_heads, triton.cdiv(num_blocks.max().item(), META['BLOCK_N']))
|
|
with torch.cuda.device(q.device.index):
|
|
block_attn_decoding_kernel[grid](q, k_min, k_max,
|
|
num_blocks, local_block_num, head_dim,
|
|
*q.stride(),
|
|
*k_min.stride(),
|
|
*attn_score.stride(),
|
|
attn_score,
|
|
)
|
|
return attn_score
|
|
|
|
@torch.compile(fullgraph=True)
|
|
def get_topk_indices(block_attn_score, block_index_mask, max_num_selected_blocks):
|
|
block_index = torch.topk(block_attn_score, k=max_num_selected_blocks, dim=-1, sorted=True).indices
|
|
topk_indices = block_index.masked_fill_(~block_index_mask, -1)
|
|
topk_indices = torch.sort(topk_indices, dim=-1, descending=False).values
|
|
return topk_indices.to(torch.int32)
|
|
|
|
@torch.compile(fullgraph=True)
|
|
def get_num_blocks(cache_seqlens, block_size, sparse_ratio, min_block_num):
|
|
num_blocks = (cache_seqlens + (block_size - 1)) // block_size
|
|
num_selected_blocks = torch.ceil(num_blocks * sparse_ratio)
|
|
num_selected_blocks = torch.maximum(num_selected_blocks, num_blocks.clamp(max=min_block_num)).long()
|
|
return num_blocks, num_selected_blocks
|
|
|
|
class KVManager:
|
|
def __init__(self, num_heads, block_size, sparse_ratio, local_block_num, min_block_num):
|
|
self.num_heads = num_heads
|
|
self.block_size = block_size
|
|
self.sparse_ratio = sparse_ratio
|
|
self.local_block_num = local_block_num
|
|
self.min_block_num = min_block_num
|
|
self.block_max_key = None
|
|
self.block_min_key = None
|
|
self.num_elements = None
|
|
assert self.local_block_num <= self.min_block_num, "local_block_num should be less than or equal to min_block_num"
|
|
|
|
def init_centeroids(self, key, cache_seqlens):
|
|
bsz, seqlen, num_heads, head_dim = key.shape
|
|
max_val = torch.finfo(key.dtype).max
|
|
min_val = torch.finfo(key.dtype).min
|
|
self.num_elements = seqlen
|
|
key_block = key.reshape(bsz, -1, self.block_size, num_heads, head_dim)
|
|
self.block_max_key = key_block.max(dim=2).values
|
|
self.block_min_key = key_block.min(dim=2).values
|
|
num_blocks = (cache_seqlens + self.block_size - 1) // self.block_size
|
|
key_last_block = key_block[torch.arange(bsz, device=key.device), num_blocks - 1]
|
|
valid_mask = torch.arange(self.block_size, device=key.device) + (num_blocks[:, None] - 1) * self.block_size < cache_seqlens[:, None]
|
|
last_min_key = torch.masked_fill(key_last_block, ~valid_mask[:, :, None, None], max_val).min(dim=1).values
|
|
last_max_key = torch.masked_fill(key_last_block, ~valid_mask[:, :, None, None], min_val).max(dim=1).values
|
|
self.block_max_key[torch.arange(bsz, device=key.device), num_blocks - 1] = last_max_key
|
|
self.block_min_key[torch.arange(bsz, device=key.device), num_blocks - 1] = last_min_key
|
|
causal_mask = torch.arange(self.block_max_key.shape[1], device=key.device) < num_blocks[:, None]
|
|
self.block_max_key.masked_fill_(~causal_mask[:, :, None, None], min_val)
|
|
self.block_min_key.masked_fill_(~causal_mask[:, :, None, None], max_val)
|
|
|
|
@torch.compile(fullgraph=True)
|
|
def update_centeroids(self, key, cache_seqlens):
|
|
num_blocks = (cache_seqlens + self.block_size - 1) // self.block_size
|
|
batch_index = torch.arange(num_blocks.shape[0], device=key.device)
|
|
self.block_max_key[batch_index, num_blocks - 1] = torch.maximum(self.block_max_key[batch_index, num_blocks - 1], key)
|
|
self.block_min_key[batch_index, num_blocks - 1] = torch.minimum(self.block_min_key[batch_index, num_blocks - 1], key)
|
|
|
|
def clear_centeroids(self):
|
|
self.block_max_key = None
|
|
self.block_min_key = None
|
|
self.num_elements = None
|
|
|
|
def get_kv_cache_indices(self, query, cache_seqlens):
|
|
bsz, num_heads, head_dim = query.shape
|
|
max_val = torch.finfo(torch.float32).max
|
|
query = query.view(bsz, 1, self.num_heads, num_heads // self.num_heads, head_dim).mean(dim=3).float() * (head_dim ** -0.5)
|
|
attn_score = torch.maximum(query * self.block_max_key.float(), query * self.block_min_key.float()).sum(dim=-1)
|
|
topk_indices = torch.full((bsz, self.num_heads, self.block_max_key.shape[1]), device=query.device, dtype=torch.int32, fill_value=-1)
|
|
num_blocks = (cache_seqlens + self.block_size - 1) // self.block_size
|
|
num_selected_blocks = torch.ceil(num_blocks * self.sparse_ratio)[:, None].repeat(1, self.num_heads)
|
|
|
|
num_selected_blocks = torch.maximum(num_selected_blocks, num_blocks.clamp(max=self.min_block_num)[:, None]).long()
|
|
for b in range(bsz):
|
|
local_attn_score = attn_score[b, :num_blocks[b]]
|
|
local_attn_score[-self.local_block_num:] = max_val
|
|
block_index = torch.sort(local_attn_score, dim=0, descending=True).indices.transpose(0, 1)
|
|
block_index_mask = torch.arange(num_blocks[b], device=query.device) < num_selected_blocks[b][:, None]
|
|
topk_indices[b, :, :num_blocks[b]] = block_index.masked_fill(~block_index_mask, -1).to(torch.int32)
|
|
topk_indices = torch.sort(topk_indices, dim=-1, descending=True).values
|
|
return topk_indices
|
|
|
|
def get_kv_cache_indices_fast(self, query, cache_seqlens):
|
|
bsz, num_heads, head_dim = query.shape
|
|
num_blocks, num_selected_blocks = get_num_blocks(cache_seqlens, self.block_size, self.sparse_ratio, self.min_block_num)
|
|
max_num_selected_blocks = num_selected_blocks.max().item()
|
|
query = query.view(bsz, self.num_heads, num_heads // self.num_heads, head_dim).mean(dim=2) * (head_dim ** -0.5)
|
|
block_attn_score = block_attn_decoding(query, self.block_max_key, self.block_min_key, num_blocks, self.local_block_num)
|
|
block_index_mask = torch.arange(max_num_selected_blocks, device=query.device) < num_selected_blocks[:, None, None]
|
|
topk_indices = get_topk_indices(block_attn_score, block_index_mask, max_num_selected_blocks)
|
|
return topk_indices
|
|
|
|
|