block_table.py 15 KB

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