Files
2026-07-13 13:18:33 +08:00

209 lines
8.4 KiB
Python

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import operator
from functools import reduce
from typing import Any, Iterable, Optional, Tuple
import torch
import deepspeed.comm as dist
from deepspeed.comm.reduce_op import ReduceOp
from deepspeed.accelerator import get_accelerator
from ..inference_utils import elem_size
from ..logging import inference_logger
from .blocked_allocator import BlockedAllocator
from .manager_configs import AllocationMode, KVCacheConfig, MemoryConfig
def split_kv(kv_cache: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Split a KV cache instance into its key and value components.
Parameters:
kv_cache (torch.Tensor): The KV-cache to split. This should be a 5D tensor with the
following shape: [num_blocks, block_size, 2, num_heads, head_size]
Returns:
Tuple[torch.Tensor, torch.Tensor]: The key and value components of the KV-cache. Both
tensors will have the shape [num_blocks, block_size, num_heads, head_size].
"""
if kv_cache.ndim != 5:
raise ValueError(f"KV-cache must have 5 dimensions, got {kv_cache.ndim}.")
return kv_cache[:, :, 0, :, :], kv_cache[:, :, 1, :, :]
class BlockedKVCache:
_caches: Tuple[torch.Tensor, ...]
"""
Backing storage for all KV caches. This is a 6D tensor with the following shape:
(num_caches, num_blocks, block_size, 2, num_heads, head_size)
"""
_allocators: Tuple[BlockedAllocator, ...]
"""
Block allocator for tracking cache usage. This manages the GPU cache.
"""
_configs: Tuple[KVCacheConfig, ...]
"""
Configuration of the KV cache(s). See ``KVCacheConfig`` for more details. This enables the support
for different types/shapes of KV-caches (i.e. the alternating local and global attention in
GPT-Neo).
"""
def __init__(self,
configs: Tuple[KVCacheConfig, ...],
memory_config: MemoryConfig,
mp_group: Optional[Any] = None,
offload: bool = False) -> None:
"""
Create a container that will maintain the storage and allocations for a set of
blocked KV-caches.
Parameters:
config (KVCacheConfig): The configuration of the KV-cache.
slack (int): The amount of slack space to reserve in GPU memory for the cache.
enable_offload (bool): Whether to enable offloading of the cache to the host.
blocks (int): The number of blocks to pre-allocate for the cache. If this is set,
slack will be ignored.
"""
self._configs = configs
self._memory_config = memory_config
self._enable_offload = offload
if self._enable_offload:
raise NotImplementedError("Offloading of KV-caches is not yet supported.")
if AllocationMode(self._memory_config.mode) is AllocationMode.RESERVE:
# TODO(cmikeh2): Change the weighting based on the type of the KV-cache
total_per_block_footprint = 0
for config in self._configs:
per_block_footprint = reduce(operator.mul, config.cache_shape, config.block_size)
per_block_footprint *= 2 # for key and value
total_per_block_footprint += per_block_footprint * elem_size(config.cache_dtype)
# Perform a dummy nccl call before calculating available memory, on some systems (H100) we've observed higher memory allocations from NCCL
if dist.get_world_size(group=mp_group) > 1:
dummy_tensor = torch.tensor(0, dtype=torch.int32, device=get_accelerator().current_device())
dist.all_reduce(dummy_tensor, op=ReduceOp.MIN, group=mp_group)
get_accelerator().empty_cache()
available_kv_memory = get_accelerator().available_memory() - self._memory_config.size
total_memory = get_accelerator().total_memory()
inference_logger().debug(
f"Memory usage before KV-cache allocation: total_memory={total_memory}, available_kv_memory={available_kv_memory}, total_per_block_footprint={total_per_block_footprint}"
)
if available_kv_memory < total_per_block_footprint:
raise ValueError(
f"Insufficient memory to allocate KV-caches. Required: {total_per_block_footprint}, Available: {available_kv_memory}"
)
num_blocks = available_kv_memory // total_per_block_footprint
# In a multi-process setting, we need to ensure that all processes have the same
# KV cache capacity to ensure scheduling guarantees are equivalent on all ranks.
if dist.get_world_size(group=mp_group) > 1:
reduce_tensor = torch.tensor(num_blocks, dtype=torch.int32, device=get_accelerator().current_device())
dist.all_reduce(reduce_tensor, op=ReduceOp.MIN, group=mp_group)
num_blocks = reduce_tensor.item()
# This is ugly but don't want the fragmentation of the 8 byte Tensor maybe
# hanging around.
del reduce_tensor
get_accelerator().empty_cache()
else: # AllocationMode.ALLOCATE
num_blocks = self._memory_config.size
caches = []
allocators = []
for cache_group_id, config in enumerate(self._configs):
num_caches = config.cache_shape[0]
num_heads = config.cache_shape[1]
head_size = config.cache_shape[2]
alloc_shape = (num_caches, num_blocks, config.block_size, 2, num_heads, head_size)
inference_logger().info(
f"Allocating KV-cache {cache_group_id} with shape: {alloc_shape} consisting of {num_blocks} blocks.")
caches.append(torch.empty(alloc_shape, dtype=config.cache_dtype,
device=get_accelerator().current_device()))
allocators.append(BlockedAllocator(num_blocks))
self._caches = tuple(caches)
self._allocators = tuple(allocators)
def reserve(self, num_blocks: int, cache_group: int = 0) -> torch.Tensor:
"""
Reserve a number of blocks from the cache. This will return a 1D tensor of
block_ids that have been marked as reserved.
Parameters:
num_blocks (int): The number of blocks to reserve.
cache_group (int): The cache group to reserve from. Default is 0.
"""
return self._allocators[cache_group].allocate(num_blocks)
def free(self, blocks: Iterable[int], cache_group: int = 0) -> None:
"""
Free a set of blocks from the cache. This will mark the blocks as free in the
allocator.
Parameters:
blocks (Iterable[int]): The blocks to free.
cache_group (int): The cache group to free from. Default is 0.
"""
self._allocators[cache_group].free(blocks)
def offload(self, blocks: Iterable[int], cache_group: int = 0) -> torch.Tensor:
"""
Offload KV-cache blocks from accelerator memory to the host.
Parameters:
blocks (Iterable[int]): The blocks to offload.
cache_group (int): The cache group to offload from. Default is 0.
"""
raise NotImplementedError("Offloading is not yet supported.")
def restore(self, blocks: Iterable[int], cache_group: int = 0) -> torch.Tensor:
"""
Restore KV-cache blocks from the host to accelerator memory.
Parameters:
blocks (Iterable[int]): The blocks to restore.
cache_group (int): The cache group to restore to. Default is 0.
"""
raise NotImplementedError("Offloading is not yet supported.")
def get_cache(self, cache_id: int, cache_group: int = 0) -> torch.Tensor:
"""
Get the tensor associated with the given cache ID.
Parameters:
cache_id (int): The ID of the cache tensor to get.
cache_group (int): The cache group to get from. Default is 0.
"""
return self._caches[cache_group][cache_id]
@property
def free_blocks(self) -> torch.Tensor:
"""
Return the number of free blocks in each cache
"""
return [allocator.free_blocks for allocator in self._allocators]
@property
def num_caches(self) -> int:
"""
Return the number of caches
"""
return len(self._caches)