Hybrid kv cache for LLaMA4 (#6563)
Co-authored-by: Cheng Wan <54331508+ch-wan@users.noreply.github.com> Co-authored-by: tarinkk <rt572@physics.rutger.edu> Co-authored-by: tarinkk <rt572@rutgers.physics.edu> Co-authored-by: Hanming Lu <69857889+hanming-lu@users.noreply.github.com>
This commit is contained in:
co-authored by
Cheng Wan
tarinkk
tarinkk
Hanming Lu
parent
357921aa51
commit
eb6c2c1663
@@ -20,12 +20,14 @@ Page-aligned memory pool.
|
||||
"""
|
||||
|
||||
import abc
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.mem_cache.memory_pool import SWAKVPool
|
||||
from sglang.srt.utils import get_bool_env_var, next_power_of_2
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -55,6 +57,11 @@ class BaseTokenToKVPoolAllocator(abc.ABC):
|
||||
def debug_print(self) -> str:
|
||||
return ""
|
||||
|
||||
def log_usage(self, evictable_size: int = 0):
|
||||
num_used = self.size - (self.available_size() + evictable_size)
|
||||
msg = f"#token: {num_used}, token usage: {num_used / self.size:.2f}, "
|
||||
return msg, num_used
|
||||
|
||||
def available_size(self):
|
||||
return len(self.free_pages) * self.page_size
|
||||
|
||||
@@ -146,6 +153,128 @@ class TokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
return self._kvcache.load_cpu_copy(kv_cache_cpu, indices)
|
||||
|
||||
|
||||
class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
"""Allocator for SWA hybrid KV cache."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
size_swa: int,
|
||||
dtype: torch.dtype,
|
||||
device: str,
|
||||
kvcache: SWAKVPool,
|
||||
):
|
||||
super().__init__(size, 1, dtype, device, kvcache)
|
||||
assert isinstance(kvcache, SWAKVPool)
|
||||
self._size_full = size
|
||||
self._size_swa = size_swa
|
||||
self.full_attn_allocator = TokenToKVPoolAllocator(
|
||||
size,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.full_kv_pool,
|
||||
)
|
||||
self.swa_attn_allocator = TokenToKVPoolAllocator(
|
||||
size_swa,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.swa_kv_pool,
|
||||
)
|
||||
self.full_to_swa_index_mapping = torch.empty(
|
||||
size + size_swa + 1,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
self.clear()
|
||||
|
||||
self._kvcache.full_to_swa_index_mapping = self.full_to_swa_index_mapping
|
||||
|
||||
def available_size(self):
|
||||
return min(self.full_available_size(), self.swa_available_size())
|
||||
|
||||
def full_available_size(self):
|
||||
return self.full_attn_allocator.available_size()
|
||||
|
||||
def swa_available_size(self):
|
||||
return self.swa_attn_allocator.available_size()
|
||||
|
||||
@property
|
||||
def size_full(self):
|
||||
return self._size_full
|
||||
|
||||
@property
|
||||
def size_swa(self):
|
||||
return self._size_swa
|
||||
|
||||
def debug_print(self) -> str:
|
||||
msg = ""
|
||||
msg += f"#swa-available-size: {self.swa_attn_allocator.available_size()}, "
|
||||
msg += (
|
||||
f"#full-attn-available-size: {self.full_attn_allocator.available_size()}, "
|
||||
)
|
||||
return msg
|
||||
|
||||
def log_usage(self, swa_evictable_size: int = 0, full_evictable_size: int = 0):
|
||||
used_full = self.size_full - (self.full_available_size() + full_evictable_size)
|
||||
used_swa = self.size_swa - (self.swa_available_size() + swa_evictable_size)
|
||||
msg = (
|
||||
f"#token: full={used_full}, swa={used_swa}, "
|
||||
f"token usage: full={used_full / self.size_full:.2f}, "
|
||||
f"swa={used_swa / self.size_swa:.2f}, "
|
||||
)
|
||||
return msg, used_full
|
||||
|
||||
def get_kvcache(self):
|
||||
return self._kvcache
|
||||
|
||||
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
|
||||
assert self.full_to_swa_index_mapping is not None
|
||||
return self.full_to_swa_index_mapping[kv_indices].to(torch.int32)
|
||||
|
||||
def alloc(self, need_size: int):
|
||||
if need_size > self.full_attn_allocator.available_size():
|
||||
return None
|
||||
if need_size > self.swa_attn_allocator.available_size():
|
||||
return None
|
||||
|
||||
alloc_full_indices = self.full_attn_allocator.alloc(need_size)
|
||||
alloc_swa_indices = self.swa_attn_allocator.alloc(need_size)
|
||||
self.full_to_swa_index_mapping[alloc_full_indices] = alloc_swa_indices
|
||||
return alloc_full_indices
|
||||
|
||||
def free(self, free_index: torch.Tensor):
|
||||
if free_index.numel() == 0:
|
||||
return
|
||||
if self.is_not_in_free_group:
|
||||
self.full_attn_allocator.free(free_index)
|
||||
self.free_swa(free_index)
|
||||
else:
|
||||
self.free_group.append(free_index)
|
||||
assert (
|
||||
self.full_attn_allocator.available_size() <= self.full_attn_allocator.size
|
||||
)
|
||||
assert self.swa_attn_allocator.available_size() <= self.swa_attn_allocator.size
|
||||
|
||||
def free_swa(self, free_index: torch.Tensor):
|
||||
swa_indices = self.full_to_swa_index_mapping[free_index]
|
||||
swa_indices = swa_indices[swa_indices > 0]
|
||||
self.swa_attn_allocator.free(swa_indices)
|
||||
self.full_to_swa_index_mapping[free_index] = 0
|
||||
|
||||
def backup_state(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def restore_state(self, state):
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
self.swa_attn_allocator.clear()
|
||||
self.full_attn_allocator.clear()
|
||||
self.full_to_swa_index_mapping.fill_(0)
|
||||
self.is_in_free_group = False
|
||||
self.free_group = []
|
||||
|
||||
|
||||
@triton.jit
|
||||
def alloc_extend_kernel(
|
||||
pre_lens_ptr,
|
||||
|
||||
@@ -2,11 +2,14 @@ from __future__ import annotations
|
||||
|
||||
"""Cache for chunked prefill, used when RadixCache is disabled."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
|
||||
from sglang.srt.mem_cache.allocator import (
|
||||
BaseTokenToKVPoolAllocator,
|
||||
SWATokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache, MatchResult
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
|
||||
@@ -63,3 +66,32 @@ class ChunkCache(BasePrefixCache):
|
||||
|
||||
def pretty_print(self):
|
||||
return ""
|
||||
|
||||
|
||||
class SWAChunkCache(ChunkCache):
|
||||
"""ChunkCache with support for hybrid KV cache operations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
req_to_token_pool: ReqToTokenPool,
|
||||
token_to_kv_pool_allocator: SWATokenToKVPoolAllocator,
|
||||
page_size: int,
|
||||
):
|
||||
super().__init__(req_to_token_pool, token_to_kv_pool_allocator, page_size)
|
||||
assert isinstance(token_to_kv_pool_allocator, SWATokenToKVPoolAllocator)
|
||||
|
||||
def evict(
|
||||
self,
|
||||
req: Req,
|
||||
prelen: int,
|
||||
attention_chunk_size: int,
|
||||
):
|
||||
if prelen >= req.evicted_seqlen_local + attention_chunk_size:
|
||||
new_evicted_seqlen_local = attention_chunk_size * (
|
||||
prelen // attention_chunk_size
|
||||
)
|
||||
free_slots = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, req.evicted_seqlen_local : new_evicted_seqlen_local
|
||||
]
|
||||
self.token_to_kv_pool_allocator.free_swa(free_slots)
|
||||
req.evicted_seqlen_local = new_evicted_seqlen_local
|
||||
|
||||
@@ -27,10 +27,11 @@ KVCache actually holds the physical kv cache.
|
||||
import abc
|
||||
import logging
|
||||
from contextlib import nullcontext
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
@@ -66,6 +67,7 @@ class ReqToTokenPool:
|
||||
self.req_to_token = torch.zeros(
|
||||
(size, max_context_len), dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
self.free_slots = list(range(size))
|
||||
|
||||
def write(self, indices, values):
|
||||
@@ -191,7 +193,6 @@ class MHATokenToKVPool(KVCache):
|
||||
start_layer,
|
||||
end_layer,
|
||||
)
|
||||
|
||||
self.head_num = head_num
|
||||
self.head_dim = head_dim
|
||||
|
||||
@@ -392,10 +393,14 @@ class MHATokenToKVPool(KVCache):
|
||||
cache_v: torch.Tensor,
|
||||
k_scale: Optional[float] = None,
|
||||
v_scale: Optional[float] = None,
|
||||
layer_id_override: Optional[int] = None,
|
||||
):
|
||||
from sglang.srt.model_executor.cuda_graph_runner import get_is_capture_mode
|
||||
|
||||
layer_id = layer.layer_id
|
||||
if layer_id_override is not None:
|
||||
layer_id = layer_id_override
|
||||
else:
|
||||
layer_id = layer.layer_id
|
||||
if cache_k.dtype != self.dtype:
|
||||
if k_scale is not None:
|
||||
cache_k.div_(k_scale)
|
||||
@@ -431,6 +436,136 @@ class MHATokenToKVPool(KVCache):
|
||||
)
|
||||
|
||||
|
||||
class SWAKVPool(KVCache):
|
||||
"""KV cache with separate pools for full and SWA attention layers."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
size_swa: int,
|
||||
dtype: torch.dtype,
|
||||
head_num: int,
|
||||
head_dim: int,
|
||||
swa_attention_layer_ids: List[int],
|
||||
full_attention_layer_ids: List[int],
|
||||
enable_kvcache_transpose: bool,
|
||||
device: str,
|
||||
):
|
||||
self.size = size
|
||||
self.size_swa = size_swa
|
||||
self.dtype = dtype
|
||||
self.device = device
|
||||
self.swa_layer_nums = len(swa_attention_layer_ids)
|
||||
self.full_layer_nums = len(full_attention_layer_ids)
|
||||
self.page_size = 1
|
||||
# TODO MHATransposedTokenToKVPool if enable_kvcache_transpose is True
|
||||
assert not enable_kvcache_transpose
|
||||
TokenToKVPoolClass = MHATokenToKVPool
|
||||
self.swa_kv_pool = TokenToKVPoolClass(
|
||||
size=size_swa,
|
||||
page_size=self.page_size,
|
||||
dtype=dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
layer_num=self.swa_layer_nums,
|
||||
device=device,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
self.full_kv_pool = TokenToKVPoolClass(
|
||||
size=size,
|
||||
page_size=self.page_size,
|
||||
dtype=dtype,
|
||||
head_num=head_num,
|
||||
head_dim=head_dim,
|
||||
layer_num=self.full_layer_nums,
|
||||
device=device,
|
||||
enable_memory_saver=False,
|
||||
)
|
||||
self.layers_mapping: Dict[int, Tuple[int, bool]] = {}
|
||||
for full_attn_layer_id, global_layer_id in enumerate(full_attention_layer_ids):
|
||||
self.layers_mapping[global_layer_id] = (full_attn_layer_id, False)
|
||||
for swa_layer_id, global_layer_id in enumerate(swa_attention_layer_ids):
|
||||
self.layers_mapping[global_layer_id] = (swa_layer_id, True)
|
||||
self.full_to_swa_index_mapping: Optional[torch.Tensor] = None
|
||||
|
||||
def get_kv_size_bytes(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_contiguous_buf_infos(self):
|
||||
full_kv_data_ptrs, full_kv_data_lens, full_kv_item_lens = (
|
||||
self.full_kv_pool.get_contiguous_buf_infos()
|
||||
)
|
||||
swa_kv_data_ptrs, swa_kv_data_lens, swa_kv_item_lens = (
|
||||
self.swa_kv_pool.get_contiguous_buf_infos()
|
||||
)
|
||||
|
||||
kv_data_ptrs = full_kv_data_ptrs + swa_kv_data_ptrs
|
||||
kv_data_lens = full_kv_data_lens + swa_kv_data_lens
|
||||
kv_item_lens = full_kv_item_lens + swa_kv_item_lens
|
||||
|
||||
return kv_data_ptrs, kv_data_lens, kv_item_lens
|
||||
|
||||
def get_key_buffer(self, layer_id: int):
|
||||
layer_id_pool, is_swa = self.layers_mapping[layer_id]
|
||||
if is_swa:
|
||||
return self.swa_kv_pool.get_key_buffer(layer_id_pool)
|
||||
else:
|
||||
return self.full_kv_pool.get_key_buffer(layer_id_pool)
|
||||
|
||||
def get_value_buffer(self, layer_id: int):
|
||||
layer_id_pool, is_swa = self.layers_mapping[layer_id]
|
||||
if is_swa:
|
||||
return self.swa_kv_pool.get_value_buffer(layer_id_pool)
|
||||
else:
|
||||
return self.full_kv_pool.get_value_buffer(layer_id_pool)
|
||||
|
||||
def get_kv_buffer(self, layer_id: int):
|
||||
layer_id_pool, is_swa = self.layers_mapping[layer_id]
|
||||
if is_swa:
|
||||
return self.swa_kv_pool.get_kv_buffer(layer_id_pool)
|
||||
else:
|
||||
return self.full_kv_pool.get_kv_buffer(layer_id_pool)
|
||||
|
||||
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
|
||||
assert self.full_to_swa_index_mapping is not None
|
||||
return self.full_to_swa_index_mapping[kv_indices].to(torch.int32)
|
||||
|
||||
def set_kv_buffer(
|
||||
self,
|
||||
layer: RadixAttention,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
cache_v: torch.Tensor,
|
||||
k_scale: float = 1.0,
|
||||
v_scale: float = 1.0,
|
||||
):
|
||||
|
||||
layer_id = layer.layer_id
|
||||
layer_id_pool, is_swa = self.layers_mapping[layer_id]
|
||||
if is_swa:
|
||||
if self.full_to_swa_index_mapping is not None:
|
||||
loc = self.translate_loc_from_full_to_swa(loc)
|
||||
self.swa_kv_pool.set_kv_buffer(
|
||||
None,
|
||||
loc,
|
||||
cache_k,
|
||||
cache_v,
|
||||
k_scale,
|
||||
v_scale,
|
||||
layer_id_override=layer_id_pool,
|
||||
)
|
||||
else:
|
||||
self.full_kv_pool.set_kv_buffer(
|
||||
None,
|
||||
loc,
|
||||
cache_k,
|
||||
cache_v,
|
||||
k_scale,
|
||||
v_scale,
|
||||
layer_id_override=layer_id_pool,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def set_mla_kv_buffer_kernel(
|
||||
kv_buffer_ptr,
|
||||
|
||||
Reference in New Issue
Block a user