flashinfer_sparse.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """DeepSeek V4 FlashInfer sparse MLA backend."""
  4. from typing import TYPE_CHECKING, ClassVar, cast
  5. import torch
  6. from vllm.config import VllmConfig
  7. from vllm.config.cache import CacheDType
  8. from vllm.forward_context import get_forward_context
  9. from vllm.models.deepseek_v4.nvidia.ops.o_proj import (
  10. compute_fp8_einsum_recipe,
  11. deep_gemm_fp8_o_proj,
  12. )
  13. from vllm.models.deepseek_v4_1.attention import DeepseekV4Attention
  14. from vllm.models.deepseek_v4_1.common.ops import (
  15. build_flashinfer_mixed_sparse_indices,
  16. compute_global_topk_indices_and_lens,
  17. )
  18. from vllm.models.deepseek_v4_1.sparse_mla import (
  19. DeepseekV4FlashMLAMetadata,
  20. DeepseekV4SparseMLABackend,
  21. DeepseekV4SparseMLAMetadataBuilder,
  22. DeepseekV41SparseSWAMetadataBuilder,
  23. )
  24. from vllm.platforms.interface import DeviceCapability
  25. from vllm.utils.flashinfer import flashinfer_trtllm_batch_decode_sparse_mla_dsv4
  26. from vllm.v1.attention.backend import AttentionCGSupport, MultipleOf
  27. from vllm.v1.attention.backends.mla.compressor_utils import (
  28. get_dspark_swa_index_width,
  29. )
  30. from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWABackend
  31. if TYPE_CHECKING:
  32. from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
  33. _FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE = 128 * 1024 * 1024
  34. _flashinfer_dsv4_workspace_by_device: dict[torch.device, torch.Tensor] = {}
  35. def _get_flashinfer_dsv4_workspace(device: torch.device) -> torch.Tensor:
  36. workspace = _flashinfer_dsv4_workspace_by_device.get(device)
  37. if workspace is None:
  38. workspace = torch.zeros(
  39. _FLASHINFER_DSV4_WORKSPACE_BUFFER_SIZE,
  40. dtype=torch.uint8,
  41. device=device,
  42. )
  43. _flashinfer_dsv4_workspace_by_device[device] = workspace
  44. return workspace
  45. def _packed_block_span(pool: torch.Tensor) -> int:
  46. """Per-block stride of ``pool`` in tokens (``stride(0)//stride(-2)``): ==
  47. block_size for unpacked KV, larger when packed (#44577). Raises if not
  48. token-aligned."""
  49. block_stride = pool.stride(0)
  50. token_stride = pool.stride(-2)
  51. if block_stride % token_stride != 0:
  52. raise NotImplementedError(
  53. "FLASHINFER_MLA_SPARSE_DSV4 packed KV requires the per-block stride "
  54. f"({block_stride}) to be a multiple of the per-token stride "
  55. f"({token_stride}); this layout is not supported yet."
  56. )
  57. return block_stride // token_stride
  58. # Sparse MLA h_q counts accepted natively (flashinfer>=0.6.14, #3545).
  59. _SPARSE_MLA_SUPPORTED_Q_HEADS = (8, 16, 32, 64, 128)
  60. def _pad_to_supported_q_heads(num_heads: int) -> int:
  61. for supported in _SPARSE_MLA_SUPPORTED_Q_HEADS:
  62. if num_heads <= supported:
  63. return supported
  64. raise ValueError(
  65. f"DeepseekV4 FlashInfer MLA Sparse does not support {num_heads} heads "
  66. "(sparse MLA kernel requires h_q in {8, 16, 32, 64, 128})."
  67. )
  68. def _required_sm120_sparse_topk(vllm_config: VllmConfig, window_size: int) -> int:
  69. """Return the SM120 DSV4 SWA specialization needed by this model."""
  70. if not vllm_config.attention_config.use_non_causal:
  71. return window_size
  72. speculative_config = vllm_config.speculative_config
  73. if speculative_config is None:
  74. return window_size
  75. return get_dspark_swa_index_width(
  76. window_size,
  77. speculative_config.num_speculative_tokens,
  78. )
  79. class DeepseekV4FlashInferMLASparseBackend(DeepseekV4SparseMLABackend):
  80. """FlashInfer backend using the DSv4 sparse metadata/cache layout.
  81. Inherits the base and backend reuses its``DeepseekV4SparseMLAMetadataBuilder``
  82. """
  83. supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
  84. supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
  85. "auto",
  86. "bfloat16",
  87. "fp8",
  88. "fp8_e4m3",
  89. "fp8_ds_mla",
  90. ]
  91. @staticmethod
  92. def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
  93. return [128]
  94. @staticmethod
  95. def get_name() -> str:
  96. return "FLASHINFER_MLA_SPARSE_DSV41"
  97. @classmethod
  98. def get_supported_head_sizes(cls) -> list[int]:
  99. return [512]
  100. @classmethod
  101. def supports_sink(cls) -> bool:
  102. return True
  103. @classmethod
  104. def is_sparse(cls) -> bool:
  105. return True
  106. @classmethod
  107. def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
  108. return capability.major in [10, 12]
  109. @classmethod
  110. def supports_combination(
  111. cls,
  112. head_size: int,
  113. dtype: torch.dtype,
  114. kv_cache_dtype: CacheDType | None,
  115. block_size: int | None,
  116. use_mla: bool,
  117. has_sink: bool,
  118. use_sparse: bool,
  119. use_mm_prefix: bool,
  120. device_capability: DeviceCapability,
  121. ) -> str | None:
  122. if device_capability.major == 10:
  123. if kv_cache_dtype == "fp8_ds_mla":
  124. return (
  125. "FLASHINFER_MLA_SPARSE_DSV4 SM10x uses the plain "
  126. "per-tensor FP8 KV layout, not fp8_ds_mla"
  127. )
  128. if kv_cache_dtype not in (None, "auto", "bfloat16", "fp8", "fp8_e4m3"):
  129. return "kv_cache_dtype not supported"
  130. return None
  131. if device_capability.major == 12:
  132. if kv_cache_dtype not in ("fp8", "fp8_e4m3", "fp8_ds_mla"):
  133. return "kv_cache_dtype not supported"
  134. from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120
  135. if not has_flashinfer_sparse_mla_sm120():
  136. return (
  137. "FLASHINFER_MLA_SPARSE_DSV4 SM120 requires FlashInfer's "
  138. "sparse MLA decode API"
  139. )
  140. return None
  141. return "FLASHINFER_MLA_SPARSE_DSV4 requires SM10x or SM12x"
  142. @staticmethod
  143. def get_builder_cls() -> type["DeepseekV4FlashInferSparseMLAMetadataBuilder"]:
  144. return DeepseekV4FlashInferSparseMLAMetadataBuilder
  145. class DeepseekV4FlashInferSparseMLAMetadataBuilder(DeepseekV4SparseMLAMetadataBuilder):
  146. """Varlen-capable metadata builder for the FlashInfer sparse MLA backend."""
  147. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
  148. class DeepseekSparseSWAFlashInferMetadataBuilder(DeepseekV41SparseSWAMetadataBuilder):
  149. """SWA metadata for the FlashInfer sparse decode path (varlen decode)."""
  150. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
  151. class DeepseekSparseSWAFlashInferBackend(DeepseekSparseSWABackend):
  152. @staticmethod
  153. def get_builder_cls() -> type[DeepseekSparseSWAFlashInferMetadataBuilder]:
  154. return DeepseekSparseSWAFlashInferMetadataBuilder
  155. class DeepseekV4FlashInferMLAAttention(DeepseekV4Attention):
  156. """FlashInfer TRTLLM-gen sparse MLA attention layer for SM100 DeepSeek V4."""
  157. backend_cls = DeepseekV4FlashInferMLASparseBackend
  158. swa_backend_cls = DeepseekSparseSWAFlashInferBackend
  159. use_fp8_ds_mla_layout: ClassVar[bool] = False
  160. @classmethod
  161. def get_padded_num_q_heads(cls, num_heads: int) -> int:
  162. return _pad_to_supported_q_heads(num_heads)
  163. def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
  164. return deep_gemm_fp8_o_proj(
  165. o,
  166. positions,
  167. self.rotary_emb.cos_sin_cache,
  168. self.wo_a,
  169. self.wo_b,
  170. n_groups=self.n_local_groups,
  171. heads_per_group=self.n_local_heads // self.n_local_groups,
  172. nope_dim=self.nope_head_dim,
  173. rope_dim=self.rope_head_dim,
  174. o_lora_rank=self.o_lora_rank,
  175. einsum_recipe=self._einsum_recipe,
  176. tma_aligned_scales=self._tma_aligned_scales,
  177. )
  178. def __init__(self, *args, **kwargs) -> None:
  179. super().__init__(*args, **kwargs)
  180. self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
  181. self._o_proj_block_size
  182. )
  183. # Per-tensor FP8 scale buffers + precomputed scalar BMM scales. Only the
  184. # per-tensor FP8 cache path consumes these; bf16 reads ``self.scale``.
  185. if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
  186. return
  187. fp8_q_scale = 1.0
  188. fp8_kv_scale = 1.0
  189. self.register_buffer(
  190. "_flashinfer_fp8_q_scale",
  191. torch.tensor([fp8_q_scale], dtype=torch.float32),
  192. persistent=False,
  193. )
  194. self.register_buffer(
  195. "_flashinfer_fp8_q_scale_inv",
  196. torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
  197. persistent=False,
  198. )
  199. self.register_buffer(
  200. "_flashinfer_fp8_kv_scale",
  201. torch.tensor([fp8_kv_scale], dtype=torch.float32),
  202. persistent=False,
  203. )
  204. # TRTLLM-gen takes scalar scale args on a distinct C++ path vs
  205. # one-element tensors, so these are Python floats.
  206. self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
  207. self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
  208. def forward_mqa(
  209. self,
  210. q: torch.Tensor,
  211. kv: torch.Tensor,
  212. positions: torch.Tensor,
  213. output: torch.Tensor,
  214. ) -> None:
  215. # The TRTLLM-gen kernel requires h_q in {64, 128}, so the output buffer
  216. # is allocated at the padded head count while q arrives at the local
  217. # head count; _forward pads q to match before the launcher.
  218. assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
  219. f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
  220. )
  221. assert output.shape[1] >= q.shape[1], (
  222. f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
  223. )
  224. # Per-tensor FP8 q produces a bf16 attention output.
  225. expected_output_dtype = (
  226. torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
  227. )
  228. assert output.dtype == expected_output_dtype, (
  229. f"output dtype {output.dtype} must match expected {expected_output_dtype} "
  230. f"for q dtype {q.dtype}"
  231. )
  232. forward_context = get_forward_context()
  233. attn_metadata = forward_context.attn_metadata
  234. if attn_metadata is None:
  235. # Warmup dummy run: FlashInfer reads the cache directly and lazily
  236. # allocates its workspace, so nothing to reserve here.
  237. output.zero_()
  238. return
  239. assert isinstance(attn_metadata, dict)
  240. # Compressed-cache metadata lives on the kv-source layer's prefix;
  241. # consumers share that cache and its block table.
  242. flashmla_metadata = cast(
  243. DeepseekV4FlashMLAMetadata | None,
  244. attn_metadata.get(self.compressed_cache_prefix)
  245. if self.compressed_cache_prefix is not None
  246. else None,
  247. )
  248. swa_metadata = cast(
  249. "DeepseekSparseSWAMetadata | None",
  250. attn_metadata.get(self.swa_cache_layer.prefix),
  251. )
  252. assert swa_metadata is not None
  253. swa_only = self.compress_ratio == 0
  254. # SWA-only layers have no compressed KV cache; consumers read the kv
  255. # source's cache.
  256. self_kv_cache = None if swa_only else self._compressed_kv_cache()
  257. swa_kv_cache = self.swa_cache_layer.kv_cache
  258. self._forward(
  259. q=q,
  260. kv_cache=self_kv_cache,
  261. swa_k_cache=swa_kv_cache,
  262. swa_metadata=swa_metadata,
  263. attn_metadata=flashmla_metadata,
  264. swa_only=swa_only,
  265. output=output,
  266. )
  267. def _build_sparse_index_metadata(
  268. self,
  269. kv_cache: torch.Tensor | None,
  270. swa_k_cache: torch.Tensor,
  271. swa_metadata: "DeepseekSparseSWAMetadata",
  272. attn_metadata: DeepseekV4FlashMLAMetadata | None,
  273. swa_only: bool,
  274. ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
  275. """Build the combined sparse-index tensors for the mixed batch.
  276. Returns ``(compressed_kv_cache, seq_lens, sparse_indices,
  277. sparse_topk_lens)``.
  278. """
  279. num_decodes = swa_metadata.num_decodes
  280. num_prefills = swa_metadata.num_prefills
  281. num_decode_tokens = swa_metadata.num_decode_tokens
  282. num_prefill_tokens = swa_metadata.num_prefill_tokens
  283. num_reqs = num_decodes + num_prefills
  284. num_tokens = num_decode_tokens + num_prefill_tokens
  285. assert swa_metadata.seq_lens is not None
  286. assert swa_metadata.query_start_loc is not None
  287. assert swa_metadata.token_to_req_indices is not None
  288. assert swa_metadata.decode_swa_indices is not None
  289. assert swa_metadata.block_table is not None
  290. decode_swa_indices = swa_metadata.decode_swa_indices.reshape(
  291. num_decode_tokens, swa_metadata.decode_swa_width
  292. )
  293. decode_compressed_topk_lens = None
  294. decode_compressed_indices_are_local = False
  295. decode_is_valid_token = None
  296. if swa_only:
  297. assert self.topk_indices_buffer is not None
  298. compressed_kv_cache = swa_k_cache
  299. decode_compressed_indices = None
  300. prefill_topk_indices = self.topk_indices_buffer[
  301. num_decode_tokens:num_tokens, :0
  302. ]
  303. compressed_block_table = None
  304. compressed_block_size = swa_metadata.block_size
  305. top_k = 0
  306. else:
  307. assert kv_cache is not None
  308. assert attn_metadata is not None
  309. assert self.topk_indices_buffer is not None
  310. assert swa_metadata.is_valid_token is not None
  311. compressed_kv_cache = kv_cache
  312. compressed_block_table = attn_metadata.block_table[:num_reqs]
  313. compressed_block_size = attn_metadata.block_size // self.compress_ratio
  314. # Local indices filled by the index-source layer's indexer.
  315. if num_prefill_tokens > 0:
  316. prefill_topk_indices = self.topk_indices_buffer[
  317. num_decode_tokens:num_tokens
  318. ]
  319. top_k = prefill_topk_indices.shape[-1]
  320. else:
  321. prefill_topk_indices = self.topk_indices_buffer[:0, :0]
  322. top_k = 0
  323. decode_compressed_indices_are_local = True
  324. decode_is_valid_token = swa_metadata.is_valid_token[:num_decode_tokens]
  325. if num_decode_tokens > 0:
  326. decode_compressed_indices = self.topk_indices_buffer[:num_decode_tokens]
  327. else:
  328. # Keep the logical width aligned with the mixed-batch case so
  329. # pure-prefill steps reuse the same Triton specialization.
  330. decode_compressed_indices = prefill_topk_indices[:0]
  331. query_start_loc = swa_metadata.query_start_loc[: num_reqs + 1]
  332. seq_lens = swa_metadata.seq_lens[:num_reqs]
  333. assert seq_lens.dtype == torch.int32
  334. # SWA-only layers all build the same mixed sparse indices, so the first
  335. # one caches them for the step; indexer layers depend on their own topk
  336. # indices and stay uncached.
  337. cached_sparse = (
  338. swa_metadata.flashinfer_sparse_index_cache.get("swa_only")
  339. if swa_only
  340. else None
  341. )
  342. if cached_sparse is None:
  343. swa_block_span = _packed_block_span(swa_k_cache)
  344. compressed_block_span = _packed_block_span(compressed_kv_cache)
  345. sparse_indices, sparse_topk_lens = build_flashinfer_mixed_sparse_indices(
  346. decode_swa_indices,
  347. decode_compressed_indices,
  348. decode_compressed_topk_lens,
  349. prefill_topk_indices[:num_prefill_tokens],
  350. query_start_loc,
  351. seq_lens,
  352. swa_metadata.token_to_req_indices[:num_tokens],
  353. swa_metadata.block_table[:num_reqs],
  354. swa_metadata.block_size,
  355. compressed_block_table,
  356. compressed_block_size,
  357. self.window_size,
  358. self.compress_ratio,
  359. top_k,
  360. decode_compressed_indices_are_local=decode_compressed_indices_are_local,
  361. decode_is_valid_token=decode_is_valid_token,
  362. swa_block_span=swa_block_span,
  363. compressed_block_span=compressed_block_span,
  364. prefill_left_visible=swa_metadata.prefill_left_visible,
  365. prefill_right_visible=swa_metadata.prefill_right_visible,
  366. # getattr for tests that bypass __init__ via object.__new__.
  367. max_image_tokens=getattr(self, "max_image_tokens", 0),
  368. )
  369. if swa_only:
  370. swa_metadata.flashinfer_sparse_index_cache["swa_only"] = (
  371. sparse_indices,
  372. sparse_topk_lens,
  373. )
  374. else:
  375. sparse_indices, sparse_topk_lens = cached_sparse
  376. return compressed_kv_cache, seq_lens, sparse_indices, sparse_topk_lens
  377. def _forward(
  378. self,
  379. q: torch.Tensor,
  380. kv_cache: torch.Tensor | None,
  381. swa_k_cache: torch.Tensor,
  382. swa_metadata: "DeepseekSparseSWAMetadata",
  383. attn_metadata: DeepseekV4FlashMLAMetadata | None,
  384. swa_only: bool,
  385. output: torch.Tensor,
  386. ) -> None:
  387. assert self.kv_cache_torch_dtype in (torch.bfloat16, torch.float8_e4m3fn)
  388. num_decodes = swa_metadata.num_decodes
  389. num_prefills = swa_metadata.num_prefills
  390. num_decode_tokens = swa_metadata.num_decode_tokens
  391. num_prefill_tokens = swa_metadata.num_prefill_tokens
  392. num_reqs = num_decodes + num_prefills
  393. num_tokens = num_decode_tokens + num_prefill_tokens
  394. if num_tokens == 0:
  395. return
  396. (
  397. compressed_kv_cache,
  398. seq_lens,
  399. sparse_indices,
  400. sparse_topk_lens,
  401. ) = self._build_sparse_index_metadata(
  402. kv_cache=kv_cache,
  403. swa_k_cache=swa_k_cache,
  404. swa_metadata=swa_metadata,
  405. attn_metadata=attn_metadata,
  406. swa_only=swa_only,
  407. )
  408. # CUDA graph execution can pad q/output past the scheduled token count;
  409. # restrict to the real tokens (the launcher validates sparse indices).
  410. query = q[:num_tokens]
  411. output = output[:num_tokens]
  412. bmm1_scale: float | torch.Tensor = self.scale
  413. bmm2_scale: float | torch.Tensor = 1.0
  414. if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
  415. assert query.dtype == torch.float8_e4m3fn
  416. bmm1_scale = self._flashinfer_fp8_bmm1_scale
  417. bmm2_scale = self._flashinfer_fp8_bmm2_scale
  418. else:
  419. assert query.dtype == torch.bfloat16
  420. query = query.contiguous()
  421. # The TRTLLM-gen sparse-MLA kernel requires h_q in {64, 128}; zero-pad
  422. # the query heads to the allocated output head count. Padded heads attend
  423. # to the shared KV and are sliced off downstream (output is padded too).
  424. padded_heads = output.shape[1]
  425. if query.shape[1] < padded_heads:
  426. padded_query = query.new_zeros(
  427. (query.shape[0], padded_heads, query.shape[2])
  428. )
  429. padded_query[:, : query.shape[1], :] = query
  430. query = padded_query
  431. workspace = _get_flashinfer_dsv4_workspace(q.device)
  432. query_start_loc = swa_metadata.query_start_loc
  433. query_start_loc_cpu = swa_metadata.query_start_loc_cpu
  434. assert query_start_loc is not None and query_start_loc_cpu is not None
  435. # Keep the TRTLLM-gen decode/prefill split: the launcher is tuned for
  436. # uniform-q batches, and this avoids flattening mixed batches into one call.
  437. if num_decode_tokens > 0:
  438. decode_cu = query_start_loc[: num_decodes + 1]
  439. flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  440. query=query[:num_decode_tokens],
  441. swa_kv_cache=swa_k_cache,
  442. workspace_buffer=workspace,
  443. sparse_indices=sparse_indices[:num_decode_tokens],
  444. compressed_kv_cache=compressed_kv_cache,
  445. sparse_topk_lens=sparse_topk_lens[:num_decode_tokens],
  446. seq_lens=seq_lens[:num_decodes],
  447. out=output[:num_decode_tokens],
  448. bmm1_scale=bmm1_scale,
  449. bmm2_scale=bmm2_scale,
  450. sinks=self.attn_sink,
  451. cum_seq_lens_q=decode_cu,
  452. max_q_len=swa_metadata.max_decode_query_len,
  453. )
  454. if num_prefill_tokens > 0:
  455. # The prefill query view re-anchors at offset 0, so rebase the
  456. # cumulative query offsets to start at 0.
  457. prefill_cu = (
  458. query_start_loc[num_decodes : num_reqs + 1]
  459. - query_start_loc[num_decodes]
  460. )
  461. prefill_cu_cpu = query_start_loc_cpu[num_decodes : num_reqs + 1]
  462. prefill_lens_cpu = prefill_cu_cpu[1:] - prefill_cu_cpu[:-1]
  463. flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  464. query=query[num_decode_tokens:num_tokens],
  465. swa_kv_cache=swa_k_cache,
  466. workspace_buffer=workspace,
  467. sparse_indices=sparse_indices[num_decode_tokens:num_tokens],
  468. compressed_kv_cache=compressed_kv_cache,
  469. sparse_topk_lens=sparse_topk_lens[num_decode_tokens:num_tokens],
  470. seq_lens=seq_lens[num_decodes:num_reqs],
  471. out=output[num_decode_tokens:num_tokens],
  472. bmm1_scale=bmm1_scale,
  473. bmm2_scale=bmm2_scale,
  474. sinks=self.attn_sink,
  475. cum_seq_lens_q=prefill_cu,
  476. max_q_len=int(prefill_lens_cpu.max().item()),
  477. )
  478. class DeepseekV4FlashInferSM120Attention(DeepseekV4Attention):
  479. """DeepSeek V4 sparse MLA attention through FlashInfer's SM120 kernels."""
  480. backend_cls = DeepseekV4FlashInferMLASparseBackend
  481. swa_backend_cls = DeepseekSparseSWAFlashInferBackend
  482. use_fp8_ds_mla_layout: ClassVar[bool] = True
  483. @staticmethod
  484. def _get_workspace(device: torch.device) -> torch.Tensor:
  485. return _get_flashinfer_dsv4_workspace(device)
  486. @staticmethod
  487. def _as_sparse_cache(kv_cache: torch.Tensor) -> torch.Tensor:
  488. if kv_cache.dtype == torch.float8_e4m3fn:
  489. kv_cache = kv_cache.view(torch.uint8)
  490. if kv_cache.dim() == 4:
  491. return kv_cache
  492. return kv_cache.unsqueeze(-2)
  493. @classmethod
  494. def get_padded_num_q_heads(cls, num_heads: int) -> int:
  495. return _pad_to_supported_q_heads(num_heads)
  496. def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
  497. return deep_gemm_fp8_o_proj(
  498. o,
  499. positions,
  500. self.rotary_emb.cos_sin_cache,
  501. self.wo_a,
  502. self.wo_b,
  503. n_groups=self.n_local_groups,
  504. heads_per_group=self.n_local_heads // self.n_local_groups,
  505. nope_dim=self.nope_head_dim,
  506. rope_dim=self.rope_head_dim,
  507. o_lora_rank=self.o_lora_rank,
  508. einsum_recipe=self._einsum_recipe,
  509. tma_aligned_scales=self._tma_aligned_scales,
  510. )
  511. def __init__(self, vllm_config: VllmConfig, *args, **kwargs) -> None:
  512. super().__init__(vllm_config, *args, **kwargs)
  513. from vllm.utils.flashinfer import has_flashinfer_sparse_mla_sm120_config
  514. required_topk = _required_sm120_sparse_topk(vllm_config, self.window_size)
  515. if not has_flashinfer_sparse_mla_sm120_config(self.padded_heads, required_topk):
  516. raise RuntimeError(
  517. "FLASHINFER_MLA_SPARSE_DSV4 on SM120 requires a FlashInfer "
  518. "DSV4 sparse MLA decode specialization for "
  519. f"(num_q_heads={self.padded_heads}, top_k={required_topk}). "
  520. "Install a FlashInfer build containing "
  521. "flashinfer-ai/flashinfer#4380."
  522. )
  523. self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
  524. self._o_proj_block_size
  525. )
  526. # Per-tensor FP8 cache path scales.
  527. if self.kv_cache_torch_dtype != torch.float8_e4m3fn:
  528. return
  529. fp8_q_scale = 1.0
  530. fp8_kv_scale = 1.0
  531. self.register_buffer(
  532. "_flashinfer_fp8_q_scale",
  533. torch.tensor([fp8_q_scale], dtype=torch.float32),
  534. persistent=False,
  535. )
  536. self.register_buffer(
  537. "_flashinfer_fp8_q_scale_inv",
  538. torch.tensor([1.0 / fp8_q_scale], dtype=torch.float32),
  539. persistent=False,
  540. )
  541. self.register_buffer(
  542. "_flashinfer_fp8_kv_scale",
  543. torch.tensor([fp8_kv_scale], dtype=torch.float32),
  544. persistent=False,
  545. )
  546. # FlashInfer expects scalar scale arguments for this path.
  547. self._flashinfer_fp8_bmm1_scale = self.scale * fp8_q_scale * fp8_kv_scale
  548. self._flashinfer_fp8_bmm2_scale = fp8_kv_scale
  549. def _reserve_empty_forward_workspace(self) -> None:
  550. self._get_workspace(
  551. torch.device("cuda", torch.accelerator.current_device_index())
  552. )
  553. def _forward_sparse_impl(
  554. self,
  555. q: torch.Tensor,
  556. output: torch.Tensor,
  557. flashmla_metadata: DeepseekV4FlashMLAMetadata | None,
  558. swa_metadata: "DeepseekSparseSWAMetadata",
  559. self_kv_cache: torch.Tensor | None,
  560. swa_kv_cache: torch.Tensor,
  561. swa_only: bool,
  562. ) -> None:
  563. num_decode_tokens = swa_metadata.num_decode_tokens
  564. if swa_metadata.num_prefills > 0:
  565. self._forward_prefill(
  566. q=q[num_decode_tokens:],
  567. compressed_k_cache=self_kv_cache,
  568. swa_k_cache=swa_kv_cache,
  569. output=output[num_decode_tokens:],
  570. attn_metadata=flashmla_metadata,
  571. swa_metadata=swa_metadata,
  572. )
  573. if swa_metadata.num_decodes > 0:
  574. self._forward_decode(
  575. q=q[:num_decode_tokens],
  576. kv_cache=self_kv_cache,
  577. swa_metadata=swa_metadata,
  578. attn_metadata=flashmla_metadata,
  579. swa_only=swa_only,
  580. output=output[:num_decode_tokens],
  581. )
  582. def forward_mqa(
  583. self,
  584. q: torch.Tensor,
  585. kv: torch.Tensor,
  586. positions: torch.Tensor,
  587. output: torch.Tensor,
  588. ) -> None:
  589. # Output may be padded to backend-supported head counts.
  590. assert output.shape[0] == q.shape[0] and output.shape[-1] == q.shape[-1], (
  591. f"output buffer shape {output.shape} incompatible with q shape {q.shape}"
  592. )
  593. assert output.shape[1] >= q.shape[1], (
  594. f"output heads {output.shape[1]} must be >= q heads {q.shape[1]}"
  595. )
  596. # Per-tensor FP8 q produces a bf16 attention output.
  597. expected_output_dtype = (
  598. torch.bfloat16 if q.dtype == torch.float8_e4m3fn else q.dtype
  599. )
  600. assert output.dtype == expected_output_dtype, (
  601. f"output dtype {output.dtype} must match expected {expected_output_dtype} "
  602. f"for q dtype {q.dtype}"
  603. )
  604. forward_context = get_forward_context()
  605. attn_metadata = forward_context.attn_metadata
  606. if attn_metadata is None:
  607. self._reserve_empty_forward_workspace()
  608. output.zero_()
  609. return
  610. assert isinstance(attn_metadata, dict)
  611. # Compressed-cache metadata lives on the kv-source layer's prefix;
  612. # consumers share that cache and its block table.
  613. flashmla_metadata = cast(
  614. DeepseekV4FlashMLAMetadata | None,
  615. attn_metadata.get(self.compressed_cache_prefix)
  616. if self.compressed_cache_prefix is not None
  617. else None,
  618. )
  619. swa_metadata = cast(
  620. "DeepseekSparseSWAMetadata | None",
  621. attn_metadata.get(self.swa_cache_layer.prefix),
  622. )
  623. assert swa_metadata is not None
  624. swa_only = self.compress_ratio == 0
  625. # SWA-only layers have no compressed KV cache; consumers read the kv
  626. # source's cache.
  627. self_kv_cache = None if swa_only else self._compressed_kv_cache()
  628. swa_kv_cache = self.swa_cache_layer.kv_cache
  629. self._forward_sparse_impl(
  630. q=q,
  631. output=output,
  632. flashmla_metadata=flashmla_metadata,
  633. swa_metadata=swa_metadata,
  634. self_kv_cache=self_kv_cache,
  635. swa_kv_cache=swa_kv_cache,
  636. swa_only=swa_only,
  637. )
  638. def _prepare_query(self, q: torch.Tensor, output: torch.Tensor) -> torch.Tensor:
  639. if self.kv_cache_torch_dtype == torch.float8_e4m3fn:
  640. assert q.dtype == torch.float8_e4m3fn
  641. q = q.to(torch.bfloat16)
  642. else:
  643. assert q.dtype == torch.bfloat16
  644. padded_heads = output.shape[1]
  645. if q.shape[1] < padded_heads:
  646. padded_query = q.new_zeros((q.shape[0], padded_heads, q.shape[2]))
  647. padded_query[:, : q.shape[1], :] = q
  648. q = padded_query
  649. return q.contiguous()
  650. def _forward_decode(
  651. self,
  652. q: torch.Tensor,
  653. kv_cache: torch.Tensor | None,
  654. swa_metadata: "DeepseekSparseSWAMetadata",
  655. attn_metadata: DeepseekV4FlashMLAMetadata | None,
  656. swa_only: bool,
  657. output: torch.Tensor,
  658. ) -> None:
  659. num_decodes = swa_metadata.num_decodes
  660. num_decode_tokens = swa_metadata.num_decode_tokens
  661. extra_sparse_indices = None
  662. extra_sparse_lengths = None
  663. if not swa_only:
  664. if attn_metadata is None:
  665. raise RuntimeError(
  666. "Sparse MLA metadata is required for compressed layers."
  667. )
  668. if swa_metadata.is_valid_token is None:
  669. raise RuntimeError(
  670. "SWA validity metadata is required for compressed layers."
  671. )
  672. if self.topk_indices_buffer is None:
  673. raise RuntimeError(
  674. "Compressed-layer decode requires top-k indices from the indexer."
  675. )
  676. # Local indices filled by the index-source layer's indexer.
  677. is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
  678. block_size = attn_metadata.block_size // self.compress_ratio
  679. global_indices, extra_sparse_lengths = compute_global_topk_indices_and_lens(
  680. self.topk_indices_buffer[:num_decode_tokens],
  681. swa_metadata.token_to_req_indices,
  682. attn_metadata.block_table[:num_decodes],
  683. block_size,
  684. is_valid,
  685. )
  686. extra_sparse_indices = global_indices.view(num_decode_tokens, 1, -1)
  687. swa_indices = swa_metadata.decode_swa_indices
  688. swa_lens = swa_metadata.decode_swa_lens
  689. assert swa_indices is not None
  690. assert swa_lens is not None
  691. q = self._prepare_query(q, output)
  692. swa_cache = self._as_sparse_cache(self.swa_cache_layer.kv_cache)
  693. extra_cache = self._as_sparse_cache(kv_cache) if kv_cache is not None else None
  694. if extra_cache is not None and extra_sparse_indices is None:
  695. raise RuntimeError(
  696. "Compressed sparse MLA decode requires compressed sparse indices."
  697. )
  698. flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  699. query=q,
  700. swa_kv_cache=swa_cache,
  701. workspace_buffer=self._get_workspace(q.device),
  702. sparse_indices=swa_indices,
  703. compressed_kv_cache=extra_cache,
  704. out=output,
  705. bmm1_scale=self.scale,
  706. sinks=self.attn_sink,
  707. kv_layout="NHD",
  708. swa_topk_lens=swa_lens,
  709. extra_sparse_indices=extra_sparse_indices,
  710. extra_sparse_topk_lens=extra_sparse_lengths,
  711. )
  712. def _forward_prefill(
  713. self,
  714. q: torch.Tensor,
  715. compressed_k_cache: torch.Tensor | None,
  716. swa_k_cache: torch.Tensor,
  717. output: torch.Tensor,
  718. attn_metadata: DeepseekV4FlashMLAMetadata | None,
  719. swa_metadata: "DeepseekSparseSWAMetadata",
  720. ) -> None:
  721. swa_only = self.compress_ratio == 0
  722. num_prefills = swa_metadata.num_prefills
  723. num_decodes = swa_metadata.num_decodes
  724. num_decode_tokens = swa_metadata.num_decode_tokens
  725. num_prefill_tokens = swa_metadata.num_prefill_tokens
  726. query_start_loc_cpu = swa_metadata.query_start_loc_cpu
  727. assert query_start_loc_cpu is not None
  728. prefill_token_base = query_start_loc_cpu[num_decodes]
  729. extra_sparse_indices: torch.Tensor | None = None
  730. extra_sparse_lengths: torch.Tensor | None = None
  731. if not swa_only:
  732. if self.topk_indices_buffer is None:
  733. raise RuntimeError(
  734. "Compressed-layer prefill requires top-k indices from the indexer."
  735. )
  736. if attn_metadata is None:
  737. raise RuntimeError("Compressed-layer prefill metadata is missing.")
  738. if swa_metadata.token_to_req_indices is None:
  739. raise RuntimeError(
  740. "Compressed-layer prefill request mapping is missing."
  741. )
  742. if swa_metadata.is_valid_token is None:
  743. raise RuntimeError(
  744. "Compressed-layer prefill validity metadata is missing."
  745. )
  746. # Local indices filled by the index-source layer's indexer.
  747. local_topk_indices = self.topk_indices_buffer[
  748. num_decode_tokens : num_decode_tokens + num_prefill_tokens
  749. ]
  750. prefill_token_slice = slice(
  751. num_decode_tokens, num_decode_tokens + num_prefill_tokens
  752. )
  753. block_size = attn_metadata.block_size // self.compress_ratio
  754. extra_sparse_indices, extra_sparse_lengths = (
  755. compute_global_topk_indices_and_lens(
  756. local_topk_indices,
  757. swa_metadata.token_to_req_indices[prefill_token_slice],
  758. attn_metadata.block_table,
  759. block_size,
  760. swa_metadata.is_valid_token[prefill_token_slice],
  761. )
  762. )
  763. assert swa_metadata.prefill_swa_indices is not None
  764. assert swa_metadata.prefill_swa_lens is not None
  765. q = self._prepare_query(q, output)
  766. swa_kv_paged = self._as_sparse_cache(swa_k_cache)
  767. if swa_only:
  768. extra_kv_paged = None
  769. else:
  770. if compressed_k_cache is None:
  771. raise RuntimeError(
  772. "Compressed sparse MLA layers require their compressed KV cache."
  773. )
  774. extra_kv_paged = self._as_sparse_cache(compressed_k_cache)
  775. num_chunks = (
  776. num_prefills + self.PREFILL_CHUNK_SIZE - 1
  777. ) // self.PREFILL_CHUNK_SIZE
  778. for chunk_idx in range(num_chunks):
  779. chunk_start = chunk_idx * self.PREFILL_CHUNK_SIZE
  780. chunk_end = min(chunk_start + self.PREFILL_CHUNK_SIZE, num_prefills)
  781. query_start = (
  782. query_start_loc_cpu[num_decodes + chunk_start] - prefill_token_base
  783. )
  784. query_end = (
  785. query_start_loc_cpu[num_decodes + chunk_end] - prefill_token_base
  786. )
  787. extra_sparse_indices_chunk = (
  788. extra_sparse_indices[query_start:query_end]
  789. if extra_sparse_indices is not None
  790. else None
  791. )
  792. extra_sparse_lengths_chunk = (
  793. extra_sparse_lengths[query_start:query_end]
  794. if extra_sparse_lengths is not None
  795. else None
  796. )
  797. q_chunk = q[query_start:query_end]
  798. swa_indices_chunk = swa_metadata.prefill_swa_indices[query_start:query_end]
  799. swa_lens_chunk = swa_metadata.prefill_swa_lens[query_start:query_end]
  800. if extra_kv_paged is not None and extra_sparse_indices_chunk is None:
  801. raise RuntimeError(
  802. "Compressed sparse MLA prefill requires compressed sparse indices."
  803. )
  804. flashinfer_trtllm_batch_decode_sparse_mla_dsv4(
  805. query=q_chunk,
  806. swa_kv_cache=swa_kv_paged,
  807. workspace_buffer=self._get_workspace(q.device),
  808. sparse_indices=swa_indices_chunk,
  809. compressed_kv_cache=extra_kv_paged,
  810. out=output[query_start:query_end],
  811. bmm1_scale=self.scale,
  812. sinks=self.attn_sink,
  813. kv_layout="NHD",
  814. swa_topk_lens=swa_lens_chunk,
  815. extra_sparse_indices=extra_sparse_indices_chunk,
  816. extra_sparse_topk_lens=extra_sparse_lengths_chunk,
  817. )