| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- """v3 = v2 (sglang#39187 verbatim, candidate_metadata backends) + rhys101's SG18 native prefill
- TP split: when a prefill chunk is eligible (>= SPARK_PREFILL_TP_MIN_ROWS rows and
- >= SPARK_PREFILL_TP_MIN_CONTEXT context, SPARK_PREFILL_TP_SPLIT=1), each TP rank scores only
- its slice of the query rows and the top-k / candidate block ids are all-gathered as ints.
- Otherwise the chunked #39187 path runs unchanged.
- Original v2 header: Bound the dense prefill indexer transient on the upstream dsv4.1 branch: verbatim
- backport of sgl-project/sglang#39187 (head cb8dd033, kpham-sgl) for backends that keep
- candidate masks in ``forward_metadata.candidate_metadata`` (branch f80c91a4b and later).
- The v1 adapter (indexer_chunked.py) covers image e087e662, which uses self.candidate_masks.
- Gate: ``DSV41_INDEXER_CHUNKED=1``. Budget: ``DSV41_INDEXER_LOGITS_BUDGET_BYTES``
- (default 2 GiB of fp32 logits). Off = stock path untouched.
- """
- import inspect
- import logging
- import os
- import torch
- logger = logging.getLogger(__name__)
- DEFAULT_BUDGET_BYTES = 1 << 31
- def _enabled(name, default="0"):
- return os.environ.get(name, default).strip() not in ("0", "off", "false", "")
- def budget_bytes():
- raw = os.environ.get("DSV41_INDEXER_LOGITS_BUDGET_BYTES", "").strip()
- try:
- value = int(raw) if raw else DEFAULT_BUDGET_BYTES
- except ValueError:
- value = DEFAULT_BUDGET_BYTES
- return value if value > 0 else DEFAULT_BUDGET_BYTES
- def _split_local(indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
- world, rank, *, budget, scorer, topk_fn, mask_topk, consume, ceil_align):
- """This rank's slice of the query rows, scored in row chunks of <= budget bytes of fp32
- logits (the #39187 bound applied inside the SG18 partition). Returns the packed int32
- [extent, topk + blocks] tensor the all-gather concatenates: top-k ids, then candidate
- block ids for a source layer."""
- from spark_prefill_dense import partition, candidate_block_ids
- rows = q_fp4.shape[0]
- start, end, extent = partition(rows, world, rank)
- source = indexer.is_candidate_source
- width_max = max(lengths)
- blocks = (min(indexer.candidate_topk_blocks,
- (width_max + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
- if source else 0)
- packed = torch.full((extent, indexer.index_topk + blocks), -1, dtype=torch.int32, device=q_fp4.device)
- if start >= end:
- return packed
- lc_aligned = ceil_align(width_max, 4)
- rows_per_chunk = max(1, budget // (lc_aligned * 4))
- # request boundaries in global row space
- bounds = []
- offset = 0
- for width, count in zip(lengths, q_lengths):
- bounds.append((offset, offset + count, width))
- offset += count
- assert offset == rows
- for c0 in range(start, end, rows_per_chunk):
- c1 = min(c0 + rows_per_chunk, end)
- rows_sl = slice(c0, c1)
- local_lens = lens[rows_sl]
- local_ks = ks[rows_sl]
- logits = scorer((q_fp4[rows_sl], q_sf[rows_sl]), (k_fp4, k_sf), weights[rows_sl],
- local_ks, local_ks + local_lens, lc_aligned)
- for request, (lo_r, hi_r, width) in enumerate(bounds):
- lo = max(c0, lo_r)
- hi = min(c1, hi_r)
- if not width or lo >= hi:
- continue
- take = slice(lo - c0, hi - c0)
- scores = logits[take, :width]
- if source:
- row_lens = local_lens[take, None]
- scores.masked_fill_(torch.arange(width, device=logits.device)[None, :] >= row_lens, -torch.inf)
- nblocks = min(indexer.candidate_topk_blocks,
- (width + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
- ids = candidate_block_ids(scores, row_lens, indexer.candidate_topk_blocks,
- indexer.candidate_block_size)
- packed[lo - start:hi - start, indexer.index_topk:indexer.index_topk + nblocks] = ids
- elif consume is not None:
- scores.masked_fill_(~consume[request][lo - lo_r:hi - lo_r], -torch.inf)
- selected = torch.empty((c1 - c0, indexer.index_topk), dtype=torch.int32, device=logits.device)
- topk_fn(logits, local_lens, out_offsets=local_ks, out_indices=selected)
- if consume is not None:
- selected = mask_topk(logits, selected, local_ks)
- packed[c0 - start:c1 - start, :indexer.index_topk] = selected
- del logits
- return packed
- def _split_select(tp, indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
- *, budget, scorer, topk_fn, mask_topk, consume, publish_rows, layer_id, ratio,
- ceil_align):
- """Score this rank's partition, all-gather the packed ids, rebuild the (tail-only)
- candidate masks. Mirrors NativePrefillTPSplit.select with the chunk bound and the
- #39187 tail-only publishing."""
- from spark_prefill_dense import partition, candidate_mask
- local = _split_local(indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, lens, lengths, q_lengths,
- tp.world, tp.rank, budget=budget, scorer=scorer, topk_fn=topk_fn,
- mask_topk=mask_topk, consume=consume, ceil_align=ceil_align)
- gathered = tp.group.all_gather(local, dim=0)[: q_fp4.shape[0]]
- masks = None
- if indexer.is_candidate_source:
- masks = []
- offset = 0
- for b, (width, count) in enumerate(zip(lengths, q_lengths)):
- if not width or not count:
- masks.append(torch.zeros((0, 0), dtype=torch.bool, device=gathered.device))
- else:
- nblocks = min(indexer.candidate_topk_blocks,
- (width + indexer.candidate_block_size - 1) // indexer.candidate_block_size)
- pub = count if publish_rows is None else min(int(publish_rows[b]), count)
- ids = gathered[offset + count - pub: offset + count,
- indexer.index_topk: indexer.index_topk + nblocks]
- masks.append(candidate_mask(ids, width, indexer.candidate_block_size))
- offset += count
- key = (ratio, bool(indexer.is_candidate_source), bool(indexer.uses_candidates))
- if key not in tp._logged:
- tp._logged.add(key)
- start, end, extent = partition(q_fp4.shape[0], tp.world, tp.rank)
- 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",
- tp.rank, layer_id, ratio, indexer.is_candidate_source, indexer.uses_candidates,
- q_fp4.shape[0], max(lengths), start, end, budget >> 20)
- return gathered[:, : indexer.index_topk], masks
- def make_index_topk_dense(module, quantize_fp4_indexer_tensor, budget):
- """The PR's function, bound to the module's own helpers (names resolved once)."""
- _dense_fp4_mqa_logits = module._dense_fp4_mqa_logits
- topk_transform_ragged_v2 = module.topk_transform_ragged_v2
- mask_topk_scores = module.mask_topk_scores
- select_candidate_blocks = module.select_candidate_blocks
- ceil_align = module.ceil_align
- _as_int_list = module._as_int_list
- CandidateMasks = module.CandidateMasks
- published_masks = module.published_masks
- _DENSE_INDEXER_LOGITS_BUDGET_BYTES = budget
- # ---- begin verbatim from sglang#39187 (deepseek_v4_backend.py @ cb8dd033) ----
- def _low_ratio_index_topk_dense(
- self, layer, x, q_lora, pos, forward_batch, q_lens, q_lens_cpu
- ) -> None:
- """Dense fp4 indexer over rows laid out request after request, `q_lens[b]` each."""
- from sglang.kernels.ops.attention.dsv4.fp4_indexer import (
- quantize_fp4_indexer_tensor,
- )
- pool = self.token_to_kv_pool
- core = self.forward_metadata.core_metadata
- ratio = layer.compress_ratio
- indexer = layer.indexer
- page_indices = core.sparse_page_indices(ratio)
- raw_indices = core.sparse_raw_indices(ratio)
- page_indices.fill_(-1)
- if raw_indices is not None:
- raw_indices.fill_(-1)
- seq_lens_cpu = _as_int_list(forward_batch.seq_lens_cpu)
- assert seq_lens_cpu is not None
- device = pos.device
- # Visible compressed positions per request at its newest token; the
- # per-token count (pos + 1) // ratio bounds each row below.
- lc_per_req = [s // ratio for s in seq_lens_cpu]
- req_pool_indices = forward_batch.req_pool_indices.to(torch.int64)
- slot_chunks, starts, start = [], [], 0
- for r, lc in enumerate(lc_per_req):
- starts.append(start)
- if lc == 0:
- continue
- j = torch.arange(lc, device=device)
- slot_chunks.append(
- self.req_to_token[req_pool_indices[r], j * ratio].to(torch.int64)
- // ratio
- )
- start += lc
- empty_mask = torch.zeros(0, 0, dtype=torch.bool, device=device)
- num_tokens = pos.shape[0]
- # TODO: move this to candidate indexer
- if not slot_chunks or num_tokens == 0:
- if indexer.is_candidate_source:
- self.forward_metadata.candidate_metadata = CandidateMasks(
- request_masks=[empty_mask for _ in lc_per_req]
- )
- return
- k_slots = torch.cat(slot_chunks)
- k_fp4, k_sf = pool.get_low_ratio_index_k_fp4(layer.layer_id, k_slots)
- q = indexer.queries(q_lora, layer.freqs_cis[pos]) # [T, H, 128] fp4 grid
- num_heads = q.shape[1]
- q_fp4, q_sf = quantize_fp4_indexer_tensor(q.flatten(0, 1), rne=True)
- q_fp4 = q_fp4.view(num_tokens, num_heads, 64)
- q_sf = q_sf.view(num_tokens, num_heads)
- weights = indexer.head_weights(x).float()
- compress_lens = ((pos + 1) // ratio).to(torch.int32)
- ks = torch.repeat_interleave(
- torch.tensor(starts, dtype=torch.int32, device=device),
- q_lens.to(torch.int64),
- output_size=num_tokens,
- )
- topk = indexer.index_topk
- selected = torch.empty((num_tokens, topk), dtype=torch.int32, device=device)
- _tp = getattr(self, "_dsv41_native_prefill_tp", None)
- if _tp is not None and _tp.eligible(
- num_tokens, max(lc_per_req),
- prefill=forward_batch.forward_mode.is_extend_without_speculative(),
- device=device,
- ):
- _consume = (
- published_masks(self.forward_metadata.candidate_metadata).request_masks
- if indexer.uses_candidates and not indexer.is_candidate_source else None
- )
- _tail = self.tail_forward_metadata
- _pub_rows = None
- if (indexer.is_candidate_source and _tail is not None
- and _tail.late_layer_tail is not None
- and getattr(_tail.late_layer_tail, "cp_metadata", None) is None):
- _pub_rows = list(_tail.late_layer_tail.extend_seq_lens_cpu)
- selected, _masks = _split_select(
- _tp, indexer, q_fp4, q_sf, k_fp4, k_sf, weights, ks, compress_lens,
- lc_per_req, q_lens_cpu, budget=_DENSE_INDEXER_LOGITS_BUDGET_BYTES,
- scorer=_dense_fp4_mqa_logits, topk_fn=topk_transform_ragged_v2,
- mask_topk=mask_topk_scores, consume=_consume, publish_rows=_pub_rows,
- layer_id=layer.layer_id, ratio=ratio, ceil_align=ceil_align,
- )
- if indexer.is_candidate_source:
- self.forward_metadata.candidate_metadata = CandidateMasks(request_masks=_masks)
- unselected = torch.iinfo(torch.int32).max
- selected = selected.masked_fill(selected < 0, unselected).sort(dim=-1).values
- chosen = selected != unselected
- page_indices[:num_tokens, :topk] = torch.where(
- chosen, k_slots[selected.clamp_max(k_slots.shape[0] - 1)], -1
- ).to(torch.int32)
- if raw_indices is not None:
- raw_indices[:num_tokens, :topk] = torch.where(
- chosen, selected - ks[:, None], -1
- )
- return
- publish = [] if indexer.is_candidate_source else None
- consume = (
- published_masks(self.forward_metadata.candidate_metadata).request_masks
- if indexer.uses_candidates and publish is None
- else None
- )
- publish_rows_per_req = None
- tail_metadata = self.tail_forward_metadata
- if (
- publish is not None
- and tail_metadata is not None
- and tail_metadata.late_layer_tail is not None
- and getattr(tail_metadata.late_layer_tail, "cp_metadata", None) is None
- ):
- publish_rows_per_req = tail_metadata.late_layer_tail.extend_seq_lens_cpu
- assert len(publish_rows_per_req) == len(q_lens_cpu)
- tok_start = 0
- for b, (lc, t_len) in enumerate(zip(lc_per_req, q_lens_cpu)):
- rows_b = slice(tok_start, tok_start + t_len)
- tok_start += t_len
- if lc == 0 or t_len == 0:
- if publish is not None:
- publish.append(empty_mask)
- if t_len:
- selected[rows_b].fill_(-1)
- continue
- lc_aligned = ceil_align(lc, 4)
- rows_per_chunk = max(
- 1, _DENSE_INDEXER_LOGITS_BUDGET_BYTES // (lc_aligned * 4)
- )
- mask_b = None
- first_pub = 0
- if publish is not None:
- pub_rows = (
- t_len
- if publish_rows_per_req is None
- else min(int(publish_rows_per_req[b]), t_len)
- )
- first_pub = t_len - pub_rows
- mask_b = torch.empty((pub_rows, lc), dtype=torch.bool, device=device)
- j = torch.arange(lc, device=device)
- for c0 in range(0, t_len, rows_per_chunk):
- c1 = min(c0 + rows_per_chunk, t_len)
- rows = slice(rows_b.start + c0, rows_b.start + c1)
- lens = compress_lens[rows]
- logits = _dense_fp4_mqa_logits(
- (q_fp4[rows], q_sf[rows]),
- (k_fp4, k_sf),
- weights[rows],
- ks[rows],
- ks[rows] + lens,
- lc_aligned,
- )
- scores = logits[:, :lc]
- if publish is not None:
- scores.masked_fill_(j[None, :] >= lens[:, None], -torch.inf)
- p0 = max(c0, first_pub)
- if p0 < c1:
- mask_b[p0 - first_pub : c1 - first_pub] = (
- select_candidate_blocks(
- scores[p0 - c0 :],
- lens[p0 - c0 :, None],
- topk_blocks=indexer.candidate_topk_blocks,
- block_size=indexer.candidate_block_size,
- )
- )
- elif consume is not None:
- scores.masked_fill_(~consume[b][c0:c1], -torch.inf)
- topk_transform_ragged_v2(
- logits, lens, out_offsets=ks[rows], out_indices=selected[rows]
- )
- if consume is not None:
- selected[rows] = mask_topk_scores(logits, selected[rows], ks[rows])
- del logits, scores
- if publish is not None:
- publish.append(mask_b)
- if publish is not None:
- self.forward_metadata.candidate_metadata = CandidateMasks(
- request_masks=publish
- )
- unselected = torch.iinfo(torch.int32).max
- selected = selected.masked_fill(selected < 0, unselected).sort(dim=-1).values
- chosen = selected != unselected
- page_indices[:num_tokens, :topk] = torch.where(
- chosen, k_slots[selected.clamp_max(k_slots.shape[0] - 1)], -1
- ).to(torch.int32)
- if raw_indices is not None:
- raw_indices[:num_tokens, :topk] = torch.where(
- chosen, selected - ks[:, None], -1
- )
- # ---- end verbatim ----
- return _low_ratio_index_topk_dense
- NEEDED = ("_dense_fp4_mqa_logits", "topk_transform_ragged_v2", "mask_topk_scores",
- "select_candidate_blocks", "ceil_align", "_as_int_list", "CandidateMasks",
- "published_masks", "get_parallel")
- def install(module):
- if not _enabled("DSV41_INDEXER_CHUNKED"):
- return
- cls = getattr(module, "DeepseekV4AttnBackend", None)
- missing = [n for n in NEEDED if not hasattr(module, n)]
- if cls is None or missing or not hasattr(cls, "_low_ratio_index_topk_dense"):
- raise RuntimeError(f"DSV41 indexer chunked v3: backend drifted (missing {missing or 'class/method'}); refusing to boot")
- stock_src = inspect.getsource(cls._low_ratio_index_topk_dense)
- if "candidate_metadata" not in stock_src or "self.candidate_masks" in stock_src:
- raise RuntimeError("DSV41 indexer chunked v2: stock indexer is not the candidate_metadata variant; use indexer_chunked (v1) -- refusing to boot")
- if "_DENSE_INDEXER_LOGITS_BUDGET_BYTES" in stock_src:
- logger.warning("DSV41 indexer chunked v2: stock already carries #39187; leaving it alone")
- return
- from sglang.kernels.ops.attention.dsv4.fp4_indexer import quantize_fp4_indexer_tensor
- budget = budget_bytes()
- cls._low_ratio_index_topk_dense = make_index_topk_dense(module, quantize_fp4_indexer_tensor, budget)
- split_on = _enabled("SPARK_PREFILL_TP_SPLIT")
- if split_on:
- from spark_prefill_dense import NativePrefillTPSplit
- get_parallel = module.get_parallel
- original_init = cls.__init__
- def __init__(self, *args, **kwargs):
- original_init(self, *args, **kwargs)
- self._dsv41_native_prefill_tp = NativePrefillTPSplit.from_parallel(
- get_parallel(), is_draft=self.is_draft_runner
- )
- cls.__init__ = __init__
- 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)
|