indexer_chunked_v3.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. """v3 = v2 (sglang#39187 verbatim, candidate_metadata backends) + rhys101's SG18 native prefill
  2. TP split: when a prefill chunk is eligible (>= SPARK_PREFILL_TP_MIN_ROWS rows and
  3. >= SPARK_PREFILL_TP_MIN_CONTEXT context, SPARK_PREFILL_TP_SPLIT=1), each TP rank scores only
  4. its slice of the query rows and the top-k / candidate block ids are all-gathered as ints.
  5. Otherwise the chunked #39187 path runs unchanged.
  6. Original v2 header: Bound the dense prefill indexer transient on the upstream dsv4.1 branch: verbatim
  7. backport of sgl-project/sglang#39187 (head cb8dd033, kpham-sgl) for backends that keep
  8. candidate masks in ``forward_metadata.candidate_metadata`` (branch f80c91a4b and later).
  9. The v1 adapter (indexer_chunked.py) covers image e087e662, which uses self.candidate_masks.
  10. Gate: ``DSV41_INDEXER_CHUNKED=1``. Budget: ``DSV41_INDEXER_LOGITS_BUDGET_BYTES``
  11. (default 2 GiB of fp32 logits). Off = stock path untouched.
  12. """
  13. import inspect
  14. import logging
  15. import os
  16. import torch
  17. logger = logging.getLogger(__name__)
  18. DEFAULT_BUDGET_BYTES = 1 << 31
  19. def _enabled(name, default="0"):
  20. return os.environ.get(name, default).strip() not in ("0", "off", "false", "")
  21. def budget_bytes():
  22. raw = os.environ.get("DSV41_INDEXER_LOGITS_BUDGET_BYTES", "").strip()
  23. try:
  24. value = int(raw) if raw else DEFAULT_BUDGET_BYTES
  25. except ValueError:
  26. value = DEFAULT_BUDGET_BYTES
  27. return value if value > 0 else DEFAULT_BUDGET_BYTES
  28. def _split_local(indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
  29. world, rank, *, budget, scorer, topk_fn, mask_topk, consume, ceil_align):
  30. """This rank's slice of the query rows, scored in row chunks of <= budget bytes of fp32
  31. logits (the #39187 bound applied inside the SG18 partition). Returns the packed int32
  32. [extent, topk + blocks] tensor the all-gather concatenates: top-k ids, then candidate
  33. block ids for a source layer."""
  34. from spark_prefill_dense import partition, candidate_block_ids
  35. rows = q_fp4.shape[0]
  36. start, end, extent = partition(rows, world, rank)
  37. source = indexer.is_candidate_source
  38. width_max = max(lengths)
  39. blocks = (min(indexer.candidate_topk_blocks,
  40. (width_max + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
  41. if source else 0)
  42. packed = torch.full((extent, indexer.index_topk + blocks), -1, dtype=torch.int32, device=q_fp4.device)
  43. if start >= end:
  44. return packed
  45. lc_aligned = ceil_align(width_max, 4)
  46. rows_per_chunk = max(1, budget // (lc_aligned * 4))
  47. # request boundaries in global row space
  48. bounds = []
  49. offset = 0
  50. for width, count in zip(lengths, q_lengths):
  51. bounds.append((offset, offset + count, width))
  52. offset += count
  53. assert offset == rows
  54. for c0 in range(start, end, rows_per_chunk):
  55. c1 = min(c0 + rows_per_chunk, end)
  56. rows_sl = slice(c0, c1)
  57. local_lens = lens[rows_sl]
  58. local_ks = ks[rows_sl]
  59. logits = scorer((q_fp4[rows_sl], q_sf[rows_sl]), (k_fp4, k_sf), weights[rows_sl],
  60. local_ks, local_ks + local_lens, lc_aligned)
  61. for request, (lo_r, hi_r, width) in enumerate(bounds):
  62. lo = max(c0, lo_r)
  63. hi = min(c1, hi_r)
  64. if not width or lo >= hi:
  65. continue
  66. take = slice(lo - c0, hi - c0)
  67. scores = logits[take, :width]
  68. if source:
  69. row_lens = local_lens[take, None]
  70. scores.masked_fill_(torch.arange(width, device=logits.device)[None, :] >= row_lens, -torch.inf)
  71. nblocks = min(indexer.candidate_topk_blocks,
  72. (width + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
  73. ids = candidate_block_ids(scores, row_lens, indexer.candidate_topk_blocks,
  74. indexer.candidate_block_size)
  75. packed[lo - start:hi - start, indexer.index_topk:indexer.index_topk + nblocks] = ids
  76. elif consume is not None:
  77. scores.masked_fill_(~consume[request][lo - lo_r:hi - lo_r], -torch.inf)
  78. selected = torch.empty((c1 - c0, indexer.index_topk), dtype=torch.int32, device=logits.device)
  79. topk_fn(logits, local_lens, out_offsets=local_ks, out_indices=selected)
  80. if consume is not None:
  81. selected = mask_topk(logits, selected, local_ks)
  82. packed[c0 - start:c1 - start, :indexer.index_topk] = selected
  83. del logits
  84. return packed
  85. def _split_select(tp, indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
  86. *, budget, scorer, topk_fn, mask_topk, consume, publish_rows, layer_id, ratio,
  87. ceil_align):
  88. """Score this rank's partition, all-gather the packed ids, rebuild the (tail-only)
  89. candidate masks. Mirrors NativePrefillTPSplit.select with the chunk bound and the
  90. #39187 tail-only publishing."""
  91. from spark_prefill_dense import partition, candidate_mask
  92. local = _split_local(indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
  93. tp.world, tp.rank, budget=budget, scorer=scorer, topk_fn=topk_fn,
  94. mask_topk=mask_topk, consume=consume, ceil_align=ceil_align)
  95. gathered = tp.group.all_gather(local, dim=0)[: q_fp4.shape[0]]
  96. masks = None
  97. if indexer.is_candidate_source:
  98. masks = []
  99. offset = 0
  100. for b, (width, count) in enumerate(zip(lengths, q_lengths)):
  101. if not width or not count:
  102. masks.append(torch.zeros((0, 0), dtype=torch.bool, device=gathered.device))
  103. else:
  104. nblocks = min(indexer.candidate_topk_blocks,
  105. (width + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
  106. pub = count if publish_rows is None else min(int(publish_rows[b]), count)
  107. ids = gathered[offset + count - pub: offset + count,
  108. indexer.index_topk: indexer.index_topk + nblocks]
  109. masks.append(candidate_mask(ids, width, indexer.candidate_block_size))
  110. offset += count
  111. key = (ratio, bool(indexer.is_candidate_source), bool(indexer.uses_candidates))
  112. if key not in tp._logged:
  113. tp._logged.add(key)
  114. start, end, extent = partition(q_fp4.shape[0], tp.world, tp.rank)
  115. logger.warning("DSV41 prefill TP split (v3) rank=%s layer=%s ratio=%s source=%s consumer=%s rows=%s width=%s start=%s end=%s budget=%d MiB",
  116. tp.rank, layer_id, ratio, indexer.is_candidate_source, indexer.uses_candidates,
  117. q_fp4.shape[0], max(lengths), start, end, budget >> 20)
  118. return gathered[:, : indexer.index_topk], masks
  119. def make_index_topk_dense(module, quantize_fp4_indexer_tensor, budget):
  120. """The PR's function, bound to the module's own helpers (names resolved once)."""
  121. _dense_fp4_mqa_logits = module._dense_fp4_mqa_logits
  122. topk_transform_ragged_v2 = module.topk_transform_ragged_v2
  123. mask_topk_scores = module.mask_topk_scores
  124. select_candidate_blocks = module.select_candidate_blocks
  125. ceil_align = module.ceil_align
  126. _as_int_list = module._as_int_list
  127. CandidateMasks = module.CandidateMasks
  128. published_masks = module.published_masks
  129. _DENSE_INDEXER_LOGITS_BUDGET_BYTES = budget
  130. # ---- begin verbatim from sglang#39187 (deepseek_v4_backend.py @ cb8dd033) ----
  131. def _low_ratio_index_topk_dense(
  132. self, layer, x, q_lora, pos, forward_batch, q_lens, q_lens_cpu
  133. ) -> None:
  134. """Dense fp4 indexer over rows laid out request after request, `q_lens[b]` each."""
  135. from sglang.kernels.ops.attention.dsv4.fp4_indexer import (
  136. quantize_fp4_indexer_tensor,
  137. )
  138. pool = self.token_to_kv_pool
  139. core = self.forward_metadata.core_metadata
  140. ratio = layer.compress_ratio
  141. indexer = layer.indexer
  142. page_indices = core.sparse_page_indices(ratio)
  143. raw_indices = core.sparse_raw_indices(ratio)
  144. page_indices.fill_(-1)
  145. if raw_indices is not None:
  146. raw_indices.fill_(-1)
  147. seq_lens_cpu = _as_int_list(forward_batch.seq_lens_cpu)
  148. assert seq_lens_cpu is not None
  149. device = pos.device
  150. # Visible compressed positions per request at its newest token; the
  151. # per-token count (pos + 1) // ratio bounds each row below.
  152. lc_per_req = [s // ratio for s in seq_lens_cpu]
  153. req_pool_indices = forward_batch.req_pool_indices.to(torch.int64)
  154. slot_chunks, starts, start = [], [], 0
  155. for r, lc in enumerate(lc_per_req):
  156. starts.append(start)
  157. if lc == 0:
  158. continue
  159. j = torch.arange(lc, device=device)
  160. slot_chunks.append(
  161. self.req_to_token[req_pool_indices[r], j * ratio].to(torch.int64)
  162. // ratio
  163. )
  164. start += lc
  165. empty_mask = torch.zeros(0, 0, dtype=torch.bool, device=device)
  166. num_tokens = pos.shape[0]
  167. # TODO: move this to candidate indexer
  168. if not slot_chunks or num_tokens == 0:
  169. if indexer.is_candidate_source:
  170. self.forward_metadata.candidate_metadata = CandidateMasks(
  171. request_masks=[empty_mask for _ in lc_per_req]
  172. )
  173. return
  174. k_slots = torch.cat(slot_chunks)
  175. k_fp4, k_sf = pool.get_low_ratio_index_k_fp4(layer.layer_id, k_slots)
  176. q = indexer.queries(q_lora, layer.freqs_cis[pos]) # [T, H, 128] fp4 grid
  177. num_heads = q.shape[1]
  178. q_fp4, q_sf = quantize_fp4_indexer_tensor(q.flatten(0, 1), rne=True)
  179. q_fp4 = q_fp4.view(num_tokens, num_heads, 64)
  180. q_sf = q_sf.view(num_tokens, num_heads)
  181. weights = indexer.head_weights(x).float()
  182. compress_lens = ((pos + 1) // ratio).to(torch.int32)
  183. ks = torch.repeat_interleave(
  184. torch.tensor(starts, dtype=torch.int32, device=device),
  185. q_lens.to(torch.int64),
  186. output_size=num_tokens,
  187. )
  188. topk = indexer.index_topk
  189. selected = torch.empty((num_tokens, topk), dtype=torch.int32, device=device)
  190. _tp = getattr(self, "_dsv41_native_prefill_tp", None)
  191. if _tp is not None and _tp.eligible(
  192. num_tokens, max(lc_per_req),
  193. prefill=forward_batch.forward_mode.is_extend_without_speculative(),
  194. device=device,
  195. ):
  196. _consume = (
  197. published_masks(self.forward_metadata.candidate_metadata).request_masks
  198. if indexer.uses_candidates and not indexer.is_candidate_source else None
  199. )
  200. _tail = self.tail_forward_metadata
  201. _pub_rows = None
  202. if (indexer.is_candidate_source and _tail is not None
  203. and _tail.late_layer_tail is not None
  204. and getattr(_tail.late_layer_tail, "cp_metadata", None) is None):
  205. _pub_rows = list(_tail.late_layer_tail.extend_seq_lens_cpu)
  206. selected, _masks = _split_select(
  207. _tp, indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, compress_lens,
  208. lc_per_req, q_lens_cpu, budget=_DENSE_INDEXER_LOGITS_BUDGET_BYTES,
  209. scorer=_dense_fp4_mqa_logits, topk_fn=topk_transform_ragged_v2,
  210. mask_topk=mask_topk_scores, consume=_consume, publish_rows=_pub_rows,
  211. layer_id=layer.layer_id, ratio=ratio, ceil_align=ceil_align,
  212. )
  213. if indexer.is_candidate_source:
  214. self.forward_metadata.candidate_metadata = CandidateMasks(request_masks=_masks)
  215. unselected = torch.iinfo(torch.int32).max
  216. selected = selected.masked_fill(selected < 0, unselected).sort(dim=-1).values
  217. chosen = selected != unselected
  218. page_indices[:num_tokens, :topk] = torch.where(
  219. chosen, k_slots[selected.clamp_max(k_slots.shape[0] - 1)], -1
  220. ).to(torch.int32)
  221. if raw_indices is not None:
  222. raw_indices[:num_tokens, :topk] = torch.where(
  223. chosen, selected - ks[:, None], -1
  224. )
  225. return
  226. publish = [] if indexer.is_candidate_source else None
  227. consume = (
  228. published_masks(self.forward_metadata.candidate_metadata).request_masks
  229. if indexer.uses_candidates and publish is None
  230. else None
  231. )
  232. publish_rows_per_req = None
  233. tail_metadata = self.tail_forward_metadata
  234. if (
  235. publish is not None
  236. and tail_metadata is not None
  237. and tail_metadata.late_layer_tail is not None
  238. and getattr(tail_metadata.late_layer_tail, "cp_metadata", None) is None
  239. ):
  240. publish_rows_per_req = tail_metadata.late_layer_tail.extend_seq_lens_cpu
  241. assert len(publish_rows_per_req) == len(q_lens_cpu)
  242. tok_start = 0
  243. for b, (lc, t_len) in enumerate(zip(lc_per_req, q_lens_cpu)):
  244. rows_b = slice(tok_start, tok_start + t_len)
  245. tok_start += t_len
  246. if lc == 0 or t_len == 0:
  247. if publish is not None:
  248. publish.append(empty_mask)
  249. if t_len:
  250. selected[rows_b].fill_(-1)
  251. continue
  252. lc_aligned = ceil_align(lc, 4)
  253. rows_per_chunk = max(
  254. 1, _DENSE_INDEXER_LOGITS_BUDGET_BYTES // (lc_aligned * 4)
  255. )
  256. mask_b = None
  257. first_pub = 0
  258. if publish is not None:
  259. pub_rows = (
  260. t_len
  261. if publish_rows_per_req is None
  262. else min(int(publish_rows_per_req[b]), t_len)
  263. )
  264. first_pub = t_len - pub_rows
  265. mask_b = torch.empty((pub_rows, lc), dtype=torch.bool, device=device)
  266. j = torch.arange(lc, device=device)
  267. for c0 in range(0, t_len, rows_per_chunk):
  268. c1 = min(c0 + rows_per_chunk, t_len)
  269. rows = slice(rows_b.start + c0, rows_b.start + c1)
  270. lens = compress_lens[rows]
  271. logits = _dense_fp4_mqa_logits(
  272. (q_fp4[rows], q_sf[rows]),
  273. (k_fp4, k_sf),
  274. weights[rows],
  275. ks[rows],
  276. ks[rows] + lens,
  277. lc_aligned,
  278. )
  279. scores = logits[:, :lc]
  280. if publish is not None:
  281. scores.masked_fill_(j[None, :] >= lens[:, None], -torch.inf)
  282. p0 = max(c0, first_pub)
  283. if p0 < c1:
  284. mask_b[p0 - first_pub : c1 - first_pub] = (
  285. select_candidate_blocks(
  286. scores[p0 - c0 :],
  287. lens[p0 - c0 :, None],
  288. topk_blocks=indexer.candidate_topk_blocks,
  289. block_size=indexer.candidate_block_size,
  290. )
  291. )
  292. elif consume is not None:
  293. scores.masked_fill_(~consume[b][c0:c1], -torch.inf)
  294. topk_transform_ragged_v2(
  295. logits, lens, out_offsets=ks[rows], out_indices=selected[rows]
  296. )
  297. if consume is not None:
  298. selected[rows] = mask_topk_scores(logits, selected[rows], ks[rows])
  299. del logits, scores
  300. if publish is not None:
  301. publish.append(mask_b)
  302. if publish is not None:
  303. self.forward_metadata.candidate_metadata = CandidateMasks(
  304. request_masks=publish
  305. )
  306. unselected = torch.iinfo(torch.int32).max
  307. selected = selected.masked_fill(selected < 0, unselected).sort(dim=-1).values
  308. chosen = selected != unselected
  309. page_indices[:num_tokens, :topk] = torch.where(
  310. chosen, k_slots[selected.clamp_max(k_slots.shape[0] - 1)], -1
  311. ).to(torch.int32)
  312. if raw_indices is not None:
  313. raw_indices[:num_tokens, :topk] = torch.where(
  314. chosen, selected - ks[:, None], -1
  315. )
  316. # ---- end verbatim ----
  317. return _low_ratio_index_topk_dense
  318. NEEDED = ("_dense_fp4_mqa_logits", "topk_transform_ragged_v2", "mask_topk_scores",
  319. "select_candidate_blocks", "ceil_align", "_as_int_list", "CandidateMasks",
  320. "published_masks", "get_parallel")
  321. def install(module):
  322. if not _enabled("DSV41_INDEXER_CHUNKED"):
  323. return
  324. cls = getattr(module, "DeepseekV4AttnBackend", None)
  325. missing = [n for n in NEEDED if not hasattr(module, n)]
  326. if cls is None or missing or not hasattr(cls, "_low_ratio_index_topk_dense"):
  327. raise RuntimeError(f"DSV41 indexer chunked v3: backend drifted (missing {missing or 'class/method'}); refusing to boot")
  328. stock_src = inspect.getsource(cls._low_ratio_index_topk_dense)
  329. if "candidate_metadata" not in stock_src or "self.candidate_masks" in stock_src:
  330. raise RuntimeError("DSV41 indexer chunked v2: stock indexer is not the candidate_metadata variant; use indexer_chunked (v1) -- refusing to boot")
  331. if "_DENSE_INDEXER_LOGITS_BUDGET_BYTES" in stock_src:
  332. logger.warning("DSV41 indexer chunked v2: stock already carries #39187; leaving it alone")
  333. return
  334. from sglang.kernels.ops.attention.dsv4.fp4_indexer import quantize_fp4_indexer_tensor
  335. budget = budget_bytes()
  336. cls._low_ratio_index_topk_dense = make_index_topk_dense(module, quantize_fp4_indexer_tensor, budget)
  337. split_on = _enabled("SPARK_PREFILL_TP_SPLIT")
  338. if split_on:
  339. from spark_prefill_dense import NativePrefillTPSplit
  340. get_parallel = module.get_parallel
  341. original_init = cls.__init__
  342. def __init__(self, *args, **kwargs):
  343. original_init(self, *args, **kwargs)
  344. self._dsv41_native_prefill_tp = NativePrefillTPSplit.from_parallel(
  345. get_parallel(), is_draft=self.is_draft_runner
  346. )
  347. cls.__init__ = __init__
  348. logger.warning("DSV41 indexer chunked v3 (sglang#39187 verbatim + SG18 prefill TP split=%s) ARMED: <= %d MiB fp32 logits per row chunk, tail-only candidate masks; DSV41_INDEXER_CHUNKED=0 disables", split_on, budget >> 20)