indexer_chunked.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """Bound the dense prefill indexer transient (backport of sgl-project/sglang#39187).
  2. The stock ``_low_ratio_index_topk_dense`` scores a whole prefill chunk against the full
  3. compressed context in ONE fp32 tensor ``[T, lc]`` and then builds the candidate masks
  4. as a list plus a ``torch.cat`` copy. Peak transient is ~14 B x chunk x prefix per rank
  5. (docs/chunked-prefill-memory.md), which is why this fleet runs CHUNKED_PREFILL_SIZE=1024
  6. and an adaptive sizer. Upstream #39187 (kpham-sgl, dsv4.1 branch) scores each request
  7. in row chunks bounded by a fixed logits budget, builds the masks straight into one
  8. preallocated tensor, and under decoder SWA bounded replay publishes only each request's
  9. tail rows (enter_late_layer_tail slices every mask down to those rows anyway).
  10. Measured upstream on 4x GB300: 300K cold prompt transient 49.7 -> 10.7 GB, 1M prompt
  11. 9.6 GB, prefill time unchanged-to-better; page_indices / raw_indices / masks bitwise
  12. equal to the original path (DeepGEMM fp8_fp4_mqa_logits is row-count invariant).
  13. Adapted to this image (e087e662): candidate masks live in ``self.candidate_masks`` (a
  14. list) rather than ``forward_metadata.candidate_metadata``; the helper is
  15. ``_mask_topk_scores``. Everything else is taken from the module at install time, so
  16. a renamed symbol is a loud failure at boot, not silent corruption.
  17. Gate: ``DSV41_INDEXER_CHUNKED=1``. Budget: ``DSV41_INDEXER_LOGITS_BUDGET_BYTES``
  18. (default 2 GiB of fp32 logits, upstream's value). Off = stock path untouched.
  19. """
  20. import logging
  21. import os
  22. import torch
  23. logger = logging.getLogger(__name__)
  24. DEFAULT_BUDGET_BYTES = 1 << 31
  25. def _enabled(name, default="0"):
  26. return os.environ.get(name, default).strip() not in ("0", "off", "false", "")
  27. def budget_bytes():
  28. raw = os.environ.get("DSV41_INDEXER_LOGITS_BUDGET_BYTES", "").strip()
  29. if not raw:
  30. return DEFAULT_BUDGET_BYTES
  31. try:
  32. value = int(raw)
  33. except ValueError:
  34. logger.warning("DSV41_INDEXER_LOGITS_BUDGET_BYTES=%r is not an int; using %d",
  35. raw, DEFAULT_BUDGET_BYTES)
  36. return DEFAULT_BUDGET_BYTES
  37. return value if value > 0 else DEFAULT_BUDGET_BYTES
  38. def make_index_topk_dense(module, quantize_fp4_indexer_tensor, budget):
  39. """Build the replacement method from the module's own helpers."""
  40. dense_logits = module._dense_fp4_mqa_logits
  41. topk_ragged = module.topk_transform_ragged_v2
  42. mask_topk_scores = module._mask_topk_scores
  43. select_candidate_blocks = module.select_candidate_blocks
  44. ceil_align = module.ceil_align
  45. as_int_list = module._as_int_list
  46. def _low_ratio_index_topk_dense(
  47. self, layer, x, q_lora, pos, forward_batch, q_lens, q_lens_cpu
  48. ) -> None:
  49. """Dense fp4 indexer over rows laid out request after request, `q_lens[b]` each,
  50. scored in row chunks of at most `budget` bytes of fp32 logits per chunk."""
  51. pool = self.token_to_kv_pool
  52. core = self.forward_metadata.core_metadata
  53. ratio = layer.compress_ratio
  54. indexer = layer.indexer
  55. page_indices = core.sparse_page_indices(ratio)
  56. raw_indices = core.sparse_raw_indices(ratio)
  57. page_indices.fill_(-1)
  58. if raw_indices is not None:
  59. raw_indices.fill_(-1)
  60. seq_lens_cpu = as_int_list(forward_batch.seq_lens_cpu)
  61. assert seq_lens_cpu is not None
  62. device = pos.device
  63. lc_per_req = [s // ratio for s in seq_lens_cpu]
  64. req_pool_indices = forward_batch.req_pool_indices.to(torch.int64)
  65. slot_chunks, starts, start = [], [], 0
  66. for r, lc in enumerate(lc_per_req):
  67. starts.append(start)
  68. if lc == 0:
  69. continue
  70. j = torch.arange(lc, device=device)
  71. slot_chunks.append(
  72. self.req_to_token[req_pool_indices[r], j * ratio].to(torch.int64)
  73. // ratio
  74. )
  75. start += lc
  76. empty_mask = torch.zeros(0, 0, dtype=torch.bool, device=device)
  77. num_tokens = pos.shape[0]
  78. if not slot_chunks or num_tokens == 0:
  79. if indexer.is_candidate_source:
  80. self.candidate_masks = [empty_mask for _ in lc_per_req]
  81. return
  82. k_slots = torch.cat(slot_chunks)
  83. k_fp4, k_sf = pool.get_low_ratio_index_k_fp4(layer.layer_id, k_slots)
  84. q = indexer.queries(q_lora, layer.freqs_cis[pos]) # [T, H, 128] fp4 grid
  85. num_heads = q.shape[1]
  86. q_fp4, q_sf = quantize_fp4_indexer_tensor(q.flatten(0, 1), rne=True)
  87. q_fp4 = q_fp4.view(num_tokens, num_heads, 64)
  88. q_sf = q_sf.view(num_tokens, num_heads)
  89. weights = indexer.head_weights(x).float()
  90. compress_lens = ((pos + 1) // ratio).to(torch.int32)
  91. ks = torch.repeat_interleave(
  92. torch.tensor(starts, dtype=torch.int32, device=device),
  93. q_lens.to(torch.int64),
  94. output_size=num_tokens,
  95. )
  96. topk = indexer.index_topk
  97. selected = torch.empty((num_tokens, topk), dtype=torch.int32, device=device)
  98. publish = [] if indexer.is_candidate_source else None
  99. consume = (
  100. self.candidate_masks
  101. if indexer.uses_candidates and publish is None
  102. else None
  103. )
  104. # Under decoder SWA bounded replay only the tail rows of each request survive
  105. # enter_late_layer_tail, so publish just those (the cut there becomes a no-op).
  106. publish_rows_per_req = None
  107. tail_metadata = getattr(self, "tail_forward_metadata", None)
  108. if (
  109. publish is not None
  110. and tail_metadata is not None
  111. and tail_metadata.late_layer_tail is not None
  112. and getattr(tail_metadata.late_layer_tail, "cp_metadata", None) is None
  113. ):
  114. publish_rows_per_req = tail_metadata.late_layer_tail.extend_seq_lens_cpu
  115. assert len(publish_rows_per_req) == len(q_lens_cpu)
  116. tok_start = 0
  117. for b, (lc, t_len) in enumerate(zip(lc_per_req, q_lens_cpu)):
  118. rows_b = slice(tok_start, tok_start + t_len)
  119. tok_start += t_len
  120. if lc == 0 or t_len == 0:
  121. if publish is not None:
  122. publish.append(empty_mask)
  123. if t_len:
  124. selected[rows_b].fill_(-1)
  125. continue
  126. # the fused top-k reads score rows through 16-byte vectors
  127. lc_aligned = ceil_align(lc, 4)
  128. rows_per_chunk = max(1, budget // (lc_aligned * 4))
  129. mask_b = None
  130. first_pub = 0
  131. if publish is not None:
  132. pub_rows = (
  133. t_len
  134. if publish_rows_per_req is None
  135. else min(int(publish_rows_per_req[b]), t_len)
  136. )
  137. first_pub = t_len - pub_rows
  138. mask_b = torch.empty((pub_rows, lc), dtype=torch.bool, device=device)
  139. j = torch.arange(lc, device=device)
  140. for c0 in range(0, t_len, rows_per_chunk):
  141. c1 = min(c0 + rows_per_chunk, t_len)
  142. rows = slice(rows_b.start + c0, rows_b.start + c1)
  143. lens = compress_lens[rows]
  144. logits = dense_logits(
  145. (q_fp4[rows], q_sf[rows]),
  146. (k_fp4, k_sf),
  147. weights[rows],
  148. ks[rows],
  149. ks[rows] + lens,
  150. lc_aligned,
  151. )
  152. scores = logits[:, :lc]
  153. if publish is not None:
  154. # the block selection tells unreachable positions apart by -inf
  155. scores.masked_fill_(j[None, :] >= lens[:, None], -torch.inf)
  156. p0 = max(c0, first_pub)
  157. if p0 < c1:
  158. mask_b[p0 - first_pub : c1 - first_pub] = (
  159. select_candidate_blocks(
  160. scores[p0 - c0 :],
  161. lens[p0 - c0 :, None],
  162. topk_blocks=indexer.candidate_topk_blocks,
  163. block_size=indexer.candidate_block_size,
  164. )
  165. )
  166. elif consume is not None:
  167. scores.masked_fill_(~consume[b][c0:c1], -torch.inf)
  168. topk_ragged(
  169. logits, lens, out_offsets=ks[rows], out_indices=selected[rows]
  170. )
  171. if consume is not None:
  172. selected[rows] = mask_topk_scores(logits, selected[rows], ks[rows])
  173. del logits, scores
  174. if publish is not None:
  175. publish.append(mask_b)
  176. if publish is not None:
  177. self.candidate_masks = publish
  178. # ascending positions, padding last: the layout the consumers expect
  179. unselected = torch.iinfo(torch.int32).max
  180. selected = selected.masked_fill(selected < 0, unselected).sort(dim=-1).values
  181. chosen = selected != unselected
  182. page_indices[:num_tokens, :topk] = torch.where(
  183. chosen, k_slots[selected.clamp_max(k_slots.shape[0] - 1)], -1
  184. ).to(torch.int32)
  185. if raw_indices is not None:
  186. raw_indices[:num_tokens, :topk] = torch.where(
  187. chosen, selected - ks[:, None], -1
  188. )
  189. return _low_ratio_index_topk_dense
  190. def install(module):
  191. """Hook for sglang.srt.layers.attention.deepseek_v4_backend."""
  192. if not _enabled("DSV41_INDEXER_CHUNKED"):
  193. return
  194. cls = getattr(module, "DeepseekV4AttnBackend", None)
  195. missing = [
  196. name
  197. for name in (
  198. "_dense_fp4_mqa_logits", "topk_transform_ragged_v2", "_mask_topk_scores",
  199. "select_candidate_blocks", "ceil_align", "_as_int_list",
  200. )
  201. if not hasattr(module, name)
  202. ]
  203. if cls is None or missing or not hasattr(cls, "_low_ratio_index_topk_dense"):
  204. raise RuntimeError(
  205. f"DSV41 indexer chunked: backend drifted (missing {missing or 'class/method'}); refusing to boot"
  206. )
  207. import inspect
  208. stock_src = inspect.getsource(cls._low_ratio_index_topk_dense)
  209. if "self.candidate_masks" not in stock_src and "_publish_or_consume_candidates" not in stock_src:
  210. # Newer branches keep the masks in forward_metadata.candidate_metadata (and may
  211. # already carry #39187 natively): this replacement would publish to the wrong place.
  212. raise RuntimeError(
  213. "DSV41 indexer chunked: stock indexer no longer uses self.candidate_masks; "
  214. "this backport targets image e087e662 only -- refusing to boot"
  215. )
  216. from sglang.kernels.ops.attention.dsv4.fp4_indexer import quantize_fp4_indexer_tensor
  217. budget = budget_bytes()
  218. cls._low_ratio_index_topk_dense = make_index_topk_dense(
  219. module, quantize_fp4_indexer_tensor, budget
  220. )
  221. logger.warning(
  222. "DSV41 indexer chunked (sglang#39187 backport) ARMED: dense prefill indexer scored "
  223. "in row chunks of <= %d MiB fp32 logits, tail-only candidate masks under decoder "
  224. "replay; DSV41_INDEXER_CHUNKED=0 disables", budget >> 20,
  225. )