sparse_mla.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """DeepSeek-V4.1 FlashMLA sparse backend, metadata, and metadata builders."""
  4. from dataclasses import dataclass
  5. from typing import Any, ClassVar
  6. import torch
  7. from vllm.config import VllmConfig
  8. from vllm.config.cache import CacheDType
  9. from vllm.platforms import current_platform
  10. from vllm.platforms.interface import DeviceCapability
  11. from vllm.v1.attention.backend import (
  12. AttentionBackend,
  13. AttentionCGSupport,
  14. AttentionMetadata,
  15. AttentionMetadataBuilder,
  16. CommonAttentionMetadata,
  17. MultipleOf,
  18. )
  19. from vllm.v1.attention.backends.mla.compressor_utils import get_compressed_slot_mapping
  20. from vllm.v1.attention.backends.utils import get_dcp_local_seq_lens
  21. from vllm.v1.attention.backends.mla.sparse_swa import (
  22. _LAYER_TYPE_C1A,
  23. _LAYER_TYPE_C2A,
  24. _LAYER_TYPE_SWAONLY,
  25. DeepseekSparseSWAMetadataBuilder,
  26. )
  27. from vllm.logger import init_logger
  28. from vllm.v1.kv_cache_interface import AttentionSpec
  29. logger = init_logger(__name__)
  30. # v4.1 per-layer compress ratios: 0 = pure sliding window, 1 = full-length
  31. # compressed cache, 2 = ratio-2 compressed cache. Ratio-1 and ratio-2 layers
  32. # both attend over indexer topk indices into a shared compressed cache but
  33. # differ in compressed page block size (block_size // ratio), so each needs its
  34. # own FlashMLA tile-scheduler plan.
  35. _V41_LAYER_TYPES: dict[int, str] = {
  36. 0: _LAYER_TYPE_SWAONLY,
  37. 1: _LAYER_TYPE_C1A,
  38. 2: _LAYER_TYPE_C2A,
  39. }
  40. def deepseek_v41_layer_type(compress_ratio: int) -> str:
  41. layer_type = _V41_LAYER_TYPES.get(compress_ratio)
  42. if layer_type is None:
  43. raise ValueError(
  44. f"Unsupported DeepSeek V4.1 compress_ratio={compress_ratio}; "
  45. "expected 0, 1, or 2."
  46. )
  47. return layer_type
  48. class DeepseekV41SparseSWAMetadataBuilder(DeepseekSparseSWAMetadataBuilder):
  49. """SWA metadata builder base for v4.1.
  50. The shared builder classifies decode layer types by the v4.0 ratios
  51. (1 = SWA-only, 4, 128). v4.1 uses 0 / 1 / 2, so recompute the set of
  52. tile-scheduler plans from the v4.1 topology.
  53. """
  54. supports_non_causal_multi_token_dcp: ClassVar[bool] = True
  55. def __init__(self, *args, **kwargs):
  56. super().__init__(*args, **kwargs)
  57. compress_ratios = getattr(
  58. self.vllm_config.model_config.hf_config, "compress_ratios", None
  59. ) or [0]
  60. self._layer_types = {
  61. deepseek_v41_layer_type(int(ratio)) for ratio in compress_ratios
  62. }
  63. class DeepseekV4SparseMLABackend(AttentionBackend):
  64. """DeepSeek-V4.1 sparse-MLA backend base.
  65. Subclasses ``AttentionBackend`` directly (not the V3.2
  66. ``FlashMLASparseBackend``): DeepSeek-V4.1 runs its own attention layer
  67. (``DeepseekV4Attention``), so it does not reuse the V3.2 builder or impl, and
  68. only needs to declare its own metadata builder, KV-cache layout, and the
  69. sparse-MLA capability flags.
  70. """
  71. supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
  72. supported_kv_cache_dtypes: ClassVar[list[CacheDType]] = [
  73. "auto",
  74. "fp8_ds_mla",
  75. "fp8", # alias for fp8_ds_mla
  76. ]
  77. @staticmethod
  78. def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
  79. return [64 if current_platform.is_device_capability_family(90) else 128]
  80. @staticmethod
  81. def get_builder_cls() -> type["DeepseekV4SparseMLAMetadataBuilder"]:
  82. return DeepseekV4SparseMLAMetadataBuilder
  83. @staticmethod
  84. def get_impl_cls() -> type[Any]:
  85. # DeepSeek-V4.1 runs its attention through ``DeepseekV4Attention.forward``,
  86. # not the generic ``Attention``/``MLAAttention`` layer, so the backend's
  87. # impl class is never instantiated.
  88. raise NotImplementedError(
  89. "DeepseekV4SparseMLABackend has no separate impl class; DeepSeek-V4.1 "
  90. "attention runs through DeepseekV4Attention."
  91. )
  92. @classmethod
  93. def get_supported_head_sizes(cls) -> list[int]:
  94. # DeepSeek V4 layout: 448 NoPE + 64 RoPE = 512.
  95. return [512]
  96. @classmethod
  97. def is_mla(cls) -> bool:
  98. return True
  99. @classmethod
  100. def is_sparse(cls) -> bool:
  101. return True
  102. @classmethod
  103. def supports_sink(cls) -> bool:
  104. return True
  105. @classmethod
  106. def supports_compute_capability(cls, capability: DeviceCapability) -> bool:
  107. return capability.major in [9, 10]
  108. @dataclass
  109. class DeepseekV4FlashMLAMetadata(AttentionMetadata):
  110. num_reqs: int
  111. max_query_len: int
  112. max_seq_len: int
  113. num_actual_tokens: int # Number of tokens excluding padding.
  114. query_start_loc: torch.Tensor
  115. slot_mapping: torch.Tensor
  116. block_table: torch.Tensor
  117. req_id_per_token: torch.Tensor
  118. block_size: int
  119. topk_tokens: int
  120. # DCP: the compressed cache is sharded over STATES with this interleave;
  121. # block_size stays the per-rank kernel page (tokens). seq_lens are the
  122. # global lengths (tokens); dcp_local_seq_lens this rank's share of the
  123. # compressed length (states), None at DCP1.
  124. cp_kv_cache_interleave_size: int = 1
  125. seq_lens: torch.Tensor | None = None
  126. dcp_local_seq_lens: torch.Tensor | None = None
  127. class DeepseekV4SparseMLAMetadataBuilder(
  128. AttentionMetadataBuilder[DeepseekV4FlashMLAMetadata]
  129. ):
  130. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
  131. # The SM120 backend gathers queries / combines per token, so DSpark's
  132. # non-causal multi-token decode is supported under DCP (the draft layers
  133. # themselves are SWA-only and bypass the DCP path).
  134. supports_non_causal_multi_token_dcp: ClassVar[bool] = True
  135. def __init__(
  136. self,
  137. kv_cache_spec: AttentionSpec,
  138. layer_names: list[str],
  139. vllm_config: VllmConfig,
  140. device: torch.device,
  141. ) -> None:
  142. super().__init__(kv_cache_spec, layer_names, vllm_config, device)
  143. self.model_config = vllm_config.model_config
  144. # DCP geometry (self.dcp_world_size / dcp_rank come from the base
  145. # builder: process DCP for this sharded group). The V4.1 SM120 backend
  146. # gathers queries and combines LSEs per token, so multi-token (DSpark)
  147. # decodes stay decodes under DCP.
  148. self.cp_kv_cache_interleave_size = (
  149. vllm_config.parallel_config.cp_kv_cache_interleave_size
  150. )
  151. if self.dcp_world_size > 1 and self.cp_kv_cache_interleave_size != 1:
  152. raise NotImplementedError(
  153. "DeepSeek-V4.1 DCP supports cp_kv_cache_interleave_size=1 only"
  154. )
  155. # Classify single-token queries (plus num_speculative_tokens via
  156. # supports_spec_as_decode=True) as decodes; longer queries go to prefill.
  157. self._init_reorder_batch_threshold(
  158. 1, supports_spec_as_decode=True, supports_dcp_with_varlen=True
  159. )
  160. self.topk_tokens = self.model_config.hf_config.index_topk
  161. max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
  162. self.req_id_per_token_buffer = torch.empty(
  163. (max_num_batched_tokens,), dtype=torch.int32, device=device
  164. )
  165. assert isinstance(self.kv_cache_spec.tokens_per_state, int)
  166. self.compress_ratio = self.kv_cache_spec.tokens_per_state
  167. # Only kv-source layers own a compressed cache, and v4.1 compresses at
  168. # ratio 1 or 2.
  169. if self.compress_ratio not in (1, 2):
  170. raise ValueError(
  171. "DeepSeek V4.1 compressed-KV caches use compress_ratio 1 or 2; "
  172. f"got {self.compress_ratio}."
  173. )
  174. if self.dcp_world_size > 1:
  175. # Per-rank page geometry the state-space sharding relies on: the
  176. # builder must see the kernel page (block_size tokens = num_states
  177. # states per rank), never the manager block (x dcp).
  178. assert self.kv_cache_spec.block_size % self.compress_ratio == 0
  179. logger.info_once(
  180. "DeepSeek-V4.1 compressed KV under DCP%d: ratio %d, %d tokens "
  181. "(%d states) per page per rank, interleave %d",
  182. self.dcp_world_size,
  183. self.compress_ratio,
  184. self.kv_cache_spec.block_size,
  185. int(self.kv_cache_spec.num_states),
  186. self.cp_kv_cache_interleave_size,
  187. )
  188. # Pre-allocate compressed slot mapping buffer for CUDA graph address
  189. # stability when compress_ratio > 1.
  190. if self.compress_ratio > 1:
  191. self.compressed_slot_mapping_buffer = torch.empty(
  192. max_num_batched_tokens, dtype=torch.int64, device=device
  193. )
  194. def build(
  195. self,
  196. common_prefix_len: int,
  197. common_attn_metadata: CommonAttentionMetadata,
  198. fast_build: bool = False,
  199. ) -> DeepseekV4FlashMLAMetadata:
  200. cm = common_attn_metadata
  201. req_id_per_token = cm.token_to_req_indices(self.req_id_per_token_buffer)
  202. slot_mapping = cm.slot_mapping
  203. if self.compress_ratio > 1:
  204. slot_mapping = get_compressed_slot_mapping(
  205. cm.num_actual_tokens,
  206. cm.query_start_loc,
  207. cm.seq_lens,
  208. cm.block_table_tensor.clamp_(min=0),
  209. int(self.kv_cache_spec.num_states),
  210. self.compress_ratio,
  211. out=self.compressed_slot_mapping_buffer,
  212. dcp_world_size=self.dcp_world_size,
  213. dcp_rank=self.dcp_rank,
  214. cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
  215. )
  216. dcp_local_seq_lens = None
  217. if self.dcp_world_size > 1:
  218. dcp_local_seq_lens = get_dcp_local_seq_lens(
  219. cm.seq_lens // self.compress_ratio,
  220. self.dcp_world_size,
  221. self.dcp_rank,
  222. self.cp_kv_cache_interleave_size,
  223. )
  224. return DeepseekV4FlashMLAMetadata(
  225. num_reqs=cm.num_reqs,
  226. max_query_len=cm.max_query_len,
  227. max_seq_len=cm.max_seq_len,
  228. num_actual_tokens=cm.num_actual_tokens,
  229. query_start_loc=cm.query_start_loc,
  230. slot_mapping=slot_mapping,
  231. block_table=cm.block_table_tensor,
  232. req_id_per_token=req_id_per_token,
  233. block_size=self.kv_cache_spec.block_size,
  234. topk_tokens=self.topk_tokens,
  235. cp_kv_cache_interleave_size=self.cp_kv_cache_interleave_size,
  236. seq_lens=cm.seq_lens,
  237. dcp_local_seq_lens=dcp_local_seq_lens,
  238. )
  239. class DeepseekV4FlashMLAMetadataBuilder(DeepseekV4SparseMLAMetadataBuilder):
  240. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
  241. class DeepseekV4FlashMLABackend(DeepseekV4SparseMLABackend):
  242. @staticmethod
  243. def get_name() -> str:
  244. return "FLASHMLA_SPARSE_DSV41"
  245. @staticmethod
  246. def get_builder_cls() -> type[DeepseekV4FlashMLAMetadataBuilder]:
  247. return DeepseekV4FlashMLAMetadataBuilder