# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project from collections.abc import Iterable import torch from vllm.triton_utils import tl, triton from vllm.v1.attention.backends.utils import PAD_SLOT_ID from vllm.v1.worker.gpu.buffer_utils import ( FusedStagedWriter, StagedWriteTensor, UvaBackedTensor, _load_ptr, ) class BlockTables: def __init__( self, block_sizes: list[int], max_num_reqs: int, max_num_batched_tokens: int, max_num_blocks_per_group: list[int], device: torch.device, kernel_block_sizes: list[int], cp_size: int = 1, cp_rank: int = 0, cp_interleave: int = 1, slot_mapping_enabled: list[bool] | None = None, cp_sharded: list[bool] | None = None, ): self.block_sizes = block_sizes self.kernel_block_sizes = kernel_block_sizes self.max_num_reqs = max_num_reqs self.max_num_batched_tokens = max_num_batched_tokens self.device = device self.cp_size = cp_size self.cp_rank = cp_rank self.cp_interleave = cp_interleave self.num_kv_cache_groups = len(self.block_sizes) assert len(max_num_blocks_per_group) == self.num_kv_cache_groups if slot_mapping_enabled is None: slot_mapping_enabled = [True] * self.num_kv_cache_groups assert len(slot_mapping_enabled) == self.num_kv_cache_groups self._slot_mapping_enabled = slot_mapping_enabled # Per group: True = positions are round-robined across the cp_size # ranks (this rank stores 1/cp_size of them); False = DCP-replicated # group, every rank stores every position at its plain block geometry. if cp_sharded is None: cp_sharded = [True] * self.num_kv_cache_groups assert len(cp_sharded) == self.num_kv_cache_groups self._cp_sharded = cp_sharded self.blocks_per_kv_block = [ bs // kbs for bs, kbs in zip(block_sizes, kernel_block_sizes) ] # num_kv_cache_groups x [max_num_reqs, max_num_blocks] self.block_tables: list[StagedWriteTensor] = [] for i in range(self.num_kv_cache_groups): max_num_blocks = max_num_blocks_per_group[i] * self.blocks_per_kv_block[i] block_table = StagedWriteTensor( (self.max_num_reqs, max_num_blocks), dtype=torch.int32, device=device ) self.block_tables.append(block_table) self.num_blocks = UvaBackedTensor( (self.num_kv_cache_groups, self.max_num_reqs), dtype=torch.int32, ) self.fused_writer: FusedStagedWriter | None = None if self.num_kv_cache_groups > 1: # Only the multi-group path uses the fused writer. self.fused_writer = FusedStagedWriter( self.device, self.num_kv_cache_groups * self.max_num_reqs ) # Block tables used for model's forward pass. # num_kv_cache_groups x [max_num_reqs, max_num_blocks] self.input_block_tables: list[torch.Tensor] = [ torch.zeros_like(b.gpu) for b in self.block_tables ] self.slot_mappings = torch.zeros( self.num_kv_cache_groups, self.max_num_batched_tokens, dtype=torch.int64, device=self.device, ) self.init_block_table_layout_tensors() def _make_ptr_tensor(self, x: Iterable[torch.Tensor]) -> torch.Tensor: # NOTE(woosuk): Use uint64 instead of int64 to cover all possible addresses. return torch.tensor( [t.data_ptr() for t in x], dtype=torch.uint64, device=self.device ) def init_block_table_layout_tensors(self) -> None: self.block_table_ptrs = self._make_ptr_tensor( [b.gpu for b in self.block_tables] ) self.block_table_strides = torch.tensor( [b.gpu.stride(0) for b in self.block_tables], dtype=torch.int64, device=self.device, ) self.block_sizes_tensor = torch.tensor( self.block_sizes, dtype=torch.int32, device=self.device ) self.kernel_block_sizes_tensor = torch.tensor( self.kernel_block_sizes, dtype=torch.int32, device=self.device ) self.slot_mapping_enabled = torch.tensor( self._slot_mapping_enabled, dtype=torch.bool, device=self.device ) self.cp_sharded = torch.tensor( self._cp_sharded, dtype=torch.bool, device=self.device ) self.input_block_table_ptrs = self._make_ptr_tensor(self.input_block_tables) def append_block_ids( self, req_index: int, new_block_ids: tuple[list[int], ...], overwrite: bool, ) -> None: for i in range(self.num_kv_cache_groups): start = self.num_blocks.np[i, req_index] if not overwrite else 0 block_ids = new_block_ids[i] bpk = self.blocks_per_kv_block[i] if bpk > 1: block_ids = [b * bpk + k for b in block_ids for k in range(bpk)] end = start + len(block_ids) row_capacity = self.block_tables[i].gpu.shape[1] if end > row_capacity: raise RuntimeError( f"Block table write for request {req_index}, group {i} exceeds " f"row capacity ({end} > {row_capacity})" ) self.block_tables[i].stage_write(req_index, start, block_ids) self.num_blocks.np[i, req_index] = end def apply_staged_writes(self) -> None: if self.num_kv_cache_groups == 0: return if self.num_kv_cache_groups == 1: # Single group: write directly, skipping the per-write group lookup. self.block_tables[0].apply_write() elif self.num_kv_cache_groups > 1: # Multiple groups: apply all block tables with one fused kernel. assert self.fused_writer is not None self.fused_writer.apply( self.block_tables, self.block_table_ptrs, self.block_table_strides ) self.num_blocks.copy_to_uva() def gather_block_tables( self, idx_mapping: torch.Tensor, num_reqs_padded: int, out: tuple[torch.Tensor, ...] | None = None, out_ptrs: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...]: if self.num_kv_cache_groups == 0: return () if out is None: out = tuple(self.input_block_tables) out_ptrs = self.input_block_table_ptrs else: assert out_ptrs is not None assert len(out) == self.num_kv_cache_groups num_reqs = idx_mapping.shape[0] # Launch kernel with num_reqs_padded to fuse zeroing of padded rows. _gather_block_tables_kernel[(self.num_kv_cache_groups, num_reqs_padded)]( idx_mapping, self.block_table_ptrs, out_ptrs, self.block_table_strides, self.num_blocks.gpu, self.num_blocks.gpu.stride(0), num_reqs, BLOCK_SIZE=1024, # type: ignore ) return tuple(bt[:num_reqs_padded] for bt in out) def get_dummy_block_tables(self, num_reqs: int) -> tuple[torch.Tensor, ...]: # NOTE(woosuk): The output may be used for CUDA graph capture. # Therefore, this method must return the persistent tensor # with the same memory address as that used during the model's forward pass, # rather than allocating a new tensor. # # Zero the rows so dummy runs write mamba state to the reserved null # block rather than through the previous real step's (stale) block # ids, which may point at blocks since freed and reallocated. return tuple( block_table[:num_reqs].zero_() for block_table in self.input_block_tables ) def compute_slot_mappings( self, idx_mapping: torch.Tensor, query_start_loc: torch.Tensor, positions: torch.Tensor, num_tokens_padded: int, out: torch.Tensor | None = None, ) -> torch.Tensor: if self.num_kv_cache_groups == 0: return (self.slot_mappings if out is None else out)[:, :num_tokens_padded] num_reqs = idx_mapping.shape[0] num_groups = self.num_kv_cache_groups slot_mappings = self.slot_mappings if out is None else out _compute_slot_mappings_kernel[(num_groups, num_reqs + 1)]( slot_mappings.shape[1], idx_mapping, query_start_loc, positions, self.block_table_ptrs, self.block_table_strides, self.block_sizes_tensor, self.kernel_block_sizes_tensor, self.slot_mapping_enabled, self.cp_sharded, slot_mappings, slot_mappings.stride(0), self.cp_rank, CP_SIZE=self.cp_size, CP_INTERLEAVE=self.cp_interleave, PAD_ID=PAD_SLOT_ID, TRITON_BLOCK_SIZE=1024, # type: ignore ) return slot_mappings[:, :num_tokens_padded] def get_dummy_slot_mappings(self, num_tokens: int) -> torch.Tensor: # Fill the entire slot_mappings tensor, not just the first `num_tokens` entries. # This is because the padding logic is complex and kernels may access beyond # the requested range. self.slot_mappings.fill_(PAD_SLOT_ID) # NOTE(woosuk): The output may be used for CUDA graph capture. # Therefore, this method must return the persistent tensor # with the same memory address as that used during the model's forward pass, # rather than allocating a new tensor. return self.slot_mappings[:, :num_tokens] @triton.jit(do_not_specialize=["num_reqs"]) def _gather_block_tables_kernel( batch_idx_to_req_idx, # [batch_size] src_block_table_ptrs, # [num_kv_cache_groups] dst_block_table_ptrs, # [num_kv_cache_groups] block_table_strides, # [num_kv_cache_groups] num_blocks_ptr, # [num_kv_cache_groups, max_num_reqs] num_blocks_stride, num_reqs, # actual number of requests (for padding) BLOCK_SIZE: tl.constexpr, ): # kv cache group id group_id = tl.program_id(0) batch_idx = tl.program_id(1) stride = tl.load(block_table_strides + group_id) max_num_blocks = stride # stride equals max_num_blocks for this group. dst_block_table_ptr = _load_ptr(dst_block_table_ptrs + group_id, tl.int32) dst_row_ptr = dst_block_table_ptr + batch_idx * stride if batch_idx >= num_reqs: # Zero out padded rows. for i in tl.range(0, max_num_blocks, BLOCK_SIZE): offset = i + tl.arange(0, BLOCK_SIZE) tl.store(dst_row_ptr + offset, 0, mask=offset < max_num_blocks) return req_idx = tl.load(batch_idx_to_req_idx + batch_idx) group_num_blocks_ptr = num_blocks_ptr + group_id * num_blocks_stride num_blocks = tl.load(group_num_blocks_ptr + req_idx) src_block_table_ptr = _load_ptr(src_block_table_ptrs + group_id, tl.int32) src_row_ptr = src_block_table_ptr + req_idx * stride for i in tl.range(0, num_blocks, BLOCK_SIZE): offset = i + tl.arange(0, BLOCK_SIZE) block_ids = tl.load(src_row_ptr + offset, mask=offset < num_blocks) tl.store(dst_row_ptr + offset, block_ids, mask=offset < num_blocks) @triton.jit def _compute_slot_mappings_kernel( max_num_tokens, idx_mapping, # [num_reqs] query_start_loc, # [num_reqs + 1] pos, # [num_tokens] block_table_ptrs, # [num_kv_cache_groups] block_table_strides, # [num_kv_cache_groups] block_sizes, # [num_kv_cache_groups] kernel_block_sizes, # [num_kv_cache_groups] slot_mapping_enabled, # [num_kv_cache_groups] cp_sharded, # [num_kv_cache_groups] slot_mappings_ptr, # [num_kv_cache_groups, max_num_tokens] slot_mappings_stride, cp_rank, CP_SIZE: tl.constexpr, CP_INTERLEAVE: tl.constexpr, PAD_ID: tl.constexpr, TRITON_BLOCK_SIZE: tl.constexpr, ): # kv cache group id group_id = tl.program_id(0) batch_idx = tl.program_id(1) slot_mapping_ptr = slot_mappings_ptr + group_id * slot_mappings_stride if batch_idx == tl.num_programs(1) - 1: # Pad remaining slots to -1. This is needed for CUDA graphs. # Start from actual token count (not padded) to cover the gap # between actual tokens and padded tokens that can contain stale # valid slot IDs from previous chunks during chunked prefill. actual_num_tokens = tl.load(query_start_loc + batch_idx) for i in range(actual_num_tokens, max_num_tokens, TRITON_BLOCK_SIZE): offset = i + tl.arange(0, TRITON_BLOCK_SIZE) tl.store(slot_mapping_ptr + offset, PAD_ID, mask=offset < max_num_tokens) return block_table_ptr = _load_ptr(block_table_ptrs + group_id, tl.int32) block_table_stride = tl.load(block_table_strides + group_id) kv_block_size = tl.load(block_sizes + group_id) kernel_block_size = tl.load(kernel_block_sizes + group_id) mapping_enabled = tl.load(slot_mapping_enabled + group_id) group_sharded = tl.load(cp_sharded + group_id) req_state_idx = tl.load(idx_mapping + batch_idx) start_idx = tl.load(query_start_loc + batch_idx) end_idx = tl.load(query_start_loc + batch_idx + 1) for i in range(start_idx, end_idx, TRITON_BLOCK_SIZE): offset = i + tl.arange(0, TRITON_BLOCK_SIZE) positions = tl.load(pos + offset, mask=offset < end_idx, other=0) if CP_SIZE == 1: # Common case: Context parallelism is not used. local_positions = positions is_local = True else: # Context parallelism is used. Sharded groups round-robin positions # across the CP ranks; DCP-replicated groups (cp_sharded False) # keep every position on every rank at the plain block geometry. virtual_block_size = kv_block_size * CP_SIZE virtual_block_indices = positions // virtual_block_size virtual_block_offsets = positions % virtual_block_size owner_is_me = virtual_block_offsets // CP_INTERLEAVE % CP_SIZE == cp_rank rounds = virtual_block_offsets // (CP_INTERLEAVE * CP_SIZE) remainder = virtual_block_offsets % CP_INTERLEAVE local_offsets = rounds * CP_INTERLEAVE + remainder sharded_positions = virtual_block_indices * kv_block_size + local_offsets local_positions = tl.where(group_sharded, sharded_positions, positions) is_local = tl.where(group_sharded, owner_is_me, True) block_indices = tl.where( mapping_enabled, local_positions // kernel_block_size, 0 ) block_offsets = local_positions % kernel_block_size block_numbers = tl.load( block_table_ptr + req_state_idx * block_table_stride + block_indices, mask=is_local, other=0, ) slot_ids = block_numbers * kernel_block_size + block_offsets if CP_SIZE != 1: slot_ids = tl.where(is_local, slot_ids, PAD_ID) slot_ids = tl.where(mapping_enabled, slot_ids, PAD_ID) tl.store(slot_mapping_ptr + offset, slot_ids, mask=offset < end_idx)