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:
project6-dev
2026-08-11 03:55:38 +00:00
parent 81875fff52
commit 5862708b32
86 changed files with 24702 additions and 9860 deletions

View 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

View 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

View 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)

File diff suppressed because it is too large Load Diff