block_table.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from collections.abc import Iterable
  4. import torch
  5. from vllm.triton_utils import tl, triton
  6. from vllm.v1.attention.backends.utils import PAD_SLOT_ID
  7. from vllm.v1.worker.gpu.buffer_utils import (
  8. FusedStagedWriter,
  9. StagedWriteTensor,
  10. UvaBackedTensor,
  11. _load_ptr,
  12. )
  13. class BlockTables:
  14. def __init__(
  15. self,
  16. block_sizes: list[int],
  17. max_num_reqs: int,
  18. max_num_batched_tokens: int,
  19. max_num_blocks_per_group: list[int],
  20. device: torch.device,
  21. kernel_block_sizes: list[int],
  22. cp_size: int = 1,
  23. cp_rank: int = 0,
  24. cp_interleave: int = 1,
  25. slot_mapping_enabled: list[bool] | None = None,
  26. ):
  27. self.block_sizes = block_sizes
  28. self.kernel_block_sizes = kernel_block_sizes
  29. self.max_num_reqs = max_num_reqs
  30. self.max_num_batched_tokens = max_num_batched_tokens
  31. self.device = device
  32. self.cp_size = cp_size
  33. self.cp_rank = cp_rank
  34. self.cp_interleave = cp_interleave
  35. self.num_kv_cache_groups = len(self.block_sizes)
  36. assert len(max_num_blocks_per_group) == self.num_kv_cache_groups
  37. if slot_mapping_enabled is None:
  38. slot_mapping_enabled = [True] * self.num_kv_cache_groups
  39. assert len(slot_mapping_enabled) == self.num_kv_cache_groups
  40. self._slot_mapping_enabled = slot_mapping_enabled
  41. self.blocks_per_kv_block = [
  42. bs // kbs for bs, kbs in zip(block_sizes, kernel_block_sizes)
  43. ]
  44. # num_kv_cache_groups x [max_num_reqs, max_num_blocks]
  45. self.block_tables: list[StagedWriteTensor] = []
  46. for i in range(self.num_kv_cache_groups):
  47. max_num_blocks = max_num_blocks_per_group[i] * self.blocks_per_kv_block[i]
  48. block_table = StagedWriteTensor(
  49. (self.max_num_reqs, max_num_blocks), dtype=torch.int32, device=device
  50. )
  51. self.block_tables.append(block_table)
  52. self.num_blocks = UvaBackedTensor(
  53. (self.num_kv_cache_groups, self.max_num_reqs),
  54. dtype=torch.int32,
  55. )
  56. self.fused_writer: FusedStagedWriter | None = None
  57. if self.num_kv_cache_groups > 1:
  58. # Only the multi-group path uses the fused writer.
  59. self.fused_writer = FusedStagedWriter(
  60. self.device, self.num_kv_cache_groups * self.max_num_reqs
  61. )
  62. # Block tables used for model's forward pass.
  63. # num_kv_cache_groups x [max_num_reqs, max_num_blocks]
  64. self.input_block_tables: list[torch.Tensor] = [
  65. torch.zeros_like(b.gpu) for b in self.block_tables
  66. ]
  67. self.slot_mappings = torch.zeros(
  68. self.num_kv_cache_groups,
  69. self.max_num_batched_tokens,
  70. dtype=torch.int64,
  71. device=self.device,
  72. )
  73. self.init_block_table_layout_tensors()
  74. def _make_ptr_tensor(self, x: Iterable[torch.Tensor]) -> torch.Tensor:
  75. # NOTE(woosuk): Use uint64 instead of int64 to cover all possible addresses.
  76. return torch.tensor(
  77. [t.data_ptr() for t in x], dtype=torch.uint64, device=self.device
  78. )
  79. def init_block_table_layout_tensors(self) -> None:
  80. self.block_table_ptrs = self._make_ptr_tensor(
  81. [b.gpu for b in self.block_tables]
  82. )
  83. self.block_table_strides = torch.tensor(
  84. [b.gpu.stride(0) for b in self.block_tables],
  85. dtype=torch.int64,
  86. device=self.device,
  87. )
  88. self.block_sizes_tensor = torch.tensor(
  89. self.block_sizes, dtype=torch.int32, device=self.device
  90. )
  91. self.kernel_block_sizes_tensor = torch.tensor(
  92. self.kernel_block_sizes, dtype=torch.int32, device=self.device
  93. )
  94. self.slot_mapping_enabled = torch.tensor(
  95. self._slot_mapping_enabled, dtype=torch.bool, device=self.device
  96. )
  97. self.input_block_table_ptrs = self._make_ptr_tensor(self.input_block_tables)
  98. def append_block_ids(
  99. self,
  100. req_index: int,
  101. new_block_ids: tuple[list[int], ...],
  102. overwrite: bool,
  103. ) -> None:
  104. for i in range(self.num_kv_cache_groups):
  105. start = self.num_blocks.np[i, req_index] if not overwrite else 0
  106. block_ids = new_block_ids[i]
  107. bpk = self.blocks_per_kv_block[i]
  108. if bpk > 1:
  109. block_ids = [b * bpk + k for b in block_ids for k in range(bpk)]
  110. end = start + len(block_ids)
  111. row_capacity = self.block_tables[i].gpu.shape[1]
  112. if end > row_capacity:
  113. raise RuntimeError(
  114. f"Block table write for request {req_index}, group {i} exceeds "
  115. f"row capacity ({end} > {row_capacity})"
  116. )
  117. self.block_tables[i].stage_write(req_index, start, block_ids)
  118. self.num_blocks.np[i, req_index] = end
  119. def apply_staged_writes(self) -> None:
  120. if self.num_kv_cache_groups == 0:
  121. return
  122. if self.num_kv_cache_groups == 1:
  123. # Single group: write directly, skipping the per-write group lookup.
  124. self.block_tables[0].apply_write()
  125. elif self.num_kv_cache_groups > 1:
  126. # Multiple groups: apply all block tables with one fused kernel.
  127. assert self.fused_writer is not None
  128. self.fused_writer.apply(
  129. self.block_tables, self.block_table_ptrs, self.block_table_strides
  130. )
  131. self.num_blocks.copy_to_uva()
  132. def gather_block_tables(
  133. self,
  134. idx_mapping: torch.Tensor,
  135. num_reqs_padded: int,
  136. out: tuple[torch.Tensor, ...] | None = None,
  137. out_ptrs: torch.Tensor | None = None,
  138. ) -> tuple[torch.Tensor, ...]:
  139. if self.num_kv_cache_groups == 0:
  140. return ()
  141. if out is None:
  142. out = tuple(self.input_block_tables)
  143. out_ptrs = self.input_block_table_ptrs
  144. else:
  145. assert out_ptrs is not None
  146. assert len(out) == self.num_kv_cache_groups
  147. num_reqs = idx_mapping.shape[0]
  148. # Launch kernel with num_reqs_padded to fuse zeroing of padded rows.
  149. _gather_block_tables_kernel[(self.num_kv_cache_groups, num_reqs_padded)](
  150. idx_mapping,
  151. self.block_table_ptrs,
  152. out_ptrs,
  153. self.block_table_strides,
  154. self.num_blocks.gpu,
  155. self.num_blocks.gpu.stride(0),
  156. num_reqs,
  157. BLOCK_SIZE=1024, # type: ignore
  158. )
  159. return tuple(bt[:num_reqs_padded] for bt in out)
  160. def get_dummy_block_tables(self, num_reqs: int) -> tuple[torch.Tensor, ...]:
  161. # NOTE(woosuk): The output may be used for CUDA graph capture.
  162. # Therefore, this method must return the persistent tensor
  163. # with the same memory address as that used during the model's forward pass,
  164. # rather than allocating a new tensor.
  165. #
  166. # Zero the rows so dummy runs write mamba state to the reserved null
  167. # block rather than through the previous real step's (stale) block
  168. # ids, which may point at blocks since freed and reallocated.
  169. return tuple(
  170. block_table[:num_reqs].zero_() for block_table in self.input_block_tables
  171. )
  172. def compute_slot_mappings(
  173. self,
  174. idx_mapping: torch.Tensor,
  175. query_start_loc: torch.Tensor,
  176. positions: torch.Tensor,
  177. num_tokens_padded: int,
  178. out: torch.Tensor | None = None,
  179. ) -> torch.Tensor:
  180. if self.num_kv_cache_groups == 0:
  181. return (self.slot_mappings if out is None else out)[:, :num_tokens_padded]
  182. num_reqs = idx_mapping.shape[0]
  183. num_groups = self.num_kv_cache_groups
  184. slot_mappings = self.slot_mappings if out is None else out
  185. _compute_slot_mappings_kernel[(num_groups, num_reqs + 1)](
  186. slot_mappings.shape[1],
  187. idx_mapping,
  188. query_start_loc,
  189. positions,
  190. self.block_table_ptrs,
  191. self.block_table_strides,
  192. self.block_sizes_tensor,
  193. self.kernel_block_sizes_tensor,
  194. self.slot_mapping_enabled,
  195. slot_mappings,
  196. slot_mappings.stride(0),
  197. self.cp_rank,
  198. CP_SIZE=self.cp_size,
  199. CP_INTERLEAVE=self.cp_interleave,
  200. PAD_ID=PAD_SLOT_ID,
  201. TRITON_BLOCK_SIZE=1024, # type: ignore
  202. )
  203. return slot_mappings[:, :num_tokens_padded]
  204. def get_dummy_slot_mappings(self, num_tokens: int) -> torch.Tensor:
  205. # Fill the entire slot_mappings tensor, not just the first `num_tokens` entries.
  206. # This is because the padding logic is complex and kernels may access beyond
  207. # the requested range.
  208. self.slot_mappings.fill_(PAD_SLOT_ID)
  209. # NOTE(woosuk): The output may be used for CUDA graph capture.
  210. # Therefore, this method must return the persistent tensor
  211. # with the same memory address as that used during the model's forward pass,
  212. # rather than allocating a new tensor.
  213. return self.slot_mappings[:, :num_tokens]
  214. @triton.jit(do_not_specialize=["num_reqs"])
  215. def _gather_block_tables_kernel(
  216. batch_idx_to_req_idx, # [batch_size]
  217. src_block_table_ptrs, # [num_kv_cache_groups]
  218. dst_block_table_ptrs, # [num_kv_cache_groups]
  219. block_table_strides, # [num_kv_cache_groups]
  220. num_blocks_ptr, # [num_kv_cache_groups, max_num_reqs]
  221. num_blocks_stride,
  222. num_reqs, # actual number of requests (for padding)
  223. BLOCK_SIZE: tl.constexpr,
  224. ):
  225. # kv cache group id
  226. group_id = tl.program_id(0)
  227. batch_idx = tl.program_id(1)
  228. stride = tl.load(block_table_strides + group_id)
  229. max_num_blocks = stride # stride equals max_num_blocks for this group.
  230. dst_block_table_ptr = _load_ptr(dst_block_table_ptrs + group_id, tl.int32)
  231. dst_row_ptr = dst_block_table_ptr + batch_idx * stride
  232. if batch_idx >= num_reqs:
  233. # Zero out padded rows.
  234. for i in tl.range(0, max_num_blocks, BLOCK_SIZE):
  235. offset = i + tl.arange(0, BLOCK_SIZE)
  236. tl.store(dst_row_ptr + offset, 0, mask=offset < max_num_blocks)
  237. return
  238. req_idx = tl.load(batch_idx_to_req_idx + batch_idx)
  239. group_num_blocks_ptr = num_blocks_ptr + group_id * num_blocks_stride
  240. num_blocks = tl.load(group_num_blocks_ptr + req_idx)
  241. src_block_table_ptr = _load_ptr(src_block_table_ptrs + group_id, tl.int32)
  242. src_row_ptr = src_block_table_ptr + req_idx * stride
  243. for i in tl.range(0, num_blocks, BLOCK_SIZE):
  244. offset = i + tl.arange(0, BLOCK_SIZE)
  245. block_ids = tl.load(src_row_ptr + offset, mask=offset < num_blocks)
  246. tl.store(dst_row_ptr + offset, block_ids, mask=offset < num_blocks)
  247. @triton.jit
  248. def _compute_slot_mappings_kernel(
  249. max_num_tokens,
  250. idx_mapping, # [num_reqs]
  251. query_start_loc, # [num_reqs + 1]
  252. pos, # [num_tokens]
  253. block_table_ptrs, # [num_kv_cache_groups]
  254. block_table_strides, # [num_kv_cache_groups]
  255. block_sizes, # [num_kv_cache_groups]
  256. kernel_block_sizes, # [num_kv_cache_groups]
  257. slot_mapping_enabled, # [num_kv_cache_groups]
  258. slot_mappings_ptr, # [num_kv_cache_groups, max_num_tokens]
  259. slot_mappings_stride,
  260. cp_rank,
  261. CP_SIZE: tl.constexpr,
  262. CP_INTERLEAVE: tl.constexpr,
  263. PAD_ID: tl.constexpr,
  264. TRITON_BLOCK_SIZE: tl.constexpr,
  265. ):
  266. # kv cache group id
  267. group_id = tl.program_id(0)
  268. batch_idx = tl.program_id(1)
  269. slot_mapping_ptr = slot_mappings_ptr + group_id * slot_mappings_stride
  270. if batch_idx == tl.num_programs(1) - 1:
  271. # Pad remaining slots to -1. This is needed for CUDA graphs.
  272. # Start from actual token count (not padded) to cover the gap
  273. # between actual tokens and padded tokens that can contain stale
  274. # valid slot IDs from previous chunks during chunked prefill.
  275. actual_num_tokens = tl.load(query_start_loc + batch_idx)
  276. for i in range(actual_num_tokens, max_num_tokens, TRITON_BLOCK_SIZE):
  277. offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
  278. tl.store(slot_mapping_ptr + offset, PAD_ID, mask=offset < max_num_tokens)
  279. return
  280. block_table_ptr = _load_ptr(block_table_ptrs + group_id, tl.int32)
  281. block_table_stride = tl.load(block_table_strides + group_id)
  282. kv_block_size = tl.load(block_sizes + group_id)
  283. kernel_block_size = tl.load(kernel_block_sizes + group_id)
  284. mapping_enabled = tl.load(slot_mapping_enabled + group_id)
  285. req_state_idx = tl.load(idx_mapping + batch_idx)
  286. start_idx = tl.load(query_start_loc + batch_idx)
  287. end_idx = tl.load(query_start_loc + batch_idx + 1)
  288. for i in range(start_idx, end_idx, TRITON_BLOCK_SIZE):
  289. offset = i + tl.arange(0, TRITON_BLOCK_SIZE)
  290. positions = tl.load(pos + offset, mask=offset < end_idx, other=0)
  291. if CP_SIZE == 1:
  292. # Common case: Context parallelism is not used.
  293. local_positions = positions
  294. is_local = True
  295. else:
  296. # Context parallelism is used.
  297. virtual_block_size = kv_block_size * CP_SIZE
  298. virtual_block_indices = positions // virtual_block_size
  299. virtual_block_offsets = positions % virtual_block_size
  300. is_local = virtual_block_offsets // CP_INTERLEAVE % CP_SIZE == cp_rank
  301. rounds = virtual_block_offsets // (CP_INTERLEAVE * CP_SIZE)
  302. remainder = virtual_block_offsets % CP_INTERLEAVE
  303. local_offsets = rounds * CP_INTERLEAVE + remainder
  304. local_positions = virtual_block_indices * kv_block_size + local_offsets
  305. block_indices = tl.where(
  306. mapping_enabled, local_positions // kernel_block_size, 0
  307. )
  308. block_offsets = local_positions % kernel_block_size
  309. block_numbers = tl.load(
  310. block_table_ptr + req_state_idx * block_table_stride + block_indices,
  311. mask=is_local,
  312. other=0,
  313. )
  314. slot_ids = block_numbers * kernel_block_size + block_offsets
  315. if CP_SIZE != 1:
  316. slot_ids = tl.where(is_local, slot_ids, PAD_ID)
  317. slot_ids = tl.where(mapping_enabled, slot_ids, PAD_ID)
  318. tl.store(slot_mapping_ptr + offset, slot_ids, mask=offset < end_idx)