feat(CRITICAL): import wudixzy/competition complete corex stack — 12 prebuilt .so + 13 CUDA kernels + 2615-line qwen3_5.py
Source: github.com/wudixzy/competition (1527 files, BI-V100 competition reference)
Imported assets:
- 12 prebuilt CoreX .so extensions (corex-3.2.3-ivcore10):
corex_gdn_{beta_decay,causal_conv,gated_norm,packed_decode,qk_map}.so
corex_moe_{direct_routed,exact_reduce,weight_gather}.so
corex_attn_head_rms_norm.so, corex_paged_kv_gather.so
corex_block_major_kv_transfer.so, corex_fused_paged_prefill.so
- 13 CUDA kernel sources (.cu) for above extensions
- 11 build scripts (build_corex_*.sh)
- install_prebuilt_corex.sh (SHA256-verified .so deployment)
- qwen3_5.py (2615 lines) with FULL corex kernel integration
- 9 vllm vendor override files (block manager, sampler, etc)
- 19 patch scripts (model_runner, xformers, block_major, etc)
- Complete serving layer (serving_chat, protocol, api_server, etc)
- bi100_env.py, bi100_profile.py, gdn_prefix.py, block_major_kv_cache.py
- Dockerfile aligned with reference build chain
- computility-run.yaml with BI100_MOE_COREX_DIRECT_ROUTED=1
Call chain verified:
Dockerfile COPY → patch_ops.sh → install_prebuilt_corex.sh → 12 .so to $VLLM_ROOT
qwen3_5.py imports: from vllm import corex_gdn_* / corex_moe_* / corex_attn_*
This commit is contained in:
456
vllm_overrides/core/block/block_table.py
Normal file
456
vllm_overrides/core/block/block_table.py
Normal file
@@ -0,0 +1,456 @@
|
||||
import math
|
||||
from typing import List, Optional
|
||||
|
||||
from vllm.core.block.common import BlockList
|
||||
from vllm.core.block.interfaces import Block, DeviceAwareBlockAllocator
|
||||
from vllm.utils import Device, cdiv, chunk_list
|
||||
|
||||
|
||||
class BlockTable:
|
||||
"""A class to manage blocks for a specific sequence.
|
||||
|
||||
The BlockTable maps a sequence of tokens to a list of blocks, where each
|
||||
block represents a contiguous memory allocation for a portion of the
|
||||
sequence. The blocks are managed by a DeviceAwareBlockAllocator, which is
|
||||
responsible for allocating and freeing memory for the blocks.
|
||||
|
||||
Args:
|
||||
block_size (int): The maximum number of tokens that can be stored in a
|
||||
single block.
|
||||
block_allocator (DeviceAwareBlockAllocator): The block allocator used to
|
||||
manage memory for the blocks.
|
||||
_blocks (Optional[List[Block]], optional): An optional list of existing
|
||||
blocks to initialize the BlockTable with. If not provided, an empty
|
||||
BlockTable is created.
|
||||
max_block_sliding_window (Optional[int], optional): The number of
|
||||
blocks to keep around for each sequance. If None, all blocks
|
||||
are kept (eg., when sliding window is not used).
|
||||
It should at least fit the sliding window size of the model.
|
||||
|
||||
Attributes:
|
||||
_block_size (int): The maximum number of tokens that can be stored in a
|
||||
single block.
|
||||
_allocator (DeviceAwareBlockAllocator): The block allocator used to
|
||||
manage memory for the blocks.
|
||||
_blocks (Optional[List[Block]]): The list of blocks managed by this
|
||||
BlockTable.
|
||||
_num_full_slots (int): The number of tokens currently stored in the
|
||||
blocks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
block_size: int,
|
||||
block_allocator: DeviceAwareBlockAllocator,
|
||||
_blocks: Optional[List[Block]] = None,
|
||||
max_block_sliding_window: Optional[int] = None,
|
||||
cache_namespace: Optional[bytes] = None,
|
||||
):
|
||||
self._block_size = block_size
|
||||
self._allocator = block_allocator
|
||||
self._cache_namespace = cache_namespace
|
||||
if _blocks is None:
|
||||
_blocks = []
|
||||
self._blocks: BlockList = BlockList(_blocks)
|
||||
|
||||
self._max_block_sliding_window = max_block_sliding_window
|
||||
self._num_full_slots = self._get_num_token_ids()
|
||||
|
||||
@staticmethod
|
||||
def get_num_required_blocks(token_ids: List[int],
|
||||
block_size: int,
|
||||
num_lookahead_slots: int = 0) -> int:
|
||||
"""Calculates the minimum number of blocks required to store a given
|
||||
sequence of token IDs along with any look-ahead slots that may be
|
||||
required (like in multi-step + chunked-prefill).
|
||||
|
||||
This assumes worst-case scenario, where every block requires a new
|
||||
allocation (e.g. ignoring prefix caching).
|
||||
|
||||
Args:
|
||||
token_ids (List[int]): The sequence of token IDs to be stored.
|
||||
block_size (int): The maximum number of tokens that can be stored in
|
||||
a single block.
|
||||
num_lookahead_slots (int): look-ahead slots that the sequence may
|
||||
require.
|
||||
|
||||
Returns:
|
||||
int: The minimum number of blocks required to store the given
|
||||
sequence of token IDs along with any required look-ahead slots.
|
||||
"""
|
||||
return cdiv(len(token_ids) + num_lookahead_slots, block_size)
|
||||
|
||||
def allocate(self,
|
||||
token_ids: List[int],
|
||||
device: Device = Device.GPU) -> None:
|
||||
"""Allocates memory blocks for storing the given sequence of token IDs.
|
||||
|
||||
This method allocates the required number of blocks to store the given
|
||||
sequence of token IDs.
|
||||
|
||||
Args:
|
||||
token_ids (List[int]): The sequence of token IDs to be stored.
|
||||
device (Device, optional): The device on which the blocks should be
|
||||
allocated. Defaults to Device.GPU.
|
||||
"""
|
||||
assert not self._is_allocated
|
||||
assert token_ids
|
||||
blocks = self._allocate_blocks_for_token_ids(prev_block=None,
|
||||
token_ids=token_ids,
|
||||
device=device)
|
||||
self.update(blocks)
|
||||
self._num_full_slots = len(token_ids)
|
||||
|
||||
def update(self, blocks: List[Block]) -> None:
|
||||
"""Resets the table to the newly provided blocks
|
||||
(with their corresponding block ids)
|
||||
"""
|
||||
self._blocks.update(blocks)
|
||||
|
||||
def get_content_hashes(self) -> List[bytes]:
|
||||
"""Returns block-level content hashes for full blocks in order."""
|
||||
content_hashes: List[bytes] = []
|
||||
for block in self._blocks:
|
||||
block_hash = block.content_hash
|
||||
if block_hash is not None:
|
||||
content_hashes.append(block_hash)
|
||||
return content_hashes
|
||||
|
||||
def append_token_ids(self,
|
||||
token_ids: List[int],
|
||||
num_lookahead_slots: int = 0,
|
||||
num_computed_slots: Optional[int] = None) -> None:
|
||||
"""Appends a sequence of token IDs to the existing blocks in the
|
||||
BlockTable.
|
||||
|
||||
This method appends the given sequence of token IDs to the existing
|
||||
blocks in the BlockTable. If there is not enough space in the existing
|
||||
blocks, new blocks are allocated using the `ensure_num_empty_slots`
|
||||
method to accommodate the additional tokens.
|
||||
|
||||
The token IDs are divided into chunks of size `block_size` (except for
|
||||
the first chunk, which may be smaller), and each chunk is appended to a
|
||||
separate block.
|
||||
|
||||
Args:
|
||||
token_ids (List[int]): The sequence of token IDs to be appended.
|
||||
num_computed_slots (Optional[int]): The number of KV cache slots
|
||||
that are already filled (computed).
|
||||
When sliding window is enabled, this is used to compute how many
|
||||
blocks to drop at the front of the sequence.
|
||||
Without sliding window, None can be passed.
|
||||
Without chunked prefill, it should be the same as
|
||||
_num_full_slots.
|
||||
"""
|
||||
assert self._is_allocated, "no blocks have been allocated"
|
||||
assert len(self._blocks) > 0
|
||||
|
||||
# Drop blocks that are no longer needed due to sliding window
|
||||
if self._max_block_sliding_window is not None:
|
||||
null_block = self._allocator.allocate_or_get_null_block()
|
||||
assert num_computed_slots is not None
|
||||
end_block_idx = (num_computed_slots //
|
||||
self._block_size) - self._max_block_sliding_window
|
||||
for idx in range(0, end_block_idx):
|
||||
b = self._blocks[idx]
|
||||
if b is not null_block:
|
||||
self._allocator.free(b)
|
||||
self._blocks[idx] = null_block
|
||||
|
||||
# Ensure there are enough empty slots for the new tokens plus
|
||||
# lookahead slots
|
||||
self.ensure_num_empty_slots(num_empty_slots=len(token_ids) +
|
||||
num_lookahead_slots)
|
||||
|
||||
# Update the blocks with the new tokens
|
||||
first_block_idx = self._num_full_slots // self._block_size
|
||||
token_blocks = self._chunk_token_blocks_for_append(token_ids)
|
||||
|
||||
for i, token_block in enumerate(token_blocks):
|
||||
self._blocks.append_token_ids(first_block_idx + i, token_block)
|
||||
|
||||
self._num_full_slots += len(token_ids)
|
||||
|
||||
def ensure_num_empty_slots(self, num_empty_slots: int) -> None:
|
||||
"""Ensures that the BlockTable has at least the specified number of
|
||||
empty slots available.
|
||||
|
||||
This method checks if the BlockTable has enough empty slots (i.e.,
|
||||
available space) to accommodate the requested number of tokens. If not,
|
||||
it allocates additional blocks on the GPU to ensure that the required
|
||||
number of empty slots is available.
|
||||
|
||||
Args:
|
||||
num_empty_slots (int): The minimum number of empty slots required.
|
||||
"""
|
||||
# Currently the block table only supports
|
||||
# appending tokens to GPU blocks.
|
||||
device = Device.GPU
|
||||
assert self._is_allocated
|
||||
|
||||
if self._num_empty_slots >= num_empty_slots:
|
||||
return
|
||||
|
||||
slots_to_allocate = num_empty_slots - self._num_empty_slots
|
||||
blocks_to_allocate = cdiv(slots_to_allocate, self._block_size)
|
||||
|
||||
for _ in range(blocks_to_allocate):
|
||||
assert len(self._blocks) > 0
|
||||
self._blocks.append(
|
||||
self._allocator.allocate_mutable_block(
|
||||
prev_block=self._blocks[-1], device=device))
|
||||
|
||||
def fork(self) -> "BlockTable":
|
||||
"""Creates a new BlockTable instance with a copy of the blocks from the
|
||||
current instance.
|
||||
|
||||
This method creates a new BlockTable instance with the same block size,
|
||||
block allocator, and a copy of the blocks from the current instance. The
|
||||
new BlockTable has its own independent set of blocks, but shares the
|
||||
same underlying memory allocation with the original BlockTable.
|
||||
|
||||
Returns:
|
||||
BlockTable: A new BlockTable instance with a copy of the blocks from
|
||||
the current instance.
|
||||
"""
|
||||
assert self._is_allocated
|
||||
assert len(self._blocks) > 0
|
||||
forked_blocks = self._allocator.fork(self._blocks[-1])
|
||||
return BlockTable(
|
||||
block_size=self._block_size,
|
||||
block_allocator=self._allocator,
|
||||
_blocks=forked_blocks,
|
||||
max_block_sliding_window=self._max_block_sliding_window,
|
||||
cache_namespace=self._cache_namespace,
|
||||
)
|
||||
|
||||
def free(self) -> None:
|
||||
"""Frees the memory occupied by the blocks in the BlockTable.
|
||||
|
||||
This method iterates over all the blocks in the `_blocks` list and calls
|
||||
the `free` method of the `_allocator` object to release the memory
|
||||
occupied by each block. After freeing all the blocks, the `_blocks` list
|
||||
is set to `None`.
|
||||
"""
|
||||
for block in self.blocks:
|
||||
self._allocator.free(block)
|
||||
self._blocks.reset()
|
||||
|
||||
@property
|
||||
def physical_block_ids(self) -> List[int]:
|
||||
"""Returns a list of physical block indices for the blocks in the
|
||||
BlockTable.
|
||||
|
||||
This property returns a list of integers, where each integer represents
|
||||
the physical block index of a corresponding block in the `_blocks` list.
|
||||
The physical block index is a unique identifier for the memory location
|
||||
occupied by the block.
|
||||
|
||||
Returns:
|
||||
List[int]: A list of physical block indices for the blocks in the
|
||||
BlockTable.
|
||||
"""
|
||||
return self._blocks.ids()
|
||||
|
||||
def get_unseen_token_ids(self, sequence_token_ids: List[int]) -> List[int]:
|
||||
"""Get the number of "unseen" tokens in the sequence.
|
||||
|
||||
Unseen tokens are tokens in the sequence corresponding to this block
|
||||
table, but are not yet appended to this block table.
|
||||
|
||||
Args:
|
||||
sequence_token_ids (List[int]): The list of token ids in the
|
||||
sequence.
|
||||
|
||||
Returns:
|
||||
List[int]: The postfix of sequence_token_ids that has not yet been
|
||||
appended to the block table.
|
||||
"""
|
||||
|
||||
# Since the block table is append-only, the unseen token ids are the
|
||||
# ones after the appended ones.
|
||||
return sequence_token_ids[self.num_full_slots:]
|
||||
|
||||
def _allocate_blocks_for_token_ids(self, prev_block: Optional[Block],
|
||||
token_ids: List[int],
|
||||
device: Device) -> List[Block]:
|
||||
blocks: List[Block] = []
|
||||
|
||||
block_token_ids = []
|
||||
tail_token_ids = []
|
||||
for cur_token_ids in chunk_list(token_ids, self._block_size):
|
||||
if len(cur_token_ids) == self._block_size:
|
||||
block_token_ids.append(cur_token_ids)
|
||||
else:
|
||||
tail_token_ids.append(cur_token_ids)
|
||||
|
||||
if block_token_ids:
|
||||
blocks.extend(self._allocate_immutable_blocks(
|
||||
prev_block=prev_block,
|
||||
block_token_ids=block_token_ids,
|
||||
device=device))
|
||||
prev_block = blocks[-1]
|
||||
|
||||
if tail_token_ids:
|
||||
assert len(tail_token_ids) == 1
|
||||
cur_token_ids = tail_token_ids[0]
|
||||
|
||||
block = self._allocate_mutable_block(prev_block=prev_block,
|
||||
device=device)
|
||||
block.append_token_ids(cur_token_ids)
|
||||
|
||||
blocks.append(block)
|
||||
|
||||
return blocks
|
||||
|
||||
def _allocate_mutable_block(self, prev_block: Optional[Block],
|
||||
device: Device) -> Block:
|
||||
if self._cache_namespace is None:
|
||||
return self._allocator.allocate_mutable_block(
|
||||
prev_block=prev_block, device=device)
|
||||
|
||||
with_cache_namespace = getattr(
|
||||
self._allocator, "allocate_mutable_block_with_cache_namespace",
|
||||
None)
|
||||
if callable(with_cache_namespace):
|
||||
return with_cache_namespace(
|
||||
prev_block=prev_block,
|
||||
cache_namespace=self._cache_namespace,
|
||||
device=device)
|
||||
|
||||
backend_allocators = getattr(self._allocator, "_allocators", None)
|
||||
if isinstance(backend_allocators, dict):
|
||||
device_allocator = backend_allocators.get(device)
|
||||
if device_allocator is not None:
|
||||
with_cache_namespace = getattr(
|
||||
device_allocator,
|
||||
"allocate_mutable_block_with_cache_namespace", None)
|
||||
if callable(with_cache_namespace):
|
||||
return with_cache_namespace(
|
||||
prev_block=prev_block,
|
||||
cache_namespace=self._cache_namespace)
|
||||
|
||||
return self._allocator.allocate_mutable_block(
|
||||
prev_block=prev_block, device=device)
|
||||
|
||||
def _allocate_immutable_blocks(self,
|
||||
prev_block: Optional[Block],
|
||||
block_token_ids: List[List[int]],
|
||||
device: Device) -> List[Block]:
|
||||
if self._cache_namespace is None:
|
||||
return self._allocator.allocate_immutable_blocks(
|
||||
prev_block,
|
||||
block_token_ids=block_token_ids,
|
||||
device=device)
|
||||
|
||||
with_cache_namespace = getattr(
|
||||
self._allocator, "allocate_immutable_blocks_with_cache_namespace", None)
|
||||
if callable(with_cache_namespace):
|
||||
return with_cache_namespace(
|
||||
prev_block=prev_block,
|
||||
block_token_ids=block_token_ids,
|
||||
cache_namespace=self._cache_namespace,
|
||||
device=device)
|
||||
|
||||
backend_allocator = getattr(self._allocator, "_allocators", None)
|
||||
if isinstance(backend_allocator, dict):
|
||||
device_allocator = backend_allocator.get(device)
|
||||
if device_allocator is not None:
|
||||
with_cache_namespace = getattr(
|
||||
device_allocator,
|
||||
"allocate_immutable_blocks_with_cache_namespace",
|
||||
None)
|
||||
if callable(with_cache_namespace):
|
||||
return with_cache_namespace(
|
||||
prev_block=prev_block,
|
||||
block_token_ids=block_token_ids,
|
||||
cache_namespace=self._cache_namespace)
|
||||
|
||||
# Fallback: keep behavior identical when no namespace-aware allocator
|
||||
# is available.
|
||||
return self._allocator.allocate_immutable_blocks(
|
||||
prev_block,
|
||||
block_token_ids=block_token_ids,
|
||||
device=device)
|
||||
|
||||
def _get_all_token_ids(self) -> List[int]:
|
||||
# NOTE: This function is O(seq_len); use sparingly.
|
||||
token_ids: List[int] = []
|
||||
|
||||
if not self._is_allocated:
|
||||
return token_ids
|
||||
|
||||
for block in self.blocks:
|
||||
token_ids.extend(block.token_ids)
|
||||
|
||||
return token_ids
|
||||
|
||||
def _get_num_token_ids(self) -> int:
|
||||
res = 0
|
||||
for block in self.blocks:
|
||||
res += len(block.token_ids)
|
||||
|
||||
return res
|
||||
|
||||
@property
|
||||
def _is_allocated(self) -> bool:
|
||||
return len(self._blocks) > 0
|
||||
|
||||
@property
|
||||
def blocks(self) -> List[Block]:
|
||||
return self._blocks.list()
|
||||
|
||||
@property
|
||||
def _num_empty_slots(self) -> int:
|
||||
assert self._is_allocated
|
||||
return len(self._blocks) * self._block_size - self._num_full_slots
|
||||
|
||||
@property
|
||||
def num_full_slots(self) -> int:
|
||||
"""Returns the total number of tokens currently stored in the
|
||||
BlockTable.
|
||||
|
||||
Returns:
|
||||
int: The total number of tokens currently stored in the BlockTable.
|
||||
"""
|
||||
return self._num_full_slots
|
||||
|
||||
def get_num_blocks_touched_by_append_slots(
|
||||
self, token_ids: List[int], num_lookahead_slots: int) -> int:
|
||||
"""Determine how many blocks will be "touched" by appending the token
|
||||
ids.
|
||||
|
||||
This is required for the scheduler to determine whether a sequence can
|
||||
continue generation, or if it must be preempted.
|
||||
"""
|
||||
# Math below is equivalent to:
|
||||
# all_token_ids = token_ids + [-1] * num_lookahead_slots
|
||||
# token_blocks = self._chunk_token_blocks_for_append(all_token_ids)
|
||||
# return len(token_blocks)
|
||||
|
||||
num_token_ids = len(token_ids) + num_lookahead_slots
|
||||
first_chunk_size = self._block_size - (self._num_full_slots %
|
||||
self._block_size)
|
||||
num_token_blocks = (1 + math.ceil(
|
||||
(num_token_ids - first_chunk_size) / self._block_size))
|
||||
return num_token_blocks
|
||||
|
||||
def _chunk_token_blocks_for_append(
|
||||
self, token_ids: List[int]) -> List[List[int]]:
|
||||
"""Split the token ids into block-sized chunks so they can be easily
|
||||
appended to blocks. The first such "token block" may have less token ids
|
||||
than the block size, since the last allocated block may be partially
|
||||
full.
|
||||
|
||||
If no token ids are provided, then no chunks are returned.
|
||||
"""
|
||||
|
||||
if not token_ids:
|
||||
return []
|
||||
|
||||
first_chunk_size = self._block_size - (self._num_full_slots %
|
||||
self._block_size)
|
||||
token_blocks = [token_ids[:first_chunk_size]]
|
||||
token_blocks.extend(
|
||||
chunk_list(token_ids[first_chunk_size:], self._block_size))
|
||||
return token_blocks
|
||||
475
vllm_overrides/core/block/cpu_gpu_block_allocator.py
Normal file
475
vllm_overrides/core/block/cpu_gpu_block_allocator.py
Normal file
@@ -0,0 +1,475 @@
|
||||
from typing import Dict, FrozenSet, List, Optional, Tuple
|
||||
|
||||
from vllm.core.block.cpu_kv_content_cache import (CpuKvContentCache,
|
||||
cpu_kv_offload_enabled)
|
||||
from vllm.core.block.interfaces import (Block, BlockAllocator, BlockId,
|
||||
DeviceAwareBlockAllocator)
|
||||
from vllm.core.block.naive_block import NaiveBlock, NaiveBlockAllocator
|
||||
from vllm.core.block.prefix_caching_block import PrefixCachingBlockAllocator
|
||||
from vllm.utils import Device
|
||||
|
||||
|
||||
class CpuGpuBlockAllocator(DeviceAwareBlockAllocator):
|
||||
"""A block allocator that can allocate blocks on both CPU and GPU memory.
|
||||
|
||||
This class implements the `DeviceAwareBlockAllocator` interface and provides
|
||||
functionality for allocating and managing blocks of memory on both CPU and
|
||||
GPU devices.
|
||||
|
||||
The `CpuGpuBlockAllocator` maintains separate memory pools for CPU and GPU
|
||||
blocks, and allows for allocation, deallocation, forking, and swapping of
|
||||
blocks across these memory pools.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
allocator_type: str,
|
||||
num_gpu_blocks: int,
|
||||
num_cpu_blocks: int,
|
||||
block_size: int,
|
||||
) -> DeviceAwareBlockAllocator:
|
||||
"""Creates a CpuGpuBlockAllocator instance with the specified
|
||||
configuration.
|
||||
|
||||
This static method creates and returns a CpuGpuBlockAllocator instance
|
||||
based on the provided parameters. It initializes the CPU and GPU block
|
||||
allocators with the specified number of blocks, block size, and
|
||||
allocator type.
|
||||
|
||||
Args:
|
||||
allocator_type (str): The type of block allocator to use for CPU
|
||||
and GPU blocks. Currently supported values are "naive" and
|
||||
"prefix_caching".
|
||||
num_gpu_blocks (int): The number of blocks to allocate for GPU
|
||||
memory.
|
||||
num_cpu_blocks (int): The number of blocks to allocate for CPU
|
||||
memory.
|
||||
block_size (int): The size of each block in number of tokens.
|
||||
|
||||
Returns:
|
||||
DeviceAwareBlockAllocator: A CpuGpuBlockAllocator instance with the
|
||||
specified configuration.
|
||||
|
||||
Notes:
|
||||
- The block IDs are assigned contiguously, with GPU block IDs coming
|
||||
before CPU block IDs.
|
||||
"""
|
||||
content_offload = cpu_kv_offload_enabled()
|
||||
if content_offload and allocator_type != "prefix_caching":
|
||||
raise RuntimeError(
|
||||
"BI100_CPU_KV_OFFLOAD=1 requires prefix caching")
|
||||
if content_offload and num_cpu_blocks <= 0:
|
||||
raise RuntimeError(
|
||||
"BI100_CPU_KV_OFFLOAD=1 requires at least one CPU KV block")
|
||||
|
||||
block_ids = list(range(num_gpu_blocks + num_cpu_blocks))
|
||||
gpu_block_ids = block_ids[:num_gpu_blocks]
|
||||
cpu_block_ids = block_ids[num_gpu_blocks:]
|
||||
|
||||
if allocator_type == "naive":
|
||||
gpu_allocator: BlockAllocator = NaiveBlockAllocator(
|
||||
create_block=NaiveBlock, # type: ignore
|
||||
num_blocks=num_gpu_blocks,
|
||||
block_size=block_size,
|
||||
block_ids=gpu_block_ids,
|
||||
)
|
||||
|
||||
cpu_allocator: BlockAllocator = NaiveBlockAllocator(
|
||||
create_block=NaiveBlock, # type: ignore
|
||||
num_blocks=num_cpu_blocks,
|
||||
block_size=block_size,
|
||||
block_ids=cpu_block_ids,
|
||||
)
|
||||
elif allocator_type == "prefix_caching":
|
||||
gpu_allocator = PrefixCachingBlockAllocator(
|
||||
num_blocks=num_gpu_blocks,
|
||||
block_size=block_size,
|
||||
block_ids=gpu_block_ids,
|
||||
)
|
||||
|
||||
cpu_allocator = PrefixCachingBlockAllocator(
|
||||
num_blocks=num_cpu_blocks,
|
||||
block_size=block_size,
|
||||
block_ids=cpu_block_ids,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown allocator type {allocator_type=}")
|
||||
|
||||
return CpuGpuBlockAllocator(
|
||||
cpu_block_allocator=cpu_allocator,
|
||||
gpu_block_allocator=gpu_allocator,
|
||||
cpu_content_cache=(CpuKvContentCache(num_cpu_blocks)
|
||||
if content_offload else None),
|
||||
)
|
||||
|
||||
def __init__(self, cpu_block_allocator: BlockAllocator,
|
||||
gpu_block_allocator: BlockAllocator,
|
||||
cpu_content_cache: Optional[CpuKvContentCache] = None):
|
||||
assert not (
|
||||
cpu_block_allocator.all_block_ids
|
||||
& gpu_block_allocator.all_block_ids
|
||||
), "cpu and gpu block allocators can't have intersection of block ids"
|
||||
|
||||
self._allocators = {
|
||||
Device.CPU: cpu_block_allocator,
|
||||
Device.GPU: gpu_block_allocator,
|
||||
}
|
||||
|
||||
self._swap_mapping: Dict[int, int] = {}
|
||||
self._null_block: Optional[Block] = None
|
||||
self._cpu_content_cache = cpu_content_cache
|
||||
|
||||
self._block_ids_to_allocator: Dict[int, BlockAllocator] = {}
|
||||
for _, allocator in self._allocators.items():
|
||||
for block_id in allocator.all_block_ids:
|
||||
self._block_ids_to_allocator[block_id] = allocator
|
||||
|
||||
if self._cpu_content_cache is not None:
|
||||
if not isinstance(gpu_block_allocator,
|
||||
PrefixCachingBlockAllocator):
|
||||
raise RuntimeError(
|
||||
"CPU KV content tier requires PrefixCachingBlockAllocator")
|
||||
if (self._cpu_content_cache.capacity !=
|
||||
cpu_block_allocator.get_num_total_blocks()):
|
||||
raise RuntimeError(
|
||||
"CPU KV content capacity must cover the complete CPU cache")
|
||||
gpu_block_allocator.set_external_cache_callbacks(
|
||||
claim=self._claim_cpu_content,
|
||||
load=self._stage_cpu_to_gpu,
|
||||
cancel=self._cancel_cpu_claim,
|
||||
store=self._stage_gpu_to_cpu,
|
||||
)
|
||||
|
||||
@property
|
||||
def content_offload_enabled(self) -> bool:
|
||||
return self._cpu_content_cache is not None
|
||||
|
||||
def _claim_cpu_content(self, content_hash: bytes) -> Optional[int]:
|
||||
assert self._cpu_content_cache is not None
|
||||
return self._cpu_content_cache.claim_load(content_hash)
|
||||
|
||||
def _cancel_cpu_claim(self, content_hash: bytes, cpu_slot: int) -> None:
|
||||
assert self._cpu_content_cache is not None
|
||||
self._cpu_content_cache.cancel_load(content_hash, cpu_slot)
|
||||
|
||||
def _stage_cpu_to_gpu(self, content_hash: bytes, cpu_slot: int,
|
||||
gpu_block_id: BlockId) -> None:
|
||||
assert self._cpu_content_cache is not None
|
||||
gpu_slot = self.get_physical_block_id(Device.GPU, gpu_block_id)
|
||||
self._cpu_content_cache.stage_load(
|
||||
content_hash, cpu_slot, gpu_slot)
|
||||
|
||||
def _stage_gpu_to_cpu(self, content_hash: bytes,
|
||||
gpu_block_id: BlockId) -> bool:
|
||||
assert self._cpu_content_cache is not None
|
||||
gpu_slot = self.get_physical_block_id(Device.GPU, gpu_block_id)
|
||||
return self._cpu_content_cache.stage_store(content_hash, gpu_slot)
|
||||
|
||||
def allocate_or_get_null_block(self) -> Block:
|
||||
if self._null_block is None:
|
||||
self._null_block = NullBlock(
|
||||
self.allocate_mutable_block(None, Device.GPU))
|
||||
return self._null_block
|
||||
|
||||
def allocate_mutable_block(self, prev_block: Optional[Block],
|
||||
device: Device) -> Block:
|
||||
"""Allocates a new mutable block on the specified device.
|
||||
|
||||
Args:
|
||||
prev_block (Optional[Block]): The previous block to in the sequence.
|
||||
Used for prefix hashing.
|
||||
device (Device): The device on which to allocate the new block.
|
||||
|
||||
Returns:
|
||||
Block: The newly allocated mutable block.
|
||||
"""
|
||||
return self._allocators[device].allocate_mutable_block(prev_block)
|
||||
|
||||
def allocate_immutable_blocks(self, prev_block: Optional[Block],
|
||||
block_token_ids: List[List[int]],
|
||||
device: Device) -> List[Block]:
|
||||
"""Allocates a new group of immutable blocks with the provided block
|
||||
token IDs on the specified device.
|
||||
|
||||
Args:
|
||||
prev_block (Optional[Block]): The previous block in the sequence.
|
||||
Used for prefix hashing.
|
||||
block_token_ids (List[int]): The list of block token IDs to be
|
||||
stored in the new blocks.
|
||||
device (Device): The device on which to allocate the new block.
|
||||
|
||||
Returns:
|
||||
List[Block]: The newly allocated list of immutable blocks
|
||||
containing the provided block token IDs.
|
||||
"""
|
||||
return self._allocators[device].allocate_immutable_blocks(
|
||||
prev_block, block_token_ids)
|
||||
|
||||
def allocate_immutable_block(self, prev_block: Optional[Block],
|
||||
token_ids: List[int],
|
||||
device: Device) -> Block:
|
||||
"""Allocates a new immutable block with the provided token IDs on the
|
||||
specified device.
|
||||
|
||||
Args:
|
||||
prev_block (Optional[Block]): The previous block in the sequence.
|
||||
Used for prefix hashing.
|
||||
token_ids (List[int]): The list of token IDs to be stored in the new
|
||||
block.
|
||||
device (Device): The device on which to allocate the new block.
|
||||
|
||||
Returns:
|
||||
Block: The newly allocated immutable block containing the provided
|
||||
token IDs.
|
||||
"""
|
||||
return self._allocators[device].allocate_immutable_block(
|
||||
prev_block, token_ids)
|
||||
|
||||
def free(self, block: Block) -> None:
|
||||
"""Frees the memory occupied by the given block.
|
||||
|
||||
Args:
|
||||
block (Block): The block to be freed.
|
||||
"""
|
||||
# Null block should never be freed
|
||||
if isinstance(block, NullBlock):
|
||||
return
|
||||
block_id = block.block_id
|
||||
assert block_id is not None
|
||||
allocator = self._block_ids_to_allocator[block_id]
|
||||
allocator.free(block)
|
||||
|
||||
def fork(self, last_block: Block) -> List[Block]:
|
||||
"""Creates a new sequence of blocks that shares the same underlying
|
||||
memory as the original sequence.
|
||||
|
||||
Args:
|
||||
last_block (Block): The last block in the original sequence.
|
||||
|
||||
Returns:
|
||||
List[Block]: A new list of blocks that shares the same memory as the
|
||||
original sequence.
|
||||
"""
|
||||
# do not attempt to fork the null block
|
||||
assert not isinstance(last_block, NullBlock)
|
||||
block_id = last_block.block_id
|
||||
assert block_id is not None
|
||||
allocator = self._block_ids_to_allocator[block_id]
|
||||
return allocator.fork(last_block)
|
||||
|
||||
def get_num_free_blocks(self, device: Device) -> int:
|
||||
"""Returns the number of free blocks available on the specified device.
|
||||
|
||||
Args:
|
||||
device (Device): The device for which to query the number of free
|
||||
blocks. AssertionError is raised if None is passed.
|
||||
|
||||
Returns:
|
||||
int: The number of free blocks available on the specified device.
|
||||
"""
|
||||
return self._allocators[device].get_num_free_blocks()
|
||||
|
||||
def get_num_total_blocks(self, device: Device) -> int:
|
||||
return self._allocators[device].get_num_total_blocks()
|
||||
|
||||
def get_physical_block_id(self, device: Device, absolute_id: int) -> int:
|
||||
"""Returns the zero-offset block id on certain device given the
|
||||
absolute block id.
|
||||
|
||||
Args:
|
||||
device (Device): The device for which to query relative block id.
|
||||
absolute_id (int): The absolute block id for the block in
|
||||
whole allocator.
|
||||
|
||||
Returns:
|
||||
int: The zero-offset block id on certain device.
|
||||
"""
|
||||
return self._allocators[device].get_physical_block_id(absolute_id)
|
||||
|
||||
def swap(self, blocks: List[Block], src_device: Device,
|
||||
dst_device: Device) -> Dict[int, int]:
|
||||
"""Execute the swap for the given blocks from source_device
|
||||
on to dest_device, save the current swap mapping and append
|
||||
them to the accumulated `self._swap_mapping` for each
|
||||
scheduling move.
|
||||
|
||||
Args:
|
||||
blocks: List of blocks to be swapped.
|
||||
src_device (Device): Device to swap the 'blocks' from.
|
||||
dst_device (Device): Device to swap the 'blocks' to.
|
||||
|
||||
Returns:
|
||||
Dict[int, int]: Swap mapping from source_device
|
||||
on to dest_device.
|
||||
"""
|
||||
if self.content_offload_enabled:
|
||||
raise RuntimeError(
|
||||
"request-level preemption swap cannot share CPU slots with "
|
||||
"BI100_CPU_KV_OFFLOAD")
|
||||
|
||||
src_block_ids = [block.block_id for block in blocks]
|
||||
self._allocators[src_device].swap_out(blocks)
|
||||
self._allocators[dst_device].swap_in(blocks)
|
||||
dst_block_ids = [block.block_id for block in blocks]
|
||||
|
||||
current_swap_mapping: Dict[int, int] = {}
|
||||
for src_block_id, dst_block_id in zip(src_block_ids, dst_block_ids):
|
||||
if src_block_id is not None and dst_block_id is not None:
|
||||
self._swap_mapping[src_block_id] = dst_block_id
|
||||
current_swap_mapping[src_block_id] = dst_block_id
|
||||
return current_swap_mapping
|
||||
|
||||
def get_num_full_blocks_touched(self, blocks: List[Block],
|
||||
device: Device) -> int:
|
||||
"""Returns the number of full blocks that will be touched by
|
||||
swapping in/out the given blocks on to the 'device'.
|
||||
|
||||
Args:
|
||||
blocks: List of blocks to be swapped.
|
||||
device (Device): Device to swap the 'blocks' on.
|
||||
|
||||
Returns:
|
||||
int: the number of full blocks that will be touched by
|
||||
swapping in/out the given blocks on to the 'device'.
|
||||
Non full blocks are ignored when deciding the number
|
||||
of blocks to touch.
|
||||
"""
|
||||
return self._allocators[device].get_num_full_blocks_touched(blocks)
|
||||
|
||||
def clear_copy_on_writes(self) -> List[Tuple[int, int]]:
|
||||
"""Clears the copy-on-write (CoW) state and returns the mapping of
|
||||
source to destination block IDs.
|
||||
|
||||
Returns:
|
||||
List[Tuple[int, int]]: A list mapping source block IDs to
|
||||
destination block IDs.
|
||||
"""
|
||||
# CoW only supported on GPU
|
||||
device = Device.GPU
|
||||
return self._allocators[device].clear_copy_on_writes()
|
||||
|
||||
def mark_blocks_as_accessed(self, block_ids: List[int],
|
||||
now: float) -> None:
|
||||
"""Mark blocks as accessed, only use for prefix caching."""
|
||||
# Prefix caching only supported on GPU.
|
||||
device = Device.GPU
|
||||
return self._allocators[device].mark_blocks_as_accessed(block_ids, now)
|
||||
|
||||
def mark_blocks_as_computed(self, block_ids: List[int]) -> None:
|
||||
"""Mark blocks as accessed, only use for prefix caching."""
|
||||
# Prefix caching only supported on GPU.
|
||||
device = Device.GPU
|
||||
return self._allocators[device].mark_blocks_as_computed(block_ids)
|
||||
|
||||
def get_computed_block_ids(self, prev_computed_block_ids: List[int],
|
||||
block_ids: List[int],
|
||||
skip_last_block_id: bool) -> List[int]:
|
||||
# Prefix caching only supported on GPU.
|
||||
device = Device.GPU
|
||||
return self._allocators[device].get_computed_block_ids(
|
||||
prev_computed_block_ids, block_ids, skip_last_block_id)
|
||||
|
||||
def get_common_computed_block_ids(
|
||||
self, computed_seq_block_ids: List[List[int]]) -> List[int]:
|
||||
# Prefix caching only supported on GPU.
|
||||
device = Device.GPU
|
||||
return self._allocators[device].get_common_computed_block_ids(
|
||||
computed_seq_block_ids)
|
||||
|
||||
@property
|
||||
def all_block_ids(self) -> FrozenSet[int]:
|
||||
return frozenset(self._block_ids_to_allocator.keys())
|
||||
|
||||
def get_prefix_cache_hit_rate(self, device: Device) -> float:
|
||||
"""Prefix cache hit rate. -1 means not supported or disabled."""
|
||||
assert device in self._allocators
|
||||
return self._allocators[device].get_prefix_cache_hit_rate()
|
||||
|
||||
def get_and_reset_swaps(self) -> List[Tuple[int, int]]:
|
||||
"""Returns and clears the mapping of source to destination block IDs.
|
||||
Will be called after every swapping operations for now, and after every
|
||||
schedule when BlockManagerV2 become default. Currently not useful.
|
||||
|
||||
Returns:
|
||||
List[Tuple[int, int]]: A mapping of source to destination block IDs.
|
||||
"""
|
||||
mapping = self._swap_mapping.copy()
|
||||
self._swap_mapping.clear()
|
||||
return list(mapping.items())
|
||||
|
||||
def get_and_reset_prefix_swaps(
|
||||
self) -> Tuple[List[Tuple[int, int]], List[Tuple[int, int]]]:
|
||||
"""Return scheduler-owned (CPU->GPU, GPU->CPU) content maps."""
|
||||
if self._cpu_content_cache is None:
|
||||
return [], []
|
||||
return self._cpu_content_cache.drain_step()
|
||||
|
||||
def begin_prefix_cache_step(self) -> None:
|
||||
if self._cpu_content_cache is not None:
|
||||
self._cpu_content_cache.begin_step()
|
||||
|
||||
|
||||
class NullBlock(Block):
|
||||
"""
|
||||
Null blocks are used as a placeholders for KV cache blocks that have
|
||||
been dropped due to sliding window.
|
||||
This implementation just wraps an ordinary block and prevents it from
|
||||
being modified. It also allows for testing if a block is NullBlock
|
||||
via isinstance().
|
||||
"""
|
||||
|
||||
def __init__(self, proxy: Block):
|
||||
super().__init__()
|
||||
self._proxy = proxy
|
||||
|
||||
def append_token_ids(self, token_ids: List[BlockId]):
|
||||
raise ValueError("null block should not be modified")
|
||||
|
||||
@property
|
||||
def block_id(self):
|
||||
return self._proxy.block_id
|
||||
|
||||
@block_id.setter
|
||||
def block_id(self, value: Optional[BlockId]):
|
||||
raise ValueError("null block should not be modified")
|
||||
|
||||
@property
|
||||
def token_ids(self) -> List[BlockId]:
|
||||
return self._proxy.token_ids
|
||||
|
||||
@property
|
||||
def num_tokens_total(self) -> int:
|
||||
raise NotImplementedError(
|
||||
"num_tokens_total is not used for null block")
|
||||
|
||||
@property
|
||||
def num_empty_slots(self) -> BlockId:
|
||||
return self._proxy.num_empty_slots
|
||||
|
||||
@property
|
||||
def is_full(self):
|
||||
return self._proxy.is_full
|
||||
|
||||
@property
|
||||
def prev_block(self):
|
||||
return self._proxy.prev_block
|
||||
|
||||
@property
|
||||
def computed(self):
|
||||
return self._proxy.computed
|
||||
|
||||
@computed.setter
|
||||
def computed(self, value):
|
||||
self._proxy.computed = value
|
||||
|
||||
@property
|
||||
def last_accessed(self) -> float:
|
||||
return self._proxy.last_accessed
|
||||
|
||||
@last_accessed.setter
|
||||
def last_accessed(self, last_accessed_ts: float):
|
||||
self._proxy.last_accessed = last_accessed_ts
|
||||
|
||||
@property
|
||||
def content_hash(self):
|
||||
return self._proxy.content_hash
|
||||
255
vllm_overrides/core/block/cpu_kv_content_cache.py
Normal file
255
vllm_overrides/core/block/cpu_kv_content_cache.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""Scheduler-owned content index for an inclusive CPU KV cache tier."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List, Mapping, Optional, Set, Tuple
|
||||
|
||||
|
||||
ContentHash = bytes
|
||||
SwapMapping = List[Tuple[int, int]]
|
||||
|
||||
|
||||
def cpu_kv_offload_enabled(
|
||||
environ: Optional[Mapping[str, str]] = None,
|
||||
) -> bool:
|
||||
"""Read the experimental selector without accepting ambiguous values."""
|
||||
source = os.environ if environ is None else environ
|
||||
value = source.get("BI100_CPU_KV_OFFLOAD", "0")
|
||||
if value == "0":
|
||||
return False
|
||||
if value == "1":
|
||||
return True
|
||||
raise RuntimeError(
|
||||
"BI100_CPU_KV_OFFLOAD must be exactly '0' or '1', "
|
||||
f"got {value!r}")
|
||||
|
||||
|
||||
class CpuKvContentCache:
|
||||
"""Track immutable KV blocks held in the worker's pinned CPU cache.
|
||||
|
||||
The scheduler owns this metadata and sends identical physical block maps
|
||||
to every tensor-parallel worker. CPU copies are inclusive: loading a block
|
||||
back to GPU does not remove its CPU entry. Slots touched by either transfer
|
||||
direction are pinned for the whole scheduling step so a D2H destination
|
||||
can never overwrite an H2D source before workers execute the maps.
|
||||
"""
|
||||
|
||||
def __init__(self, capacity: int) -> None:
|
||||
if capacity <= 0:
|
||||
raise ValueError("CPU KV content cache capacity must be positive")
|
||||
self.capacity = capacity
|
||||
self._hash_to_slot: Dict[ContentHash, int] = {}
|
||||
self._slot_to_hash: Dict[int, ContentHash] = {}
|
||||
self._ready_slots: Set[int] = set()
|
||||
self._lru: OrderedDict[int, None] = OrderedDict()
|
||||
self._free_slots = list(range(capacity))
|
||||
heapq.heapify(self._free_slots)
|
||||
|
||||
self._step_slots_in_use: Set[int] = set()
|
||||
self._step_load_slots: Set[int] = set()
|
||||
self._step_h2d: Dict[int, int] = {}
|
||||
self._step_d2h: Dict[int, int] = {}
|
||||
self._deferred_d2h: Dict[int, ContentHash] = {}
|
||||
self._deferred_hashes: Set[ContentHash] = set()
|
||||
self._pending_ready_slots: Set[int] = set()
|
||||
|
||||
self.hits = 0
|
||||
self.misses = 0
|
||||
self.stores = 0
|
||||
self.deduplicated_stores = 0
|
||||
self.evictions = 0
|
||||
self.skipped_stores = 0
|
||||
|
||||
@staticmethod
|
||||
def _validate_hash(content_hash: ContentHash) -> None:
|
||||
if not isinstance(content_hash, bytes) or len(content_hash) != 32:
|
||||
raise ValueError("CPU KV cache key must be a 32-byte content hash")
|
||||
|
||||
@staticmethod
|
||||
def _validate_block_id(name: str, block_id: int) -> None:
|
||||
if not isinstance(block_id, int) or isinstance(block_id, bool):
|
||||
raise TypeError(f"{name} must be an integer")
|
||||
if block_id < 0:
|
||||
raise ValueError(f"{name} must be non-negative")
|
||||
|
||||
def _touch(self, slot: int) -> None:
|
||||
self._lru.pop(slot, None)
|
||||
self._lru[slot] = None
|
||||
|
||||
def _select_store_slot(self) -> Optional[int]:
|
||||
if self._free_slots:
|
||||
return heapq.heappop(self._free_slots)
|
||||
for slot in self._lru:
|
||||
if slot not in self._step_slots_in_use:
|
||||
return slot
|
||||
return None
|
||||
|
||||
def _commit_store(self, content_hash: ContentHash,
|
||||
gpu_block: int, slot: int) -> None:
|
||||
old_hash = self._slot_to_hash.get(slot)
|
||||
if old_hash is not None:
|
||||
if slot in self._step_slots_in_use:
|
||||
raise RuntimeError("selected an in-use CPU KV slot for eviction")
|
||||
del self._hash_to_slot[old_hash]
|
||||
self._ready_slots.discard(slot)
|
||||
self.evictions += 1
|
||||
|
||||
if slot in self._step_h2d:
|
||||
raise RuntimeError(
|
||||
"a CPU KV slot cannot be an H2D source and D2H destination "
|
||||
"in one scheduler step")
|
||||
if slot in self._step_d2h.values():
|
||||
raise RuntimeError(f"duplicate D2H destination CPU slot {slot}")
|
||||
|
||||
self._hash_to_slot[content_hash] = slot
|
||||
self._slot_to_hash[slot] = content_hash
|
||||
self._ready_slots.discard(slot)
|
||||
self._step_slots_in_use.add(slot)
|
||||
self._step_d2h[gpu_block] = slot
|
||||
self._touch(slot)
|
||||
self.stores += 1
|
||||
|
||||
def begin_step(self) -> None:
|
||||
"""Publish D2H stores returned by the preceding synchronous step."""
|
||||
if (self._step_slots_in_use or self._step_h2d or self._step_d2h
|
||||
or self._deferred_d2h or self._deferred_hashes):
|
||||
raise RuntimeError("cannot begin a CPU KV step before draining it")
|
||||
self._ready_slots.update(self._pending_ready_slots)
|
||||
self._pending_ready_slots.clear()
|
||||
|
||||
def _require_step_started(self) -> None:
|
||||
if self._pending_ready_slots:
|
||||
raise RuntimeError(
|
||||
"CPU KV step must begin before content lookup or eviction")
|
||||
|
||||
def claim_load(self, content_hash: ContentHash) -> Optional[int]:
|
||||
"""Pin and return a ready CPU source for this scheduling step."""
|
||||
self._validate_hash(content_hash)
|
||||
self._require_step_started()
|
||||
slot = self._hash_to_slot.get(content_hash)
|
||||
if slot is None or slot not in self._ready_slots:
|
||||
self.misses += 1
|
||||
return None
|
||||
if slot in self._step_slots_in_use:
|
||||
raise RuntimeError(
|
||||
f"CPU KV slot {slot} was claimed twice in one scheduler step")
|
||||
self._step_slots_in_use.add(slot)
|
||||
self._step_load_slots.add(slot)
|
||||
self._touch(slot)
|
||||
self.hits += 1
|
||||
return slot
|
||||
|
||||
def cancel_load(self, content_hash: ContentHash, cpu_slot: int) -> None:
|
||||
"""Release a claim when GPU allocation fails before H2D is staged."""
|
||||
self._validate_hash(content_hash)
|
||||
self._validate_block_id("cpu_slot", cpu_slot)
|
||||
if self._hash_to_slot.get(content_hash) != cpu_slot:
|
||||
raise RuntimeError("CPU KV load cancellation key/slot mismatch")
|
||||
if cpu_slot in self._step_h2d:
|
||||
raise RuntimeError("cannot cancel a CPU KV load after H2D staging")
|
||||
if cpu_slot not in self._step_slots_in_use:
|
||||
raise RuntimeError("cannot cancel an unclaimed CPU KV load")
|
||||
self._step_slots_in_use.remove(cpu_slot)
|
||||
self._step_load_slots.remove(cpu_slot)
|
||||
|
||||
def stage_load(self, content_hash: ContentHash, cpu_slot: int,
|
||||
gpu_block: int) -> None:
|
||||
"""Stage one CPU-to-GPU promotion after the GPU slot is reserved."""
|
||||
self._validate_hash(content_hash)
|
||||
self._validate_block_id("cpu_slot", cpu_slot)
|
||||
self._validate_block_id("gpu_block", gpu_block)
|
||||
if self._hash_to_slot.get(content_hash) != cpu_slot:
|
||||
raise RuntimeError("CPU KV load key/slot mismatch")
|
||||
if cpu_slot not in self._ready_slots:
|
||||
raise RuntimeError("CPU KV load source is not ready")
|
||||
if cpu_slot not in self._step_slots_in_use:
|
||||
raise RuntimeError("CPU KV load source was not claimed")
|
||||
if cpu_slot in self._step_h2d:
|
||||
raise RuntimeError(f"duplicate H2D source CPU slot {cpu_slot}")
|
||||
if gpu_block in self._step_h2d.values():
|
||||
raise RuntimeError(f"duplicate H2D destination GPU block {gpu_block}")
|
||||
if cpu_slot in self._step_d2h.values():
|
||||
raise RuntimeError(
|
||||
"a CPU KV slot cannot be an H2D source and D2H destination "
|
||||
"in one scheduler step")
|
||||
self._step_h2d[cpu_slot] = gpu_block
|
||||
|
||||
def stage_store(self, content_hash: ContentHash,
|
||||
gpu_block: int) -> bool:
|
||||
"""Stage a lazy GPU-to-CPU copy for an evicted immutable block."""
|
||||
self._validate_hash(content_hash)
|
||||
self._validate_block_id("gpu_block", gpu_block)
|
||||
self._require_step_started()
|
||||
if (content_hash in self._hash_to_slot
|
||||
or content_hash in self._deferred_hashes):
|
||||
slot = self._hash_to_slot.get(content_hash)
|
||||
if slot is not None:
|
||||
self._touch(slot)
|
||||
self.deduplicated_stores += 1
|
||||
return False
|
||||
if gpu_block in self._step_d2h or gpu_block in self._deferred_d2h:
|
||||
raise RuntimeError(f"duplicate D2H source GPU block {gpu_block}")
|
||||
|
||||
if self._free_slots:
|
||||
self._commit_store(
|
||||
content_hash, gpu_block, heapq.heappop(self._free_slots))
|
||||
return True
|
||||
|
||||
# Do not replace resident content until every lookup in this scheduler
|
||||
# step is known. A later H2D claim can refer to any current LRU entry.
|
||||
self._deferred_d2h[gpu_block] = content_hash
|
||||
self._deferred_hashes.add(content_hash)
|
||||
return True
|
||||
|
||||
def _resolve_deferred_stores(self) -> None:
|
||||
if self._step_load_slots:
|
||||
self.skipped_stores += len(self._deferred_d2h)
|
||||
else:
|
||||
for gpu_block, content_hash in self._deferred_d2h.items():
|
||||
slot = self._select_store_slot()
|
||||
if slot is None:
|
||||
self.skipped_stores += 1
|
||||
continue
|
||||
self._commit_store(content_hash, gpu_block, slot)
|
||||
self._deferred_d2h.clear()
|
||||
self._deferred_hashes.clear()
|
||||
|
||||
def drain_step(self) -> Tuple[SwapMapping, SwapMapping]:
|
||||
"""Finalize this synchronous step and return (H2D, D2H) maps."""
|
||||
self._resolve_deferred_stores()
|
||||
transfer_slots = (
|
||||
set(self._step_h2d) | set(self._step_d2h.values()))
|
||||
if transfer_slots != self._step_slots_in_use:
|
||||
raise RuntimeError(
|
||||
"CPU KV scheduler step contains an uncommitted slot claim")
|
||||
if set(self._step_h2d) & set(self._step_d2h.values()):
|
||||
raise RuntimeError(
|
||||
"CPU KV scheduler step reuses a CPU slot across directions")
|
||||
if set(self._step_h2d) != self._step_load_slots:
|
||||
raise RuntimeError(
|
||||
"CPU KV scheduler step contains an unstaged load claim")
|
||||
|
||||
swap_in = sorted(self._step_h2d.items())
|
||||
swap_out = sorted(self._step_d2h.items())
|
||||
self._pending_ready_slots.update(self._step_d2h.values())
|
||||
self._step_h2d.clear()
|
||||
self._step_d2h.clear()
|
||||
self._step_slots_in_use.clear()
|
||||
self._step_load_slots.clear()
|
||||
return swap_in, swap_out
|
||||
|
||||
def resident_slot(self, content_hash: ContentHash) -> Optional[int]:
|
||||
self._validate_hash(content_hash)
|
||||
return self._hash_to_slot.get(content_hash)
|
||||
|
||||
def is_ready(self, content_hash: ContentHash) -> bool:
|
||||
self._validate_hash(content_hash)
|
||||
slot = self._hash_to_slot.get(content_hash)
|
||||
return slot is not None and slot in self._ready_slots
|
||||
|
||||
@property
|
||||
def resident_count(self) -> int:
|
||||
return len(self._hash_to_slot)
|
||||
1183
vllm_overrides/core/block/prefix_caching_block.py
Normal file
1183
vllm_overrides/core/block/prefix_caching_block.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user