| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- # 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)
|