candidate_blocks.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. import os
  4. import torch
  5. from vllm.triton_utils import tl, triton
  6. def _cpu_kernels_allowed() -> bool:
  7. # CPU tests run the Triton kernels through the interpreter.
  8. return os.environ.get("TRITON_INTERPRET", "0") == "1"
  9. # Scratch cap for the DCP candidate-score all-reduce (bytes per slice).
  10. _DCP_REDUCE_BYTES = 128 * 1024 * 1024
  11. # Stage 7: opt-in prefill phase timing (DSV41_PREFILL_TIMING=1). Phases add GPU
  12. # milliseconds here; the runner reports and clears them after each eager step
  13. # of >= 1024 tokens. Inert (no events, no syncs) unless enabled.
  14. _PREFILL_TIMING = os.environ.get("DSV41_PREFILL_TIMING", "0") == "1"
  15. PREFILL_ACC: dict[str, float] = {}
  16. def prefill_acc_add(key: str, ms: float) -> None:
  17. PREFILL_ACC[key] = PREFILL_ACC.get(key, 0.0) + ms
  18. def prefill_acc_take() -> dict[str, float]:
  19. d = dict(PREFILL_ACC)
  20. PREFILL_ACC.clear()
  21. return d
  22. class _PrefillTimer:
  23. """`with _PrefillTimer(key, num_tokens): ...` — CUDA events around the block,
  24. synchronised at exit (eager prefill steps only; never while capturing)."""
  25. __slots__ = ("key", "on", "e0", "e1")
  26. def __init__(self, key: str, num_tokens: int) -> None:
  27. self.key = key
  28. self.on = (
  29. _PREFILL_TIMING
  30. and num_tokens >= 1024
  31. and not torch.cuda.is_current_stream_capturing()
  32. )
  33. def __enter__(self):
  34. if self.on:
  35. self.e0 = torch.cuda.Event(enable_timing=True)
  36. self.e1 = torch.cuda.Event(enable_timing=True)
  37. self.e0.record()
  38. return self
  39. def __exit__(self, *exc):
  40. if self.on:
  41. self.e1.record()
  42. self.e1.synchronize()
  43. prefill_acc_add(self.key, self.e0.elapsed_time(self.e1))
  44. return False
  45. @triton.jit
  46. def _max_with_nan(a, b):
  47. return tl.maximum(a, b, propagate_nan=tl.PropagateNan.ALL)
  48. @triton.jit(do_not_specialize=["width", "nblocks"])
  49. def _block_scores_kernel(
  50. logits,
  51. starts,
  52. ends,
  53. scores,
  54. stride_row,
  55. stride_col,
  56. stride_start,
  57. stride_end,
  58. width,
  59. nblocks,
  60. BLOCK_SIZE: tl.constexpr,
  61. HAS_STARTS: tl.constexpr,
  62. ROW_REPEAT: tl.constexpr,
  63. TILE: tl.constexpr,
  64. PIN_NEWEST: tl.constexpr = True,
  65. ):
  66. row = tl.program_id(0).to(tl.int64)
  67. blocks = tl.program_id(1) * TILE + tl.arange(0, TILE)
  68. start = tl.load(starts + row // ROW_REPEAT * stride_start) if HAS_STARTS else 0
  69. end = tl.load(ends + row // ROW_REPEAT * stride_end)
  70. offsets = tl.arange(0, triton.next_power_of_2(BLOCK_SIZE))
  71. cols = start + blocks[:, None] * BLOCK_SIZE + offsets[None, :]
  72. values = tl.load(
  73. logits + row * stride_row + cols * stride_col,
  74. (blocks[:, None] < nblocks)
  75. & (offsets[None, :] < BLOCK_SIZE)
  76. & (cols < end)
  77. & (cols < width),
  78. other=-float("inf"),
  79. )
  80. reduced = tl.reduce(values, 1, _max_with_nan)
  81. if PIN_NEWEST:
  82. reduced = tl.where(
  83. (end > start) & (blocks == (end - start - 1) // BLOCK_SIZE),
  84. float("inf"),
  85. reduced,
  86. )
  87. tl.store(scores + row * nblocks + blocks, reduced, blocks < nblocks)
  88. @triton.jit(do_not_specialize=["k"])
  89. def _store_candidates_kernel(
  90. values,
  91. indices,
  92. output,
  93. out_stride_row,
  94. out_stride_col,
  95. k,
  96. OUT_K: tl.constexpr,
  97. TILE: tl.constexpr,
  98. ):
  99. row = tl.program_id(0).to(tl.int64)
  100. cols = tl.program_id(1) * TILE + tl.arange(0, TILE)
  101. value = tl.load(values + row * k + cols, cols < k, other=-float("inf"))
  102. index = tl.load(indices + row * k + cols, cols < k, other=-1)
  103. tl.store(
  104. output + row * out_stride_row + cols * out_stride_col,
  105. tl.where(value > -float("inf"), index, -1),
  106. cols < OUT_K,
  107. )
  108. @triton.jit(do_not_specialize=["width", "nblocks"])
  109. def _candidate_flags_kernel(
  110. candidates,
  111. starts,
  112. flags,
  113. stride_row,
  114. stride_col,
  115. stride_start,
  116. width,
  117. nblocks,
  118. BLOCK_SIZE: tl.constexpr,
  119. K: tl.constexpr,
  120. HAS_STARTS: tl.constexpr,
  121. ROW_REPEAT: tl.constexpr,
  122. ):
  123. row = tl.program_id(0).to(tl.int64)
  124. offsets = tl.arange(0, 1024)
  125. for tile in range(tl.cdiv(nblocks + 1, 1024)):
  126. slots = tile * 1024 + offsets
  127. tl.store(flags + row * (nblocks + 1) + slots, 0, slots <= nblocks)
  128. start = tl.load(starts + row // ROW_REPEAT * stride_start) if HAS_STARTS else 0
  129. cols = tl.arange(0, triton.next_power_of_2(K))
  130. block = tl.load(
  131. candidates + row * stride_row + cols * stride_col, cols < K, other=-1
  132. ).to(tl.int64)
  133. # Preserve the packed-column clamp for candidates beyond the logits width.
  134. block = tl.where(start + block * BLOCK_SIZE >= width, nblocks, block)
  135. tl.debug_barrier()
  136. tl.store(flags + row * (nblocks + 1) + block, 1, (cols < K) & (block >= 0))
  137. @triton.jit(do_not_specialize=["width", "nblocks"])
  138. def _mask_candidates_kernel(
  139. logits,
  140. starts,
  141. ends,
  142. flags,
  143. stride_row,
  144. stride_col,
  145. stride_start,
  146. stride_end,
  147. width,
  148. nblocks,
  149. BLOCK_SIZE: tl.constexpr,
  150. HAS_STARTS: tl.constexpr,
  151. ROW_REPEAT: tl.constexpr,
  152. TILE: tl.constexpr,
  153. ):
  154. row = tl.program_id(0).to(tl.int64)
  155. cols = tl.program_id(1) * TILE + tl.arange(0, TILE)
  156. start = tl.load(starts + row // ROW_REPEAT * stride_start) if HAS_STARTS else 0
  157. end = tl.load(ends + row // ROW_REPEAT * stride_end)
  158. valid = (cols >= start) & (cols < end) & (cols < width)
  159. block = (cols - start) // BLOCK_SIZE
  160. keep = tl.load(flags + row * (nblocks + 1) + block, valid, other=0)
  161. edge = tl.load(flags + row * (nblocks + 1) + nblocks)
  162. keep = (keep != 0) | ((cols == width - 1) & (edge != 0))
  163. tl.store(
  164. logits + row * stride_row + cols * stride_col,
  165. -float("inf"),
  166. (cols < width) & ~(valid & keep),
  167. )
  168. def _block_scores(
  169. logits: torch.Tensor,
  170. row_ks: torch.Tensor | None,
  171. row_ke: torch.Tensor,
  172. block_size: int,
  173. row_repeat: int,
  174. pin_newest: bool,
  175. ) -> torch.Tensor:
  176. rows, width = logits.shape
  177. nblocks = triton.cdiv(width, block_size)
  178. scores = logits.new_empty((rows, nblocks))
  179. _block_scores_kernel[(rows, triton.cdiv(nblocks, 128))](
  180. logits,
  181. row_ks,
  182. row_ke,
  183. scores,
  184. *logits.stride(),
  185. row_ks.stride(0) if row_ks is not None else 0,
  186. row_ke.stride(0),
  187. width,
  188. nblocks,
  189. block_size,
  190. row_ks is not None,
  191. row_repeat,
  192. 128,
  193. PIN_NEWEST=pin_newest,
  194. )
  195. return scores
  196. def select_candidate_blocks(logits: torch.Tensor, *args, **kwargs) -> None:
  197. """Stage 7: timed wrapper (phase "candidates"); see _select_candidate_blocks_impl."""
  198. with _PrefillTimer("candidates", int(logits.shape[0])):
  199. return _select_candidate_blocks_impl(logits, *args, **kwargs)
  200. def _select_candidate_blocks_impl(
  201. logits: torch.Tensor,
  202. row_ks: torch.Tensor | None,
  203. row_ke: torch.Tensor,
  204. topk_blocks: int,
  205. block_size: int,
  206. out: torch.Tensor,
  207. row_repeat: int = 1,
  208. *,
  209. dcp_group=None,
  210. global_row_ke: torch.Tensor | None = None,
  211. global_block_size: int | None = None,
  212. max_global_blocks: int | None = None,
  213. ) -> None:
  214. """Select local block IDs by maximum score, pinning each row's newest block.
  215. Row bounds are in packed column space; absent starts mean zero.
  216. Decode rows share bounds in groups of ``row_repeat``. Output is -1 padded.
  217. Under DCP (``dcp_group`` given) ``logits`` hold this rank's local packed
  218. columns and ``block_size`` is the LOCAL block, ``global_block_size //
  219. dcp_world_size``: with interleave 1, local block ``b`` is exactly this
  220. rank's states of global block ``b``, so the global block score is the MAX
  221. over ranks of the local block scores (one all-reduce over
  222. ``[rows, max_global_blocks]``) and the block ids need no translation. The
  223. newest block is pinned from the GLOBAL per-row context ``global_row_ke``
  224. (states, per row or per ``row_repeat`` group) after the reduce, so every
  225. rank pins the same block. Collective: every DCP rank must call this with
  226. the same ``rows`` and ``max_global_blocks`` (an empty local shard is fine).
  227. """
  228. assert logits.is_cuda or _cpu_kernels_allowed()
  229. rows, width = logits.shape
  230. if not rows:
  231. return
  232. if dcp_group is None:
  233. if not width:
  234. out.fill_(-1)
  235. return
  236. scores = _block_scores(logits, row_ks, row_ke, block_size, row_repeat, True)
  237. nblocks = scores.shape[1]
  238. else:
  239. assert global_row_ke is not None
  240. assert global_block_size is not None and max_global_blocks is not None
  241. assert global_block_size == block_size * dcp_group.world_size, (
  242. "DCP candidate blocks need block_size == global_block_size // world"
  243. )
  244. nblocks = max(int(max_global_blocks), 1)
  245. ends = global_row_ke.reshape(-1)
  246. if row_repeat > 1:
  247. ends = ends.repeat_interleave(row_repeat)
  248. ends = ends[:rows].to(torch.int64)
  249. # Pin each row's newest GLOBAL block (DCP1 pins (end - start - 1) //
  250. # block from the global bounds); rows without context select nothing.
  251. pin = ((ends - 1) // global_block_size).clamp_(0, nblocks - 1).view(-1, 1)
  252. local = (
  253. _block_scores(logits, row_ks, row_ke, block_size, row_repeat, False)
  254. if width
  255. else None
  256. )
  257. # Reduce in row slices: a 16K-token prefill chunk at 128K context would
  258. # otherwise need a ~1 GiB scratch per rank (GB10 unified memory is the
  259. # binding constraint). Slice count derives from rows/nblocks, which are
  260. # identical on every rank, so the collectives still line up.
  261. k = min(topk_blocks, nblocks)
  262. rows_per_slice = max(1, _DCP_REDUCE_BYTES // (nblocks * 4))
  263. top_values = logits.new_empty((rows, k))
  264. top_indices = torch.empty((rows, k), dtype=torch.int64, device=logits.device)
  265. for r0 in range(0, rows, rows_per_slice):
  266. r1 = min(r0 + rows_per_slice, rows)
  267. scores = logits.new_full((r1 - r0, nblocks), -float("inf"))
  268. if local is not None:
  269. n = min(local.shape[1], nblocks)
  270. scores[:, :n] = local[r0:r1, :n]
  271. torch.distributed.all_reduce(
  272. scores, op=torch.distributed.ReduceOp.MAX, group=dcp_group.device_group
  273. )
  274. scores.scatter_(1, pin[r0:r1], float("inf"))
  275. scores.masked_fill_((ends[r0:r1] <= 0).view(-1, 1), -float("inf"))
  276. # Keep the existing top-k tie behavior.
  277. sl = scores.topk(k, dim=-1)
  278. top_values[r0:r1] = sl.values
  279. top_indices[r0:r1] = sl.indices
  280. _store_candidates_kernel[(rows, triton.cdiv(topk_blocks, 256))](
  281. top_values,
  282. top_indices,
  283. out,
  284. *out.stride(),
  285. k,
  286. topk_blocks,
  287. 256,
  288. )
  289. return
  290. # Keep the existing top-k tie behavior.
  291. top = scores.topk(min(topk_blocks, nblocks), dim=-1)
  292. _store_candidates_kernel[(rows, triton.cdiv(topk_blocks, 256))](
  293. top.values,
  294. top.indices,
  295. out,
  296. *out.stride(),
  297. top.values.shape[1],
  298. topk_blocks,
  299. 256,
  300. )
  301. def apply_candidate_mask(
  302. logits: torch.Tensor,
  303. row_ks: torch.Tensor | None,
  304. row_ke: torch.Tensor,
  305. candidate_blocks: torch.Tensor,
  306. block_size: int,
  307. row_repeat: int = 1,
  308. ) -> None:
  309. """Mask packed logits outside causal bounds and request-local candidates.
  310. Under DCP pass the LOCAL block size (global // world): local block ``b``
  311. is this rank's shard of global block ``b``, so the candidate ids apply
  312. unchanged.
  313. """
  314. assert logits.is_cuda or _cpu_kernels_allowed()
  315. rows, width = logits.shape
  316. if not rows or not width:
  317. return
  318. nblocks = triton.cdiv(width, block_size)
  319. flags = torch.empty((rows, nblocks + 1), device=logits.device, dtype=torch.uint8)
  320. start_stride = row_ks.stride(0) if row_ks is not None else 0
  321. _candidate_flags_kernel[(rows,)](
  322. candidate_blocks,
  323. row_ks,
  324. flags,
  325. *candidate_blocks.stride(),
  326. start_stride,
  327. width,
  328. nblocks,
  329. block_size,
  330. candidate_blocks.shape[1],
  331. row_ks is not None,
  332. row_repeat,
  333. )
  334. _mask_candidates_kernel[(rows, triton.cdiv(width, 1024))](
  335. logits,
  336. row_ks,
  337. row_ke,
  338. flags,
  339. *logits.stride(),
  340. start_stride,
  341. row_ke.stride(0),
  342. width,
  343. nblocks,
  344. block_size,
  345. row_ks is not None,
  346. row_repeat,
  347. 1024,
  348. )