09-models_deepseek_v4_1_nvidia_flashinfer_sparse.py.diff 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. --- baked-ours/models/deepseek_v4_1/nvidia/flashinfer_sparse.py 2026-09-10 20:13:20.000000000 +0200
  2. +++ baked/models/deepseek_v4_1/nvidia/flashinfer_sparse.py 2026-09-11 10:13:47.000000000 +0200
  3. @@ -2,11 +2,14 @@
  4. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  5. """DeepSeek V4 FlashInfer sparse MLA backend."""
  6. +import os
  7. from typing import TYPE_CHECKING, ClassVar, cast
  8. import torch
  9. from vllm.config import VllmConfig
  10. +from vllm.distributed import get_dcp_group
  11. +from vllm.logger import init_logger
  12. from vllm.config.cache import CacheDType
  13. from vllm.forward_context import get_forward_context
  14. from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
  15. @@ -32,10 +35,94 @@
  16. get_dspark_swa_index_width,
  17. )
  18. from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWABackend
  19. +from vllm.v1.attention.backends.mla.sparse_utils import (
  20. + triton_filter_and_convert_dcp_index,
  21. +)
  22. +from vllm.v1.attention.ops.dcp import (
  23. + CPTritonContext,
  24. + cp_lse_ag_out_rs,
  25. + dcp_a2a_lse_reduce,
  26. +)
  27. if TYPE_CHECKING:
  28. from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
  29. +logger = init_logger(__name__)
  30. +
  31. +from vllm.model_executor.kernels.attention.dsa.candidate_blocks import ( # noqa: E402
  32. + _PREFILL_TIMING as _DSV41_PREFILL_TIMING,
  33. + _PrefillTimer as _DSV41PrefillTimer,
  34. + prefill_acc_add as _dsv41_prefill_acc_add,
  35. +)
  36. +
  37. +# Stage 9: DCP prefill attention via compressed-KV gather (no q gather / combine).
  38. +_DCP_PREFILL_GATHER = os.environ.get("DSV41_DCP_PREFILL_GATHER", "0") == "1"
  39. +
  40. +# FlashInfer's SM120 sparse-MLA kernels write this (not -inf) into the LSE of a
  41. +# row with no candidate and no sink; the DCP combine needs -inf there.
  42. +_SM120_EMPTY_LSE = -1e29
  43. +
  44. +
  45. +def _sm120_sparse_attention_with_lse(
  46. + query: torch.Tensor,
  47. + swa_kv_cache: torch.Tensor,
  48. + workspace: torch.Tensor,
  49. + swa_indices: torch.Tensor,
  50. + swa_lens: torch.Tensor | None,
  51. + compressed_kv_cache: torch.Tensor | None,
  52. + extra_indices: torch.Tensor | None,
  53. + extra_lens: torch.Tensor | None,
  54. + out: torch.Tensor,
  55. + sm_scale: float,
  56. + sinks: torch.Tensor | None,
  57. + lse: torch.Tensor,
  58. +) -> None:
  59. + """FlashInfer's SM120 packed sparse-MLA attention, returning the LSE.
  60. +
  61. + Mirrors the SM120 branch of ``trtllm_batch_decode_sparse_mla_dsv4``
  62. + (flashinfer 0.7.0rc1, ``flashinfer/mla/_core.py``), whose public entry
  63. + hard-codes ``return_lse=False``. ``query``/``out`` are ``[T, H, 512]``
  64. + bf16, ``lse`` is ``[T, H]`` fp32 and receives the LSE in base 2 of the
  65. + ``sm_scale``-scaled logits with the sink folded in (``log2(2^lse +
  66. + 2^(sink*log2e))``); rows with no candidate and no sink hold ``-1e30``.
  67. + """
  68. + from flashinfer.mla._core import (
  69. + _check_sm120_dsv4_kv_cache_layout,
  70. + _SparseMLASegment,
  71. + _trtllm_batch_decode_sparse_mla_sm120,
  72. + )
  73. +
  74. + num_tokens = query.shape[0]
  75. + swa_kv_cache = _check_sm120_dsv4_kv_cache_layout(swa_kv_cache, "NHD", "swa_kv_cache")
  76. + segments = [
  77. + _SparseMLASegment(indices=swa_indices.reshape(num_tokens, -1), lengths=swa_lens)
  78. + ]
  79. + if extra_indices is not None:
  80. + assert compressed_kv_cache is not None
  81. + compressed_kv_cache = _check_sm120_dsv4_kv_cache_layout(
  82. + compressed_kv_cache, "NHD", "compressed_kv_cache"
  83. + )
  84. + segments.append(
  85. + _SparseMLASegment(
  86. + indices=extra_indices.reshape(num_tokens, -1),
  87. + lengths=extra_lens,
  88. + kv_cache=compressed_kv_cache,
  89. + )
  90. + )
  91. + _trtllm_batch_decode_sparse_mla_sm120(
  92. + query=query.unsqueeze(1),
  93. + kv_cache=swa_kv_cache,
  94. + workspace_buffer=workspace,
  95. + sparse_mla_segments=segments,
  96. + out=out.unsqueeze(1),
  97. + sm_scale=float(sm_scale),
  98. + sinks=sinks,
  99. + lse=lse,
  100. + return_lse=True,
  101. + kv_scale_format="auto",
  102. + kv_cache_format="fp8",
  103. + )
  104. +
  105. _FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
  106. _flashinfer_dsv4_workspace_by_device: dict[torch.device, torch.Tensor] = {}
  107. @@ -192,6 +279,23 @@
  108. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
  109. + def __init__(self, *args, **kwargs):
  110. + super().__init__(*args, **kwargs)
  111. + # DCP port staging (DCP2-PLAN.md): the sharded-KV attention path
  112. + # (per-rank sparse attention + LSE combine) lands in stage 3. Until
  113. + # DSV41_DCP_STAGE >= 3, a DCP>1 boot must fail here -- after the KV
  114. + # cache config is computed and logged, before any forward -- rather
  115. + # than run the dcp=1 kernels over a sharded cache and serve garbage.
  116. + import os
  117. +
  118. + dcp = self.vllm_config.parallel_config.decode_context_parallel_size
  119. + if dcp > 1 and int(os.environ.get("DSV41_DCP_STAGE", "3")) < 3:
  120. + raise NotImplementedError(
  121. + f"DeepSeek-V4.1 sparse MLA under DCP{dcp}: the attention combine "
  122. + "(port stage 3) is disabled by DSV41_DCP_STAGE; unset it or run "
  123. + "with --decode-context-parallel-size 1."
  124. + )
  125. +
  126. class DeepseekSparseSWAFlashInferMetadataBuilder(DeepseekV41SparseSWAMetadataBuilder):
  127. """SWA metadata for the FlashInfer sparse decode path (varlen decode)."""
  128. @@ -580,6 +684,11 @@
  129. backend_cls = DeepseekV4FlashInferMLASparseBackend
  130. swa_backend_cls = DeepseekSparseSWAFlashInferSM120Backend
  131. use_fp8_ds_mla_layout: ClassVar[bool] = True
  132. + # DCP: per-rank partial attention with the LSE + cross-rank combine.
  133. + can_return_lse_for_decode: ClassVar[bool] = True
  134. + # Under DCP the per-token q gather / combine has no cross-token dependency:
  135. + # bound the transient [T, 2*heads, 512] buffers inside a prefill chunk.
  136. + DCP_PREFILL_TOKEN_CHUNK: ClassVar[int] = 2048
  137. @staticmethod
  138. def _get_workspace(device: torch.device) -> torch.Tensor:
  139. @@ -618,14 +727,56 @@
  140. from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120_config
  141. required_topk = _required_sm120_sparse_topk(vllm_config, self.window_size)
  142. - if not has_flashinfer_sparse_mla_sm120_config(self.padded_heads, required_topk):
  143. + # DCP: the kernel sees the gathered heads [rank0 heads | rank1 heads |
  144. + # ...] in TP order; the local slice must be unpadded so the head
  145. + # reduce-scatter hands each rank exactly its own heads back.
  146. + self.n_dcp_heads = self.n_local_heads * self.dcp_world_size
  147. + if self.dcp_world_size > 1:
  148. + if self.padded_heads != self.n_local_heads:
  149. + raise NotImplementedError(
  150. + "DeepSeek-V4.1 DCP needs an unpadded local head count in "
  151. + f"{_SPARSE_MLA_SUPPORTED_Q_HEADS}; got {self.n_local_heads} "
  152. + f"(padded to {self.padded_heads})."
  153. + )
  154. + if _pad_to_supported_q_heads(self.n_dcp_heads) != self.n_dcp_heads:
  155. + raise NotImplementedError(
  156. + f"DeepSeek-V4.1 DCP gathered head count {self.n_dcp_heads} is "
  157. + f"not a supported sparse-MLA h_q {_SPARSE_MLA_SUPPORTED_Q_HEADS}."
  158. + )
  159. + kernel_heads = self.n_dcp_heads if self.dcp_world_size > 1 else self.padded_heads
  160. + if not has_flashinfer_sparse_mla_sm120_config(kernel_heads, required_topk):
  161. raise RuntimeError(
  162. "FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires a FlashInfer "
  163. "DSV4 sparse MLA decode specialization for "
  164. - f"(num_q_heads={self.padded_heads}, top_k={required_topk}). "
  165. + f"(num_q_heads={kernel_heads}, top_k={required_topk}). "
  166. "Install a FlashInfer build containing "
  167. "flashinfer-ai/flashinfer#4380."
  168. )
  169. + # DCP state: the group (consecutive TP ranks), the gathered sink
  170. + # (built lazily on the first forward, after weight loading) and the
  171. + # neutral SWA index rows used on ranks other than 0.
  172. + self.dcp_group = get_dcp_group() if self.dcp_world_size > 1 else None
  173. + self._dcp_sink: torch.Tensor | None = None
  174. + self._dcp_empty_swa: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
  175. + # One Triton launch context per layer for the LSE-combine kernel (a
  176. + # fresh context per call re-specializes the launch on every step).
  177. + self._cp_ctx = CPTritonContext() if self.dcp_world_size > 1 else None
  178. + # Combine flavour: "ag_rs" (LSE all-gather + head reduce-scatter, two
  179. + # collectives) or "a2a" (one all-to-all of packed output+LSE).
  180. + self._dcp_combine_a2a = os.environ.get("DSV41_DCP_COMBINE", "ag_rs") == "a2a"
  181. + # Optional per-phase timing (CUDA events, synchronises; diagnostics
  182. + # only): DSV41_DCP_TIMING=1 logs gather / kernel / combine ms per call,
  183. + # averaged over the last 100 calls, on DCP rank 0.
  184. + self._dcp_timing = os.environ.get("DSV41_DCP_TIMING", "0") == "1"
  185. + self._dcp_t_acc = [0.0, 0.0, 0.0, 0]
  186. + if self.dcp_world_size > 1 and self.compress_ratio > 0:
  187. + logger.info_once(
  188. + "DeepSeek-V4.1 SM120 attention under DCP%d: %d local heads -> %d "
  189. + "gathered, SWA window + sink on DCP rank 0, base-2 LSE combine",
  190. + self.dcp_world_size,
  191. + self.n_local_heads,
  192. + self.n_dcp_heads,
  193. + )
  194. self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
  195. self._o_proj_block_size
  196. )
  197. @@ -762,6 +913,262 @@
  198. q = padded_query
  199. return q.contiguous()
  200. + # ---- DCP helpers -------------------------------------------------------
  201. +
  202. + def _compressed_topk_to_slots(
  203. + self,
  204. + topk_indices: torch.Tensor,
  205. + token_to_req_indices: torch.Tensor,
  206. + block_table: torch.Tensor,
  207. + block_size: int,
  208. + is_valid_token: torch.Tensor,
  209. + ) -> tuple[torch.Tensor, torch.Tensor]:
  210. + """Indexer top-k (compressed ids) -> physical slots + per-row count.
  211. +
  212. + DCP1: the request-local ids map through the block table as today.
  213. + DCP>1: the ids are GLOBAL compressed ids (indexer merge / short-context
  214. + bypass); de-interleave them in state coordinates, drop the states other
  215. + ranks own and compact this rank's slots to a prefix (-1 tail, count).
  216. + ``block_size`` is the number of states per page on this rank.
  217. + """
  218. + num_tokens = topk_indices.shape[0]
  219. + if self.dcp_world_size == 1:
  220. + return compute_global_topk_indices_and_lens(
  221. + topk_indices,
  222. + token_to_req_indices,
  223. + block_table,
  224. + block_size,
  225. + is_valid_token,
  226. + )
  227. + slots, lens = triton_filter_and_convert_dcp_index(
  228. + token_to_req_indices[:num_tokens].contiguous(),
  229. + block_table,
  230. + topk_indices,
  231. + dcp_size=self.dcp_world_size,
  232. + dcp_rank=self.dcp_rank,
  233. + cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
  234. + BLOCK_SIZE=block_size,
  235. + BLOCK_STRIDE_ROWS=block_size,
  236. + NUM_TOPK_TOKENS=topk_indices.shape[1],
  237. + BLOCK_N=128,
  238. + return_valid_counts=True,
  239. + )
  240. + # CUDA-graph padding rows attend nothing (parity with the DCP1 kernel).
  241. + lens.masked_fill_(~is_valid_token[:num_tokens], 0)
  242. + return slots, lens
  243. +
  244. + # Stage 9: one prefill step's gathered compressed KV per source cache and
  245. + # remapped top-k per index buffer, shared by every layer of that step. The
  246. + # metadata object is held so the cache resets exactly once per step.
  247. + _dcp_gather_cache: ClassVar[dict] = {"meta": None, "kv": {}, "idx": {}, "geom": None}
  248. +
  249. + def _dcp_prefill_gather(
  250. + self,
  251. + compressed_k_cache: torch.Tensor,
  252. + topk_indices: torch.Tensor,
  253. + token_to_req: torch.Tensor,
  254. + is_valid_token: torch.Tensor,
  255. + attn_metadata: DeepseekV4FlashMLAMetadata,
  256. + num_decodes: int,
  257. + num_prefills: int,
  258. + block_size: int,
  259. + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
  260. + """DCP prefill without q gather / LSE combine: gather the DCP ranks'
  261. + compressed pages of every prefill request into one rank-major paged
  262. + buffer and remap the GLOBAL top-k ids into it (see stage 9 notes).
  263. + Returns (gathered cache [W*P_total, block_size, 1, B], slots, lens)."""
  264. + cache = type(self)._dcp_gather_cache
  265. + if cache["meta"] is not attn_metadata:
  266. + cache["meta"] = attn_metadata
  267. + cache["kv"] = {}
  268. + cache["idx"] = {}
  269. + cache["geom"] = None
  270. + W = self.dcp_world_size
  271. + K = self.cp_kv_cache_interleave_size
  272. + bs = block_size
  273. + dev = topk_indices.device
  274. + if cache["geom"] is None:
  275. + assert attn_metadata.seq_lens is not None
  276. + seq_lens = attn_metadata.seq_lens[num_decodes : num_decodes + num_prefills].tolist()
  277. + # states that exist after this step (state = pos // ratio) -> virtual
  278. + # blocks of bs*W states = one page per rank; at least one page so
  279. + # the collective never carries zero bytes.
  280. + pages = [max(1, -(-(int(sl) // self.compress_ratio) // (bs * W))) for sl in seq_lens]
  281. + base = [0]
  282. + for p in pages:
  283. + base.append(base[-1] + W * p)
  284. + cache["geom"] = (
  285. + pages,
  286. + torch.tensor(pages, device=dev, dtype=torch.int64),
  287. + torch.tensor(base[:-1], device=dev, dtype=torch.int64),
  288. + )
  289. + pages, pages_t, base_t = cache["geom"]
  290. + key = compressed_k_cache.data_ptr()
  291. + kv = cache["kv"].get(key)
  292. + if kv is None:
  293. + with _DSV41PrefillTimer("attn_gather", int(topk_indices.shape[0])):
  294. + pool = self._as_sparse_cache(compressed_k_cache)
  295. + parts = []
  296. + for i, p in enumerate(pages):
  297. + page_ids = attn_metadata.block_table[num_decodes + i, :p].long()
  298. + local = pool.index_select(0, page_ids).contiguous()
  299. + parts.append(self.dcp_group.all_gather(local, dim=0))
  300. + kv = parts[0] if len(parts) == 1 else torch.cat(parts, dim=0)
  301. + cache["kv"][key] = kv
  302. + ikey = (topk_indices.data_ptr(), tuple(topk_indices.shape))
  303. + idx = cache["idx"].get(ikey)
  304. + if idx is None:
  305. + n_req = max(len(pages) - 1, 0)
  306. + req = (token_to_req.long() - num_decodes).clamp_(0, n_req)
  307. + ids = topk_indices.long()
  308. + valid = ids >= 0
  309. + st = ids.clamp_min(0)
  310. + span = bs * W
  311. + vblock = st // span
  312. + off = st % span
  313. + owner = (off // K) % W
  314. + loff = (off // (K * W)) * K + off % K
  315. + slot = (base_t[req][:, None] + owner * pages_t[req][:, None] + vblock) * bs + loff
  316. + slots = torch.where(valid, slot, torch.full_like(slot, -1)).to(torch.int32)
  317. + lens = valid.sum(dim=-1, dtype=torch.int32)
  318. + lens.masked_fill_(~is_valid_token[: lens.shape[0]], 0)
  319. + idx = (slots, lens)
  320. + cache["idx"][ikey] = idx
  321. + return kv, idx[0], idx[1]
  322. +
  323. + def _dcp_attn_sink(self) -> torch.Tensor:
  324. + """The sink for the gathered heads, in TP head order (collective)."""
  325. + if self._dcp_sink is None:
  326. + assert self.dcp_group is not None
  327. + local = self.attn_sink.data[: self.n_local_heads].contiguous()
  328. + self._dcp_sink = self.dcp_group.all_gather(local, dim=0)
  329. + return self._dcp_sink
  330. +
  331. + def _dcp_neutral_swa(
  332. + self, num_tokens: int, width: int, device: torch.device
  333. + ) -> tuple[torch.Tensor, torch.Tensor]:
  334. + """All -1 SWA indices and zero lengths (ranks other than 0)."""
  335. + buf = self._dcp_empty_swa.get(width)
  336. + if buf is None or buf[0].shape[0] < num_tokens:
  337. + rows = max(num_tokens, self.max_num_batched_tokens)
  338. + buf = (
  339. + torch.full((rows, 1, width), -1, dtype=torch.int32, device=device),
  340. + torch.zeros(rows, dtype=torch.int32, device=device),
  341. + )
  342. + self._dcp_empty_swa[width] = buf
  343. + return buf[0][:num_tokens], buf[1][:num_tokens]
  344. +
  345. + def _run_sm120_dcp(
  346. + self,
  347. + q: torch.Tensor,
  348. + swa_cache: torch.Tensor,
  349. + swa_indices: torch.Tensor,
  350. + swa_lens: torch.Tensor,
  351. + extra_cache: torch.Tensor | None,
  352. + extra_indices: torch.Tensor | None,
  353. + extra_lens: torch.Tensor | None,
  354. + output: torch.Tensor,
  355. + ) -> None:
  356. + """One DCP attention step for ``q``/``output`` of ``[T, n_local_heads, 512]``.
  357. +
  358. + Gather the local heads to ``[T, n_local_heads * W, 512]`` (TP order),
  359. + run this rank's sparse attention over its compressed-state shard with
  360. + the LSE, then combine across the DCP group and reduce-scatter the heads
  361. + back. The replicated SWA window and the sink are attended on DCP rank 0
  362. + only, so with ``L_r`` the per-rank base-2 LSEs (sink folded into
  363. + ``L_0``) the combine ``L = log2(sum_r 2^L_r)`` and ``o = sum_r
  364. + 2^(L_r - L) o_r`` reproduce the DCP1 softmax exactly.
  365. + """
  366. + assert self.dcp_group is not None
  367. + num_tokens = q.shape[0]
  368. + # Timing events cannot be synchronised inside a CUDA-graph capture (the
  369. + # eager break runs on the capture stream during warmup/capture).
  370. + capturing = torch.cuda.is_current_stream_capturing()
  371. + prefill_timing = _DSV41_PREFILL_TIMING and num_tokens >= 1024 and not capturing
  372. + timing = (self._dcp_timing or prefill_timing) and not capturing
  373. + if self._dcp_timing:
  374. + logger.info_once(
  375. + "DCP path active on layer %d (capturing=%s, T=%d)",
  376. + self.layer_id,
  377. + capturing,
  378. + num_tokens,
  379. + )
  380. + if timing:
  381. + ev = [torch.cuda.Event(enable_timing=True) for _ in range(4)]
  382. + ev[0].record()
  383. + # Collective: every rank builds the gathered sink (lazily, once); only
  384. + # DCP rank 0 applies it, together with the replicated SWA window.
  385. + sink = self._dcp_attn_sink()
  386. + q_gathered = self.dcp_group.all_gather(q, dim=1)
  387. + num_heads = q_gathered.shape[1]
  388. + if timing:
  389. + ev[1].record()
  390. + out_gathered = torch.empty(
  391. + (num_tokens, num_heads, self.head_dim), dtype=torch.bfloat16, device=q.device
  392. + )
  393. + lse = torch.empty((num_tokens, num_heads), dtype=torch.float32, device=q.device)
  394. + if self.dcp_rank != 0:
  395. + swa_indices, swa_lens = self._dcp_neutral_swa(
  396. + num_tokens, swa_indices.shape[-1], q.device
  397. + )
  398. + _sm120_sparse_attention_with_lse(
  399. + q_gathered,
  400. + swa_cache,
  401. + self._get_workspace(q.device),
  402. + swa_indices,
  403. + swa_lens,
  404. + extra_cache,
  405. + extra_indices,
  406. + extra_lens,
  407. + out_gathered,
  408. + self.scale,
  409. + sink if self.dcp_rank == 0 else None,
  410. + lse,
  411. + )
  412. + if timing:
  413. + ev[2].record()
  414. + # Rows with no local candidate (and no sink) carry the kernel's -1e30
  415. + # LSE and a zero output; the combine kernel's max-subtracted exp2 makes
  416. + # that the merge identity without any masking (test_stage3_combine).
  417. + if self._dcp_combine_a2a:
  418. + combined = dcp_a2a_lse_reduce(
  419. + out_gathered, lse, self.dcp_group, is_lse_base_on_e=False
  420. + )
  421. + else:
  422. + combined = cp_lse_ag_out_rs(
  423. + out_gathered,
  424. + lse,
  425. + self.dcp_group,
  426. + ctx=self._cp_ctx,
  427. + is_lse_base_on_e=False,
  428. + )
  429. + output.copy_(combined)
  430. + if timing:
  431. + ev[3].record()
  432. + ev[3].synchronize()
  433. + if prefill_timing:
  434. + _dsv41_prefill_acc_add("attn_gather", ev[0].elapsed_time(ev[1]))
  435. + _dsv41_prefill_acc_add("attn_kernel", ev[1].elapsed_time(ev[2]))
  436. + _dsv41_prefill_acc_add("attn_combine", ev[2].elapsed_time(ev[3]))
  437. + acc = self._dcp_t_acc
  438. + acc[0] += ev[0].elapsed_time(ev[1])
  439. + acc[1] += ev[1].elapsed_time(ev[2])
  440. + acc[2] += ev[2].elapsed_time(ev[3])
  441. + acc[3] += 1
  442. + if self._dcp_timing and acc[3] % 100 == 0 and self.dcp_rank == 0:
  443. + logger.info(
  444. + "DCP timing layer %d (T=%d): gather %.3f ms, kernel %.3f ms, "
  445. + "combine %.3f ms per call (avg of %d)",
  446. + self.layer_id,
  447. + num_tokens,
  448. + acc[0] / acc[3],
  449. + acc[1] / acc[3],
  450. + acc[2] / acc[3],
  451. + acc[3],
  452. + )
  453. + acc[0] = acc[1] = acc[2] = 0.0
  454. + acc[3] = 0
  455. +
  456. def _forward_decode(
  457. self,
  458. q: torch.Tensor,
  459. @@ -789,10 +1196,11 @@
  460. raise RuntimeError(
  461. "Compressed-layer decode requires top-k indices from the indexer."
  462. )
  463. - # Local indices filled by the index-source layer's indexer.
  464. + # Indices filled by the index-source layer's indexer (request-local
  465. + # at DCP1, global compressed ids under DCP).
  466. is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
  467. block_size = attn_metadata.block_size // self.compress_ratio
  468. - global_indices, extra_sparse_lengths = compute_global_topk_indices_and_lens(
  469. + global_indices, extra_sparse_lengths = self._compressed_topk_to_slots(
  470. self.topk_indices_buffer[:num_decode_tokens],
  471. swa_metadata.token_to_req_indices,
  472. attn_metadata.block_table[:num_decodes],
  473. @@ -812,6 +1220,20 @@
  474. raise RuntimeError(
  475. "Compressed sparse MLA decode requires compressed sparse indices."
  476. )
  477. + if self.dcp_world_size > 1 and not swa_only:
  478. + # SWA-only layers keep the DCP1 path: their cache is replicated and
  479. + # every rank runs its own heads over the whole window.
  480. + self._run_sm120_dcp(
  481. + q,
  482. + swa_cache,
  483. + swa_indices,
  484. + swa_lens,
  485. + extra_cache,
  486. + extra_sparse_indices,
  487. + extra_sparse_lengths,
  488. + output,
  489. + )
  490. + return
  491. flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  492. query=q,
  493. swa_kv_cache=swa_cache,
  494. @@ -849,6 +1271,7 @@
  495. extra_sparse_indices: torch.Tensor | None = None
  496. extra_sparse_lengths: torch.Tensor | None = None
  497. + gathered_kv: torch.Tensor | None = None
  498. if not swa_only:
  499. if self.topk_indices_buffer is None:
  500. raise RuntimeError(
  501. @@ -872,15 +1295,30 @@
  502. num_decode_tokens, num_decode_tokens + num_prefill_tokens
  503. )
  504. block_size = attn_metadata.block_size // self.compress_ratio
  505. - extra_sparse_indices, extra_sparse_lengths = (
  506. - compute_global_topk_indices_and_lens(
  507. - local_topk_indices,
  508. - swa_metadata.token_to_req_indices[prefill_token_slice],
  509. - attn_metadata.block_table,
  510. - block_size,
  511. - swa_metadata.is_valid_token[prefill_token_slice],
  512. + if self.dcp_world_size > 1 and _DCP_PREFILL_GATHER:
  513. + assert compressed_k_cache is not None
  514. + gathered_kv, extra_sparse_indices, extra_sparse_lengths = (
  515. + self._dcp_prefill_gather(
  516. + compressed_k_cache,
  517. + local_topk_indices,
  518. + swa_metadata.token_to_req_indices[prefill_token_slice],
  519. + swa_metadata.is_valid_token[prefill_token_slice],
  520. + attn_metadata,
  521. + num_decodes,
  522. + num_prefills,
  523. + block_size,
  524. + )
  525. + )
  526. + else:
  527. + extra_sparse_indices, extra_sparse_lengths = (
  528. + self._compressed_topk_to_slots(
  529. + local_topk_indices,
  530. + swa_metadata.token_to_req_indices[prefill_token_slice],
  531. + attn_metadata.block_table,
  532. + block_size,
  533. + swa_metadata.is_valid_token[prefill_token_slice],
  534. + )
  535. )
  536. - )
  537. assert swa_metadata.prefill_swa_indices is not None
  538. assert swa_metadata.prefill_swa_lens is not None
  539. @@ -894,7 +1332,11 @@
  540. raise RuntimeError(
  541. "Compressed sparse MLA layers require their compressed KV cache."
  542. )
  543. - extra_kv_paged = self._as_sparse_cache(compressed_k_cache)
  544. + extra_kv_paged = (
  545. + gathered_kv
  546. + if gathered_kv is not None
  547. + else self._as_sparse_cache(compressed_k_cache)
  548. + )
  549. num_chunks = (
  550. num_prefills + self.PREFILL_CHUNK_SIZE - 1
  551. @@ -927,17 +1369,40 @@
  552. raise RuntimeError(
  553. "Compressed sparse MLA prefill requires compressed sparse indices."
  554. )
  555. - flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  556. - query=q_chunk,
  557. - swa_kv_cache=swa_kv_paged,
  558. - workspace_buffer=self._get_workspace(q.device),
  559. - sparse_indices=swa_indices_chunk,
  560. - compressed_kv_cache=extra_kv_paged,
  561. - out=output[query_start:query_end],
  562. - bmm1_scale=self.scale,
  563. - sinks=self.attn_sink,
  564. - kv_layout="NHD",
  565. - swa_topk_lens=swa_lens_chunk,
  566. - extra_sparse_indices=extra_sparse_indices_chunk,
  567. - extra_sparse_topk_lens=extra_sparse_lengths_chunk,
  568. - )
  569. + if self.dcp_world_size > 1 and not swa_only and gathered_kv is None:
  570. + # Chunk bounds come from the CPU query_start_loc (identical on
  571. + # every rank), so the collectives below line up across ranks.
  572. + qs, qe = int(query_start), int(query_end)
  573. + for sub_start in range(qs, qe, self.DCP_PREFILL_TOKEN_CHUNK):
  574. + sub_end = min(sub_start + self.DCP_PREFILL_TOKEN_CHUNK, qe)
  575. + assert extra_sparse_indices is not None
  576. + assert extra_sparse_lengths is not None
  577. + self._run_sm120_dcp(
  578. + q[sub_start:sub_end],
  579. + swa_kv_paged,
  580. + swa_metadata.prefill_swa_indices[sub_start:sub_end],
  581. + swa_metadata.prefill_swa_lens[sub_start:sub_end],
  582. + extra_kv_paged,
  583. + extra_sparse_indices[sub_start:sub_end],
  584. + extra_sparse_lengths[sub_start:sub_end],
  585. + output[sub_start:sub_end],
  586. + )
  587. + continue
  588. + with _DSV41PrefillTimer(
  589. + "attn_kernel",
  590. + int(query_end - query_start) if gathered_kv is not None else 0,
  591. + ):
  592. + flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  593. + query=q_chunk,
  594. + swa_kv_cache=swa_kv_paged,
  595. + workspace_buffer=self._get_workspace(q.device),
  596. + sparse_indices=swa_indices_chunk,
  597. + compressed_kv_cache=extra_kv_paged,
  598. + out=output[query_start:query_end],
  599. + bmm1_scale=self.scale,
  600. + sinks=self.attn_sink,
  601. + kv_layout="NHD",
  602. + swa_topk_lens=swa_lens_chunk,
  603. + extra_sparse_indices=extra_sparse_indices_chunk,
  604. + extra_sparse_topk_lens=extra_sparse_lengths_chunk,
  605. + )