attention.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """
  4. DeepseekV4 MLA Attention Layer
  5. """
  6. from abc import ABC, abstractmethod
  7. from collections.abc import Callable
  8. from functools import cached_property
  9. from typing import TYPE_CHECKING, Any, ClassVar, cast
  10. import regex as re
  11. import torch
  12. import torch.nn as nn
  13. import torch.nn.functional as F
  14. from transformers import DeepseekV2Config, DeepseekV3Config
  15. import vllm.envs as envs
  16. from vllm.compilation.breakable_cudagraph import eager_break_during_capture
  17. from vllm.model_executor.layers.fusion.quant_activation import QuantizedActivation
  18. from vllm.model_executor.layers.linear import (
  19. ColumnParallelLinear,
  20. MergedColumnParallelLinear,
  21. ReplicatedLinear,
  22. RowParallelLinear,
  23. )
  24. from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
  25. from vllm.models.common.ops import fused_q_kv_rmsnorm
  26. from vllm.models.deepseek_v4_1.common.ops import (
  27. MXFP4_BLOCK_SIZE,
  28. fused_indexer_q_rope_quant,
  29. indexer_k_norm_rope_store,
  30. )
  31. if TYPE_CHECKING:
  32. from vllm.v1.attention.backends.mla.sparse_swa import (
  33. DeepseekSparseSWAMetadata,
  34. )
  35. from vllm.config import (
  36. CacheConfig,
  37. VllmConfig,
  38. get_current_vllm_config,
  39. )
  40. from vllm.distributed import get_dcp_group, get_tensor_model_parallel_world_size
  41. from vllm.forward_context import get_forward_context
  42. from vllm.logger import init_logger
  43. from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
  44. from vllm.model_executor.layers.layernorm import RMSNorm
  45. from vllm.model_executor.layers.quantization import QuantizationConfig
  46. from vllm.model_executor.models.utils import extract_layer_index
  47. from vllm.models.deepseek_v4_1.common.rope import build_deepseek_v4_rope
  48. from vllm.models.deepseek_v4_1.compressor import DeepseekCompressor
  49. from vllm.triton_utils import tl, triton
  50. from vllm.utils.multi_stream_utils import (
  51. execute_in_parallel,
  52. maybe_execute_in_parallel,
  53. )
  54. from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
  55. from vllm.v1.attention.backends.mla.indexer import (
  56. DeepseekV4IndexerBackend,
  57. dsa_indexer_uses_fp4,
  58. get_max_prefill_buffer_size,
  59. )
  60. from vllm.v1.attention.backends.mla.sparse_swa import DeepseekV4SWACache
  61. from vllm.v1.kv_cache_interface import (
  62. KVCacheSpec,
  63. MLAAttentionSpec,
  64. get_kv_quant_mode,
  65. )
  66. logger = init_logger(__name__)
  67. def _replace_layer_index(prefix: str, layer_id: int) -> str:
  68. """Swap the layer index inside a ``...layers.<idx>...`` prefix."""
  69. new_prefix, n = re.subn(r"\.layers\.\d+\.", f".layers.{layer_id}.", prefix)
  70. assert n == 1, f"Cannot locate layer index in prefix {prefix}"
  71. return new_prefix
  72. def _indexer_k_cache_head_dim(index_head_dim: int, use_fp4_kv: bool) -> int:
  73. """Per-token byte width of the paged indexer K cache row."""
  74. if use_fp4_kv:
  75. # MXFP4 stores two values per byte plus one UE8M0 byte per 32 values.
  76. # head_dim bytes = 64 packed values + 4 UE8M0 scales = 68.
  77. return index_head_dim // 2 + index_head_dim // MXFP4_BLOCK_SIZE
  78. # NOTE(yifan): FP8 indexer cache uses the same layout as V3.2:
  79. # head_dim bytes = 128 fp8 + 4 fp32 scale = 132.
  80. return index_head_dim + index_head_dim // 128 * 4
  81. @triton.jit
  82. def _fill_short_context_topk_indices(
  83. output,
  84. positions,
  85. TOP_K: tl.constexpr,
  86. COMPRESS_RATIO: tl.constexpr,
  87. PADDED_TOP_K: tl.constexpr,
  88. ):
  89. # small triton kernel that selects every candidate, -1 otherwise
  90. row = tl.program_id(0)
  91. offsets = tl.arange(0, PADDED_TOP_K)
  92. num_compressed = (tl.load(positions + row) + 1) // COMPRESS_RATIO
  93. tl.store(
  94. output + row * TOP_K + offsets,
  95. tl.where(offsets < num_compressed, offsets, -1),
  96. mask=offsets < TOP_K,
  97. )
  98. def _resolve_dsv4_kv_cache_dtype(
  99. use_fp8_ds_mla_layout: bool,
  100. kv_cache_dtype: str,
  101. cache_config: CacheConfig | None,
  102. ) -> tuple[str, torch.dtype]:
  103. """Map ``(layout, --kv-cache-dtype)`` to ``(cache_dtype_str, torch_dtype)``.
  104. Both layouts are paged; they differ in the per-token block format. The
  105. ``fp8_ds_mla`` format is UE8M0 block-scaled fp8 packed as ``uint8`` (the
  106. canonical ``fp8_ds_mla`` string is written back onto ``cache_config`` so the
  107. page-size specs pick the 576B per-token slot). Plain-row backends store each
  108. token's KV row in its element dtype: bf16 or per-tensor FP8 E4M3.
  109. """
  110. if use_fp8_ds_mla_layout:
  111. # fp8_ds_mla block format: UE8M0 block-scaled fp8 packed as uint8.
  112. if kv_cache_dtype == "auto":
  113. kv_cache_dtype = "fp8"
  114. if not kv_cache_dtype.startswith("fp8"):
  115. raise ValueError(
  116. "DeepseekV4 fp8_ds_mla layout only supports fp8 "
  117. f"kv-cache, got {kv_cache_dtype}. Please set "
  118. "`--kv-cache-dtype fp8` or select a backend that supports "
  119. "bfloat16 KV cache."
  120. )
  121. if kv_cache_dtype != "fp8_ds_mla":
  122. if cache_config is not None:
  123. cache_config.cache_dtype = "fp8_ds_mla"
  124. kv_cache_dtype = "fp8_ds_mla"
  125. logger.info_once("Using DeepSeek's fp8_ds_mla KV cache format.")
  126. return kv_cache_dtype, torch.uint8
  127. # Plain bf16 / per-tensor fp8 KV row (FlashInfer).
  128. if kv_cache_dtype.startswith("fp8"):
  129. return kv_cache_dtype, torch.float8_e4m3fn
  130. # auto / bfloat16 -> plain bf16 KV row.
  131. return kv_cache_dtype, torch.bfloat16
  132. class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
  133. """DeepseekV4 MLA attention layer.
  134. The platform-specific sparse-MLA forward (``forward_mqa`` /
  135. ``get_padded_num_q_heads`` / ``_o_proj`` / ``backend_cls``) is provided by a
  136. subclass — ``DeepseekV4FlashMLAAttention`` /
  137. ``DeepseekV4FlashInferSM120Attention`` /
  138. ``DeepseekV4FlashInferMLAAttention`` (CUDA) or
  139. ``DeepseekV41ROCMAiterMLAAttention`` (ROCm) — selected by the platform-specific
  140. deepseek_v4_1 model module. The base is never instantiated directly.
  141. """
  142. # Provided by the platform subclass.
  143. backend_cls: ClassVar[type[AttentionBackend]]
  144. # Backend for the SWA cache layer; None uses the default SWA backend.
  145. swa_backend_cls: ClassVar[type[AttentionBackend] | None] = None
  146. # KV-cache per-token block format (both layouts are paged). True (default)
  147. # = fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8); False = plain
  148. # bf16 / per-tensor fp8 KV row. Backends can override the instance hook when
  149. # a single attention class dispatches across arch-specific layouts.
  150. use_fp8_ds_mla_layout: ClassVar[bool] = True
  151. # Prefill is processed in fixed-size chunks; this bounds the bf16 kv-gather
  152. # workspace allocated in _forward_prefill and is also read by the dummy-run
  153. # path to pre-reserve that workspace.
  154. PREFILL_CHUNK_SIZE: ClassVar[int] = 4
  155. # DCP contract, mirroring AttentionImpl (v1/attention/backend.py): the
  156. # V4.1 layer runs its own forward (no AttentionImpl), so
  157. # check_attention_cp_compatibility reads these from the layer. A platform
  158. # subclass that implements the per-rank partial attention + LSE combine
  159. # sets can_return_lse_for_decode = True (SM120 FlashInfer).
  160. can_return_lse_for_decode: ClassVar[bool] = False
  161. supports_mtp_with_cp_non_trivial_interleave_size: ClassVar[bool] = False
  162. @classmethod
  163. @abstractmethod
  164. def get_padded_num_q_heads(cls, num_heads: int) -> int:
  165. """Q head count the q/output buffers are allocated at.
  166. The layer allocates the q/output buffers at
  167. ``[N, get_padded_num_q_heads(n_local_heads), head_dim]``. Must satisfy
  168. ``result >= num_heads``. Backends with no padding constraint return
  169. ``num_heads``.
  170. """
  171. raise NotImplementedError
  172. @abstractmethod
  173. def forward_mqa(
  174. self,
  175. q: torch.Tensor,
  176. kv: torch.Tensor,
  177. positions: torch.Tensor,
  178. output: torch.Tensor,
  179. ) -> None:
  180. """Platform-specific sparse MLA forward; writes attention into ``output``."""
  181. raise NotImplementedError
  182. @abstractmethod
  183. def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
  184. """Inverse-RoPE + wo_a + wo_b output projection (platform-specific)."""
  185. raise NotImplementedError
  186. def _uses_fp8_ds_mla_layout(self) -> bool:
  187. """Return whether this instance stores fp8 KV in fp8_ds_mla layout."""
  188. return self.use_fp8_ds_mla_layout
  189. def __init__(
  190. self,
  191. vllm_config: VllmConfig,
  192. prefix: str,
  193. topk_indices_buffer: torch.Tensor | None = None,
  194. aux_stream_list: list[torch.cuda.Stream] | None = None,
  195. candidate_block_buffer: torch.Tensor | None = None,
  196. ) -> None:
  197. super().__init__()
  198. config = vllm_config.model_config.hf_config
  199. quant_config = vllm_config.quant_config
  200. cache_config = vllm_config.cache_config
  201. tp_size = get_tensor_model_parallel_world_size()
  202. layer_id = extract_layer_index(prefix)
  203. self.layer_id = layer_id
  204. self.prefix = prefix # Alias for compatibility with compressor
  205. self.hidden_size = config.hidden_size
  206. self.n_heads = config.num_attention_heads
  207. assert self.n_heads % tp_size == 0
  208. self.n_local_heads = self.n_heads // tp_size
  209. # Decode context parallelism: compressed/indexer caches are sharded over
  210. # compressed states across the DCP group (a subset of the TP ranks),
  211. # SWA caches are replicated; the platform subclass gathers queries and
  212. # combines per-rank partial attention (DCP2-PLAN.md).
  213. parallel_config = vllm_config.parallel_config
  214. self.dcp_world_size = parallel_config.decode_context_parallel_size
  215. self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
  216. self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
  217. self.need_to_return_lse_for_decode = (
  218. self.dcp_world_size > 1 and self.can_return_lse_for_decode
  219. )
  220. if self.dcp_world_size > 1 and self.cp_kv_cache_interleave_size != 1:
  221. raise NotImplementedError(
  222. "DeepSeek-V4.1 DCP supports cp_kv_cache_interleave_size=1 only "
  223. f"(got {self.cp_kv_cache_interleave_size})."
  224. )
  225. self.q_lora_rank = config.q_lora_rank
  226. self.o_lora_rank = config.o_lora_rank
  227. self.head_dim = config.head_dim
  228. self.rope_head_dim = config.qk_rope_head_dim
  229. self.nope_head_dim = self.head_dim - self.rope_head_dim
  230. self.n_groups = config.o_groups
  231. self.n_local_groups = self.n_groups // tp_size
  232. self.window_size = config.sliding_window
  233. # Vision variant: image spans are visible bidirectionally, widening
  234. # prefill SWA index rows by up to max_image_tokens columns.
  235. self.max_image_tokens = (
  236. getattr(config, "vision_max_n_token", 0)
  237. if getattr(config, "vision_n_layers", 0) > 0
  238. else 0
  239. )
  240. # ---- v4.1 sparse-attention topology ----
  241. # compress_ratios has one entry per layer (MTP layers included):
  242. # 0 = pure sliding window, 1 = full-length compressed cache,
  243. # 2 = ratio-2 compressed. Compressors and compressed-KV caches live
  244. # only on ``kv_source_layer_ids``; indexers only on
  245. # ``index_source_layer_ids``. Consumers reuse the most recently
  246. # published source below them.
  247. compress_ratios = getattr(config, "compress_ratios", None)
  248. if compress_ratios is not None and layer_id < len(compress_ratios):
  249. self.compress_ratio = int(compress_ratios[layer_id])
  250. else:
  251. # MTP layers past the configured list are pure sliding-window.
  252. self.compress_ratio = 0
  253. if self.compress_ratio not in (0, 1, 2):
  254. raise ValueError(
  255. f"DeepSeek V4.1 layer {layer_id} has compress_ratio="
  256. f"{self.compress_ratio}; only 0 (sliding window), 1 and 2 are "
  257. "supported."
  258. )
  259. self.kv_source_layers = tuple(
  260. getattr(config, "kv_source_layer_ids", None) or ()
  261. )
  262. self.index_source_layers = tuple(
  263. getattr(config, "index_source_layer_ids", None) or ()
  264. )
  265. self.candidate_source_layer = getattr(config, "candidate_source_layer_id", -1)
  266. self.candidate_topk_blocks = getattr(config, "candidate_topk_blocks", 0)
  267. self.candidate_block_size = getattr(config, "candidate_block_size", 0)
  268. is_backbone = layer_id < config.num_hidden_layers
  269. self.is_kv_source = is_backbone and layer_id in self.kv_source_layers
  270. self.is_index_source = is_backbone and layer_id in self.index_source_layers
  271. if self.compress_ratio > 0:
  272. if not self.kv_source_layers or not self.index_source_layers:
  273. raise ValueError(
  274. "DeepSeek V4.1 requires kv_source_layer_ids / "
  275. "index_source_layer_ids in the config for compressed "
  276. f"layers (layer {layer_id} has "
  277. f"compress_ratio={self.compress_ratio})."
  278. )
  279. self.kv_source_layer_id = max(
  280. s for s in self.kv_source_layers if s <= layer_id
  281. )
  282. self.index_source_layer_id = max(
  283. s for s in self.index_source_layers if s <= layer_id
  284. )
  285. else:
  286. self.kv_source_layer_id = None
  287. self.index_source_layer_id = None
  288. self.eps = config.rms_norm_eps
  289. self.scale = self.head_dim**-0.5
  290. # Padded Q head count is dictated by the platform subclass.
  291. self.padded_heads = self.get_padded_num_q_heads(self.n_local_heads)
  292. # Sink padded to the same head count, initialized to -inf (no sink
  293. # effect). Weight loading fills the first n_local_heads slots.
  294. self.attn_sink = nn.Parameter(
  295. torch.full((self.padded_heads,), -float("inf"), dtype=torch.float32),
  296. requires_grad=False,
  297. )
  298. self.fused_wqa_wkv = MergedColumnParallelLinear(
  299. self.hidden_size,
  300. [self.q_lora_rank, self.head_dim],
  301. bias=False,
  302. quant_config=quant_config,
  303. prefix=f"{prefix}.fused_wqa_wkv",
  304. disable_tp=True, # fused ReplicatedLinear
  305. )
  306. self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
  307. self.wq_b = ColumnParallelLinear(
  308. self.q_lora_rank,
  309. self.n_heads * self.head_dim,
  310. bias=False,
  311. quant_config=quant_config,
  312. return_bias=False,
  313. prefix=f"{prefix}.wq_b",
  314. )
  315. self.kv_norm = RMSNorm(self.head_dim, self.eps)
  316. self.wo_a = ColumnParallelLinear(
  317. self.n_heads * self.head_dim // self.n_groups,
  318. self.n_groups * self.o_lora_rank,
  319. bias=False,
  320. quant_config=quant_config,
  321. return_bias=False,
  322. prefix=f"{prefix}.wo_a",
  323. )
  324. self.wo_a.is_bmm = True
  325. self.wo_a.bmm_batch_size = self.n_local_groups
  326. self._o_proj_block_size = (
  327. 32 if getattr(self.wo_a, "weight_block_size", None) == [1, 32] else 128
  328. )
  329. self.wo_b = RowParallelLinear(
  330. self.n_groups * self.o_lora_rank,
  331. self.hidden_size,
  332. bias=False,
  333. quant_config=quant_config,
  334. return_bias=False,
  335. prefix=f"{prefix}.wo_b",
  336. )
  337. # Initialize rotary embedding before the indexer/compressor consume it.
  338. self.rotary_emb = build_deepseek_v4_rope(
  339. config,
  340. head_dim=self.head_dim,
  341. rope_head_dim=self.rope_head_dim,
  342. max_position_embeddings=config.max_position_embeddings,
  343. compress_ratio=self.compress_ratio,
  344. )
  345. self.indexer_rotary_emb = self.rotary_emb
  346. self.topk_indices_buffer = topk_indices_buffer
  347. self.candidate_block_buffer = candidate_block_buffer
  348. # Register with compilation context for metadata lookup. Done before
  349. # indexer/compressor creation so consumers can resolve their source
  350. # layers through it.
  351. compilation_config = vllm_config.compilation_config
  352. if prefix and prefix in compilation_config.static_forward_context:
  353. raise ValueError(f"Duplicate layer name: {prefix}")
  354. if prefix:
  355. compilation_config.static_forward_context[prefix] = self
  356. self.kv_cache = torch.tensor([])
  357. self._static_forward_context = compilation_config.static_forward_context
  358. self.indexer = None
  359. if self.is_index_source:
  360. index_k_cache: DeepseekV4IndexerCache | None
  361. # Index K cache: owned only by kv-source layers (their indexer has
  362. # wk/k_norm); non-owning index sources share the K cache of the
  363. # latest kv source below them (its indexer produces the keys).
  364. if self.is_kv_source:
  365. index_k_cache = DeepseekV4IndexerCache(
  366. head_dim=_indexer_k_cache_head_dim(
  367. config.index_head_dim, dsa_indexer_uses_fp4(vllm_config)
  368. ),
  369. dtype=torch.uint8,
  370. prefix=f"{prefix}.indexer.k_cache",
  371. cache_config=cache_config,
  372. compress_ratio=self.compress_ratio,
  373. )
  374. else:
  375. assert self.kv_source_layer_id is not None
  376. k_cache_prefix = (
  377. f"{_replace_layer_index(prefix, self.kv_source_layer_id)}"
  378. ".indexer.k_cache"
  379. )
  380. index_k_cache = self._static_forward_context.get(k_cache_prefix)
  381. if index_k_cache is None:
  382. raise NotImplementedError(
  383. f"Indexer K cache source {k_cache_prefix} not found on "
  384. "this rank; PP splits inside a v4.1 kv-sharing group "
  385. "are not supported."
  386. )
  387. is_candidate_source = layer_id == self.candidate_source_layer
  388. uses_candidates = 0 <= self.candidate_source_layer < layer_id
  389. self.indexer = DeepseekV4Indexer(
  390. vllm_config,
  391. config=config,
  392. hidden_size=self.hidden_size,
  393. q_lora_rank=self.q_lora_rank,
  394. quant_config=quant_config,
  395. cache_config=cache_config,
  396. topk_indices_buffer=topk_indices_buffer,
  397. compress_ratio=self.compress_ratio,
  398. prefix=f"{prefix}.indexer",
  399. owns_k=self.is_kv_source,
  400. k_cache=index_k_cache,
  401. main_head_dim=self.head_dim,
  402. candidate_block_buffer=(
  403. candidate_block_buffer
  404. if (is_candidate_source or uses_candidates)
  405. else None
  406. ),
  407. candidate_block_size=self.candidate_block_size,
  408. candidate_write=is_candidate_source,
  409. )
  410. self._prepare_and_attn_fn = self._prepare_and_attn
  411. if not vllm_config.use_v2_model_runner:
  412. # MRV1's piecewise capture only tolerates the wide eager region: with
  413. # the narrow one the attention input preparation stays in the captured
  414. # graph and MRV1 produces garbage (#51430).
  415. self._prepare_and_attn_fn = self._prepare_and_attn_eager
  416. # Will be None on ROCm for now.
  417. self.aux_stream_list = aux_stream_list
  418. # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
  419. # [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
  420. # before post-GEMM starts.
  421. self.ln_events = [torch.cuda.Event() for _ in range(4)]
  422. assert cache_config is not None, "DeepseekV4 attention requires cache_config"
  423. # ---- Attention / KV-cache setup ----
  424. self.max_num_batched_tokens = (
  425. vllm_config.scheduler_config.max_num_batched_tokens
  426. )
  427. self.max_model_len = vllm_config.model_config.max_model_len
  428. # Resolve the kv-cache dtype from this backend's block format. The same
  429. # resolution drives the SWA cache tensor dtype below.
  430. self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
  431. self._uses_fp8_ds_mla_layout(), cache_config.cache_dtype, cache_config
  432. )
  433. self.swa_cache_layer = DeepseekV4SWACache(
  434. head_dim=self.head_dim,
  435. window_size=self.window_size,
  436. dtype=self.kv_cache_torch_dtype,
  437. prefix=f"{prefix}.swa_cache",
  438. cache_config=cache_config,
  439. backend_cls=self.swa_backend_cls,
  440. # Tech2Wild/Kai 2026-09-10: page size is a property of the SWA decode
  441. # kernel (FlashInfer SM120 sparse-MLA kernels are 64-token pages).
  442. block_size=getattr(self.swa_backend_cls, "get_swa_block_size", lambda: 32)(),
  443. )
  444. # The attention layer itself was already registered with the
  445. # compilation context above (before indexer/compressor creation).
  446. # Compressors live only on kv-source layers; consumers read the
  447. # source's compressed cache through the forward context.
  448. self.compressor = None
  449. if self.is_kv_source:
  450. self.compressor = DeepseekCompressor(
  451. vllm_config=vllm_config,
  452. compress_ratio=self.compress_ratio,
  453. hidden_size=self.hidden_size,
  454. head_dim=self.head_dim,
  455. rotate=True,
  456. prefix=f"{prefix}.compressor",
  457. k_cache_prefix=self.prefix,
  458. )
  459. # Prefix of the attention layer owning this layer's compressed KV
  460. # cache (self for kv sources).
  461. if self.compress_ratio > 0:
  462. assert self.kv_source_layer_id is not None
  463. self.compressed_cache_prefix: str | None = _replace_layer_index(
  464. prefix, self.kv_source_layer_id
  465. )
  466. if (
  467. not self.is_kv_source
  468. and self.compressed_cache_prefix not in self._static_forward_context
  469. ):
  470. raise NotImplementedError(
  471. f"Compressed-KV source {self.compressed_cache_prefix} not "
  472. "found on this rank; PP splits inside a v4.1 kv-sharing "
  473. "group are not supported."
  474. )
  475. else:
  476. self.compressed_cache_prefix = None
  477. if vllm_config.kernel_config.enable_jit_warmup:
  478. from vllm.v1.attention.backends.mla.sparse_swa import (
  479. _COMPUTE_PREFILL_METADATA_KERNEL,
  480. _COMPUTE_SWA_INDICES_AND_LENS_KERNEL,
  481. )
  482. _COMPUTE_PREFILL_METADATA_KERNEL.register_warmup()
  483. _COMPUTE_SWA_INDICES_AND_LENS_KERNEL.register_warmup(
  484. window_size=self.window_size,
  485. block_size=self.swa_cache_layer.block_size,
  486. max_image_tokens=self.max_image_tokens,
  487. )
  488. if self.compress_ratio > 1:
  489. from vllm.v1.attention.backends.mla.compressor_utils import (
  490. _COMPRESSED_SLOT_MAPPING_KERNEL,
  491. )
  492. _COMPRESSED_SLOT_MAPPING_KERNEL.register_warmup()
  493. if self.indexer is not None:
  494. from vllm.v1.attention.backends.mla.indexer import (
  495. _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
  496. _PREPARE_UNIFORM_DECODE_KERNEL,
  497. )
  498. _PREPARE_UNIFORM_DECODE_KERNEL.register_warmup()
  499. _BUILD_PREFILL_CHUNK_METADATA_KERNEL.register_warmup()
  500. spec_config = vllm_config.speculative_config
  501. if spec_config is not None and spec_config.use_dspark():
  502. from vllm.v1.attention.backends.mla.sparse_swa import (
  503. _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL,
  504. )
  505. _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL.register_warmup(
  506. window_size=self.window_size,
  507. num_speculative_tokens=spec_config.num_speculative_tokens,
  508. block_size=self.swa_cache_layer.block_size,
  509. )
  510. if self.backend_cls.get_name() in (
  511. "FLASHMLA_SPARSE_DSV41",
  512. "ROCM_FLASHMLA_SPARSE_DSV4",
  513. ):
  514. from vllm.models.deepseek_v4_1.common.ops.cache_utils import (
  515. _COMBINE_TOPK_SWA_INDICES_KERNEL,
  516. )
  517. _COMBINE_TOPK_SWA_INDICES_KERNEL.register_warmup()
  518. def forward(
  519. self,
  520. positions: torch.Tensor,
  521. hidden_states: torch.Tensor,
  522. llama_4_scaling: torch.Tensor | None = None,
  523. ) -> torch.Tensor:
  524. # Pre-allocate attention output with FlashMLA-padded head count.
  525. # The op writes into `o_padded`; we slice to n_local_heads after.
  526. num_tokens = hidden_states.shape[0]
  527. o_padded = torch.empty(
  528. (num_tokens, self.padded_heads, self.head_dim),
  529. dtype=hidden_states.dtype,
  530. device=hidden_states.device,
  531. )
  532. # Keep the attention input preparation in the captured graph. Only the
  533. # sparse indexer and MLA attention run in the eager break below.
  534. qr_kv, kv_score, indexer_weights = self._run_parallel_input_projections(
  535. hidden_states
  536. )
  537. qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
  538. self._prepare_and_attn_fn(
  539. hidden_states,
  540. qr,
  541. kv,
  542. qr_scale,
  543. kv_score,
  544. indexer_weights,
  545. positions,
  546. o_padded,
  547. )
  548. o = o_padded[:, : self.n_local_heads, :]
  549. # Inverse-RoPE + wo_a + wo_b output projection (platform-specific).
  550. return self._o_proj(o, positions)
  551. @cached_property
  552. def _can_fuse_query_quant(self) -> bool:
  553. from vllm.models.deepseek_v4_1.common.ops.query_quant import (
  554. can_fuse_query_quant,
  555. )
  556. linears = [self.wq_b]
  557. if self.indexer is not None:
  558. linears.append(self.indexer.wq_b)
  559. return can_fuse_query_quant(linears)
  560. def _split_qkv_and_norm(
  561. self, qr_kv: torch.Tensor
  562. ) -> tuple[torch.Tensor | QuantizedActivation, torch.Tensor | None, torch.Tensor]:
  563. """Split the fused q-lora / kv projection and RMSNorm both halves.
  564. Compatible MXFP8 projections share the quantized Q and scales;
  565. other projection backends consume the normalized Q directly.
  566. """
  567. qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
  568. if self.q_lora_rank % 32 == 0 and self._can_fuse_query_quant:
  569. from vllm.models.deepseek_v4_1.common.ops.query_quant import (
  570. fused_q_kv_rmsnorm_quant,
  571. )
  572. qr_quant, kv = fused_q_kv_rmsnorm_quant(
  573. qr,
  574. kv,
  575. self.q_norm.weight.data,
  576. self.kv_norm.weight.data,
  577. self.eps,
  578. )
  579. return qr_quant, None, kv
  580. qr, kv = fused_q_kv_rmsnorm(
  581. qr,
  582. kv,
  583. self.q_norm.weight.data,
  584. self.kv_norm.weight.data,
  585. self.eps,
  586. )
  587. return qr, None, kv
  588. @eager_break_during_capture
  589. def _prepare_and_attn_eager(
  590. self,
  591. hidden_states: torch.Tensor,
  592. qr: torch.Tensor | QuantizedActivation,
  593. kv: torch.Tensor,
  594. qr_scale: torch.Tensor | None,
  595. kv_score: torch.Tensor,
  596. indexer_weights: torch.Tensor,
  597. positions: torch.Tensor,
  598. o_padded: torch.Tensor,
  599. ) -> None:
  600. """Wide eager region: the whole of ``_prepare_and_attn`` runs eagerly.
  601. The nested ``_sparse_indexer_and_attn`` break runs inline, since
  602. ``add_eager`` clears ``_capturing`` before invoking this.
  603. """
  604. self._prepare_and_attn(
  605. hidden_states,
  606. qr,
  607. kv,
  608. qr_scale,
  609. kv_score,
  610. indexer_weights,
  611. positions,
  612. o_padded,
  613. )
  614. def _prepare_and_attn(
  615. self,
  616. hidden_states: torch.Tensor,
  617. qr: torch.Tensor | QuantizedActivation,
  618. kv: torch.Tensor,
  619. qr_scale: torch.Tensor | None,
  620. kv_score: torch.Tensor,
  621. indexer_weights: torch.Tensor,
  622. positions: torch.Tensor,
  623. o_padded: torch.Tensor,
  624. ) -> None:
  625. """Attention input preparation followed by the sparse indexer and MLA.
  626. Only the latter runs in the eager break.
  627. Q/SWA preparation overlaps state saving and compression. Once the
  628. latent is ready, main-cache insertion overlaps indexer preparation;
  629. both cache writes finish before sparse attention reads either cache.
  630. """
  631. attn_metadata = get_forward_context().attn_metadata
  632. indexer = self.indexer
  633. compressor = self.compressor
  634. aux_streams = self.aux_stream_list
  635. def project_query_and_cache_kv() -> torch.Tensor:
  636. q = self._wq_b_proj(qr, qr_scale).view(
  637. -1, self.n_local_heads, self.head_dim
  638. )
  639. return self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
  640. index_q: torch.Tensor | None = None
  641. index_q_scale: torch.Tensor | None = None
  642. index_weights_out: torch.Tensor | None = None
  643. latent: torch.Tensor | None = None
  644. aux_stream = aux_streams[0] if aux_streams is not None else None
  645. if compressor is not None:
  646. # Q projection / KV insertion on the default stream overlaps the
  647. # compressor on aux stream 0 (sequential on ROCm).
  648. q, latent = maybe_execute_in_parallel(
  649. project_query_and_cache_kv,
  650. lambda: compressor(kv_score, positions),
  651. self.ln_events[0],
  652. self.ln_events[1],
  653. aux_stream,
  654. )
  655. else:
  656. q = project_query_and_cache_kv()
  657. def prepare_indexer():
  658. if indexer is None:
  659. return None, None, None
  660. return indexer(
  661. qr,
  662. latent,
  663. indexer_weights,
  664. positions,
  665. self.indexer_rotary_emb,
  666. qr_scale,
  667. )
  668. if compressor is not None:
  669. indexer_result, _ = maybe_execute_in_parallel(
  670. prepare_indexer,
  671. lambda: compressor.insert_cache(latent, positions, self.rotary_emb),
  672. self.ln_events[0],
  673. self.ln_events[1],
  674. aux_stream,
  675. )
  676. else:
  677. indexer_result = prepare_indexer()
  678. index_q, index_q_scale, index_weights_out = indexer_result
  679. self._sparse_indexer_and_attn(
  680. hidden_states,
  681. index_q,
  682. index_q_scale,
  683. index_weights_out,
  684. q,
  685. kv,
  686. positions,
  687. o_padded,
  688. )
  689. def _fused_wqa_wkv_gemm(self, hidden_states: torch.Tensor) -> torch.Tensor:
  690. # Override point: the ROCm layer preshuffles this weight in place, so
  691. # it cannot go through fused_wqa_wkv directly.
  692. # MergedColumnParallelLinear returns (output, bias); bias is None.
  693. qr_kv, _ = self.fused_wqa_wkv(hidden_states)
  694. return qr_kv
  695. def _wq_b_proj(
  696. self,
  697. qr: torch.Tensor | QuantizedActivation,
  698. qr_scale: torch.Tensor | None = None,
  699. ) -> torch.Tensor:
  700. """Project normalized Q, bypassing quantization when already fused."""
  701. assert qr_scale is None, "ROCm-only path"
  702. return self.wq_b(qr)
  703. def _run_parallel_input_projections(
  704. self, hidden_states: torch.Tensor
  705. ) -> tuple[
  706. torch.Tensor,
  707. torch.Tensor | None,
  708. torch.Tensor | None,
  709. ]:
  710. aux_streams = self.aux_stream_list
  711. if aux_streams is not None:
  712. aux_streams = aux_streams[:2]
  713. # fused_wqa_wkv (heaviest) on default; the two lighter input GEMMs on
  714. # aux streams 0/1 when their owning module exists. ln_events[0] is the
  715. # fan-out start event; ln_events[1..2] are per-aux done events. The
  716. # v4.1 indexer derives K from the kv-source compressor's latent, so
  717. # unlike v4.0 there is no indexer K GEMM over hidden_states here.
  718. aux_fns: list[Callable[[], Any] | None] = [None, None]
  719. if self.compressor is not None:
  720. # Local ref so the closure keeps a non-None type for mypy.
  721. compressor = self.compressor
  722. def compressor_kv_score() -> torch.Tensor:
  723. return torch.mm(
  724. hidden_states,
  725. compressor.fused_wkv_wgate.weight.T,
  726. out_dtype=torch.float32,
  727. )
  728. aux_fns[0] = compressor_kv_score
  729. if self.indexer is not None:
  730. indexer = self.indexer
  731. def indexer_weights_proj() -> torch.Tensor:
  732. # ReplicatedLinear returns (output, bias); bias is None.
  733. weights, _ = indexer.weights_proj(hidden_states)
  734. return weights
  735. aux_fns[1] = indexer_weights_proj
  736. qr_kv, (kv_score, indexer_weights) = execute_in_parallel(
  737. lambda: self._fused_wqa_wkv_gemm(hidden_states),
  738. aux_fns,
  739. self.ln_events[0],
  740. self.ln_events[1:3],
  741. aux_streams,
  742. enable=hidden_states.shape[0]
  743. <= envs.VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD,
  744. )
  745. return qr_kv, kv_score, indexer_weights
  746. @eager_break_during_capture
  747. def _sparse_indexer_and_attn(
  748. self,
  749. hidden_states: torch.Tensor,
  750. index_q: torch.Tensor | None,
  751. index_q_scale: torch.Tensor | None,
  752. index_weights: torch.Tensor | None,
  753. q: torch.Tensor,
  754. kv: torch.Tensor,
  755. positions: torch.Tensor,
  756. out: torch.Tensor,
  757. ) -> None:
  758. if self.indexer is not None and index_q is not None:
  759. assert index_weights is not None
  760. q_quant = (index_q, index_q_scale) if index_q_scale is not None else index_q
  761. self.indexer.indexer_op(
  762. hidden_states,
  763. q_quant,
  764. None,
  765. index_weights,
  766. )
  767. # MLA attention writes into the pre-allocated `out` buffer
  768. # ([num_tokens, padded_heads, head_dim]).
  769. self.forward_mqa(q, kv, positions, out)
  770. def _fused_qnorm_rope_kv_insert(
  771. self,
  772. q: torch.Tensor,
  773. kv: torch.Tensor,
  774. positions: torch.Tensor,
  775. attn_metadata: (
  776. dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None
  777. ),
  778. ) -> torch.Tensor:
  779. if not isinstance(attn_metadata, dict):
  780. # Profile run: kernel doesn't fire; produce a padded tensor so
  781. # downstream FlashMLA gets the right shape.
  782. if self.n_local_heads < self.padded_heads:
  783. return F.pad(
  784. q,
  785. (0, 0, 0, self.padded_heads - self.n_local_heads),
  786. value=0.0,
  787. )
  788. return q
  789. swa_metadata = cast(
  790. "DeepseekSparseSWAMetadata | None",
  791. attn_metadata.get(self.swa_cache_layer.prefix),
  792. )
  793. assert swa_metadata is not None
  794. swa_kv_cache = self.swa_cache_layer.kv_cache
  795. # The fused insert ops require int64 position_ids; the runner's positions
  796. # buffer is already int64, so no cast is needed.
  797. assert positions.dtype == torch.int64
  798. cos_sin_cache = self.rotary_emb.cos_sin_cache
  799. cache_dtype = swa_kv_cache.dtype
  800. # kv is unchanged; attention reads kv solely via swa_kv_cache.
  801. if cache_dtype == torch.uint8:
  802. # fp8_ds_mla UE8M0 paged path. Horizontally fused:
  803. # Q side: GPT-J RoPE, zero-filling the padding head slots; the
  804. # kernel allocates and returns the padded q tensor.
  805. # KV side: GPT-J RoPE + UE8M0 FP8 quant + paged cache insert.
  806. swa_kv_cache_2d = swa_kv_cache.view(swa_kv_cache.shape[0], -1)
  807. return torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
  808. q,
  809. kv,
  810. swa_kv_cache_2d,
  811. swa_metadata.slot_mapping,
  812. positions,
  813. cos_sin_cache,
  814. self.padded_heads,
  815. self.eps,
  816. swa_metadata.block_size,
  817. False,
  818. )
  819. # Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
  820. # row in its element dtype (no Q padding). bf16 rewrites q in place;
  821. # per-tensor fp8 writes a separately-allocated fp8 q and quantizes the
  822. # KV row.
  823. block_size = swa_metadata.block_size
  824. assert swa_kv_cache.shape[1:] == (block_size, self.head_dim)
  825. swa_kv_cache_3d = swa_kv_cache
  826. if cache_dtype == torch.bfloat16:
  827. torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert(
  828. q,
  829. kv,
  830. swa_kv_cache_3d,
  831. swa_metadata.slot_mapping,
  832. positions,
  833. cos_sin_cache,
  834. self.eps,
  835. block_size,
  836. False,
  837. )
  838. return q
  839. # per-tensor fp8 (torch.float8_e4m3fn)
  840. q_fp8 = torch.empty_like(q, dtype=torch.float8_e4m3fn)
  841. torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_fp8_insert(
  842. q,
  843. kv,
  844. q_fp8,
  845. swa_kv_cache_3d,
  846. swa_metadata.slot_mapping,
  847. positions,
  848. cos_sin_cache,
  849. self._flashinfer_fp8_kv_scale,
  850. self._flashinfer_fp8_q_scale_inv,
  851. self.eps,
  852. block_size,
  853. False,
  854. )
  855. return q_fp8
  856. def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
  857. # [B, H=1, N, C] -> [B, N, C]
  858. self.kv_cache = kv_cache.squeeze(1)
  859. def get_attn_backend(self) -> type[AttentionBackend]:
  860. return self.backend_cls
  861. def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
  862. # Only kv-source layers own a compressed-KV cache; consumers read the
  863. # source's cache through the forward context, and cr==0 layers are
  864. # pure SWA. The SWA cache is allocated separately as
  865. # DeepseekV4SWACache.
  866. if not self.is_kv_source:
  867. return None
  868. # fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B
  869. # alignment; plain bf16 / per-tensor fp8 rows use natural element-size
  870. # pages.
  871. uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla"
  872. # Tech2Wild/Kai 2026-09-10: SM120 FlashInfer sparse-MLA kernels are
  873. # instantiated for 64-STATE compressed pages, so a kv-source layer's page
  874. # is 64 * compress_ratio tokens there (ratio 1 -> 64, ratio 2 -> 128).
  875. _hook = getattr(self.backend_cls, "get_compressed_block_size", None)
  876. _bs = _hook(self.compress_ratio) if _hook is not None else None
  877. return MLAAttentionSpec(
  878. block_size=_bs or vllm_config.cache_config.block_size,
  879. num_kv_heads=1,
  880. head_size=self.head_dim,
  881. dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
  882. tokens_per_state=self.compress_ratio,
  883. cache_dtype_str=self.kv_cache_dtype,
  884. alignment=576 if uses_fp8_ds_mla_layout else 512,
  885. model_version="deepseek_v4",
  886. kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
  887. # DeepseekV4: 448B NoPE + 128B RoPE + 8B fp8 scale = 584B per token;
  888. # head_size stays semantic (512).
  889. state_content_bytes=584 if uses_fp8_ds_mla_layout else None,
  890. )
  891. def _compressed_kv_cache(self) -> torch.Tensor:
  892. """The compressed-KV cache tensor of this layer's kv source (own
  893. cache for kv-source layers)."""
  894. if self.is_kv_source:
  895. return self.kv_cache
  896. assert self.compressed_cache_prefix is not None
  897. source = self._static_forward_context[self.compressed_cache_prefix]
  898. return source.kv_cache
  899. def _dsv41_indexer_sm12x() -> bool:
  900. from vllm.platforms import current_platform
  901. return current_platform.is_cuda() and current_platform.is_device_capability_family(120)
  902. class DeepseekV4IndexerSM12xBackend(DeepseekV4IndexerBackend):
  903. """Tech2Wild 2026-09-10: SM12x indexer pages hold 64 states (64 tokens at
  904. compress ratio 1, 128 at ratio 2), so both sizes are kernel block sizes."""
  905. @staticmethod
  906. def get_supported_kernel_block_sizes() -> list[int]:
  907. return [128, 64]
  908. class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
  909. def __init__(
  910. self,
  911. head_dim: int,
  912. dtype: torch.dtype,
  913. prefix: str,
  914. cache_config: CacheConfig,
  915. compress_ratio: int = 1,
  916. ):
  917. super().__init__()
  918. self.kv_cache = torch.tensor([])
  919. self.head_dim = head_dim
  920. self.prefix = prefix
  921. self.cache_config = cache_config
  922. self.dtype = dtype
  923. self.compress_ratio = compress_ratio
  924. compilation_config = get_current_vllm_config().compilation_config
  925. if prefix in compilation_config.static_forward_context:
  926. raise ValueError(f"Duplicate layer name: {prefix}")
  927. compilation_config.static_forward_context[prefix] = self
  928. def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
  929. # [B, H=1, N, C] -> [B, N, C]
  930. self.kv_cache = kv_cache.squeeze(1)
  931. def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
  932. # head_dim already carries the fp8 scale padding
  933. # tokens_per_state=1 for V3.2, >1 for DeepseekV4; same cache layout.
  934. uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
  935. # Tech2Wild 2026-09-10 (SM12x): DeepGEMM's paged MQA logits (indexer decode)
  936. # takes 32 or 64 states per block, and the indexer metadata builder passes
  937. # this spec's num_states, so pages hold 64 states = 64 * compress_ratio tokens.
  938. block_size = self.cache_config.block_size
  939. if _dsv41_indexer_sm12x():
  940. block_size = 64 * max(1, self.compress_ratio)
  941. return MLAAttentionSpec(
  942. block_size=block_size,
  943. num_kv_heads=1,
  944. head_size=self.head_dim,
  945. dtype=self.dtype,
  946. tokens_per_state=self.compress_ratio,
  947. # 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
  948. alignment=576 if uses_fp8_ds_mla_layout else 512,
  949. )
  950. def forward(self): ...
  951. def get_attn_backend(self) -> type[AttentionBackend]:
  952. if _dsv41_indexer_sm12x():
  953. return DeepseekV4IndexerSM12xBackend
  954. return DeepseekV4IndexerBackend
  955. class DeepseekV4Indexer(nn.Module):
  956. """DeepSeek V4.1 sparse-attention indexer.
  957. Exists only on ``index_source_layer_ids``; consumers reuse the topk
  958. indices it publishes into the shared ``topk_indices_buffer``. Unlike v4.0
  959. the index key is derived from the kv-source layer's compressor latent
  960. (``k = k_norm(wk(latent))``, ``owns_k``) instead of an indexer-local
  961. compressor over hidden states, so there is no hidden-state K GEMM here.
  962. Non-owning index sources share the kv source's paged K cache.
  963. Two-level candidate filtering: the indexer at ``candidate_source_layer_id``
  964. additionally publishes the top ``candidate_topk_blocks`` blocks of
  965. ``candidate_block_size`` compressed positions (``candidate_write``);
  966. later indexers mask their scores to those blocks before their own top-k.
  967. """
  968. def __init__(
  969. self,
  970. vllm_config: VllmConfig,
  971. config: DeepseekV2Config | DeepseekV3Config,
  972. hidden_size: int,
  973. q_lora_rank: int,
  974. quant_config: QuantizationConfig | None,
  975. cache_config: CacheConfig | None,
  976. topk_indices_buffer: torch.Tensor | None,
  977. compress_ratio: int,
  978. prefix: str,
  979. *,
  980. owns_k: bool,
  981. k_cache: DeepseekV4IndexerCache,
  982. main_head_dim: int,
  983. candidate_block_buffer: torch.Tensor | None = None,
  984. candidate_block_size: int = 0,
  985. candidate_write: bool = False,
  986. ):
  987. super().__init__()
  988. self.vllm_config = vllm_config
  989. self.config = config
  990. self.quant_config = quant_config
  991. self.topk_tokens = config.index_topk
  992. self.n_head = config.index_n_heads # 32
  993. self.head_dim = config.index_head_dim # 128
  994. self.rope_dim = config.qk_rope_head_dim # 64
  995. self.q_lora_rank = q_lora_rank # 1280
  996. self.compress_ratio = compress_ratio
  997. self.owns_k = owns_k
  998. self.use_fp4_kv = dsa_indexer_uses_fp4(vllm_config)
  999. logger.info_once(
  1000. "Using %s indexer cache for Lightning Indexer.",
  1001. "MXFP4" if self.use_fp4_kv else "FP8",
  1002. )
  1003. # no tensor parallel, just replicated
  1004. self.wq_b = ReplicatedLinear(
  1005. self.q_lora_rank,
  1006. self.head_dim * self.n_head,
  1007. bias=False,
  1008. quant_config=quant_config,
  1009. prefix=f"{prefix}.wq_b",
  1010. )
  1011. self.weights_proj = ReplicatedLinear(
  1012. hidden_size,
  1013. self.n_head,
  1014. bias=False,
  1015. quant_config=None,
  1016. prefix=f"{prefix}.weights_proj",
  1017. )
  1018. self.softmax_scale = self.head_dim**-0.5
  1019. self.scale_fmt = "ue8m0"
  1020. self.quant_block_size = 128 # TODO: get from config
  1021. self.topk_indices_buffer = topk_indices_buffer
  1022. self.max_model_len = (
  1023. vllm_config.model_config.max_model_len // self.compress_ratio
  1024. )
  1025. self.prefix = prefix
  1026. self.max_total_seq_len = (
  1027. get_max_prefill_buffer_size(vllm_config) // self.compress_ratio
  1028. )
  1029. assert cache_config is not None, "Deepseek V4 indexer requires cache_config"
  1030. if owns_k:
  1031. # wk maps the main compressor's (pre-RoPE) latent to index keys.
  1032. # The checkpoint stores it in bf16 with no quantization scales.
  1033. self.wk = ReplicatedLinear(
  1034. main_head_dim,
  1035. self.head_dim,
  1036. bias=False,
  1037. quant_config=None,
  1038. prefix=f"{prefix}.wk",
  1039. )
  1040. self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
  1041. self.k_cache = k_cache
  1042. self.indexer_op = SparseAttnIndexer(
  1043. self.k_cache,
  1044. self.quant_block_size,
  1045. self.scale_fmt,
  1046. self.topk_tokens,
  1047. self.head_dim,
  1048. self.max_model_len,
  1049. self.max_total_seq_len,
  1050. self.topk_indices_buffer,
  1051. skip_k_cache_insert=True,
  1052. use_fp4_cache=self.use_fp4_kv,
  1053. compress_ratio=self.compress_ratio,
  1054. candidate_blocks=candidate_block_buffer,
  1055. candidate_block_size=candidate_block_size,
  1056. candidate_write=candidate_write,
  1057. )
  1058. def _produce_k(
  1059. self,
  1060. latent: torch.Tensor | None,
  1061. positions: torch.Tensor,
  1062. rotary_emb: nn.Module,
  1063. ) -> None:
  1064. """Turn the compressor's pre-RoPE latent into paged index keys.
  1065. ``k_norm(wk(latent))`` at group-boundary tokens, RoPE'd at the group
  1066. position and MXFP4/FP8-quantized into the indexer K cache.
  1067. """
  1068. attn_metadata = get_forward_context().attn_metadata
  1069. if not isinstance(attn_metadata, dict) or latent is None:
  1070. # Profile run: the indexer K cache is not bound yet and the
  1071. # compressor skipped its latent output.
  1072. return
  1073. assert self.owns_k
  1074. indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
  1075. # ReplicatedLinear returns (output, bias); bias is None. Rows at
  1076. # non-boundary tokens hold garbage latent and are skipped by the
  1077. # store kernel.
  1078. k_pre, _ = self.wk(latent)
  1079. indexer_k_norm_rope_store(
  1080. k_pre,
  1081. positions,
  1082. rotary_emb.cos_sin_cache,
  1083. self.k_norm.weight,
  1084. self.k_norm.variance_epsilon,
  1085. self.k_cache.kv_cache,
  1086. indexer_metadata.slot_mapping,
  1087. self.compress_ratio,
  1088. self.use_fp4_kv,
  1089. )
  1090. def forward(
  1091. self,
  1092. qr: torch.Tensor | QuantizedActivation,
  1093. latent: torch.Tensor | None,
  1094. indexer_weights: torch.Tensor,
  1095. positions: torch.Tensor,
  1096. rotary_emb: nn.Module,
  1097. qr_scale: torch.Tensor | None = None,
  1098. ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
  1099. attn_metadata = get_forward_context().attn_metadata
  1100. if isinstance(attn_metadata, dict):
  1101. indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
  1102. if (
  1103. indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens
  1104. and not torch.cuda.is_current_stream_capturing()
  1105. ):
  1106. # candidates num smaller than topk, every candidate is selected
  1107. # but we still need to build k cache
  1108. if self.owns_k:
  1109. self._produce_k(latent, positions, rotary_emb)
  1110. assert self.topk_indices_buffer is not None
  1111. num_tokens = (
  1112. indexer_metadata.num_decode_tokens
  1113. + indexer_metadata.num_prefill_tokens
  1114. )
  1115. if num_tokens > 0:
  1116. _fill_short_context_topk_indices[(num_tokens,)](
  1117. self.topk_indices_buffer,
  1118. positions,
  1119. TOP_K=self.topk_tokens,
  1120. COMPRESS_RATIO=self.compress_ratio,
  1121. PADDED_TOP_K=triton.next_power_of_2(self.topk_tokens),
  1122. num_warps=8,
  1123. )
  1124. return None, None, None
  1125. if self.owns_k:
  1126. # K write must land before indexer_op reads the cache
  1127. # (skip_k_cache_insert=True).
  1128. self._produce_k(latent, positions, rotary_emb)
  1129. q = self._wq_b_proj(qr, qr_scale)
  1130. q = q.view(-1, self.n_head, self.head_dim)
  1131. q_quant, weights = fused_indexer_q_rope_quant(
  1132. positions,
  1133. q,
  1134. rotary_emb.cos_sin_cache,
  1135. indexer_weights,
  1136. self.softmax_scale,
  1137. self.n_head**-0.5,
  1138. use_fp4=self.use_fp4_kv,
  1139. )
  1140. if isinstance(q_quant, tuple):
  1141. q, q_scale = q_quant
  1142. else:
  1143. q, q_scale = q_quant, None
  1144. return q, q_scale, weights
  1145. def _wq_b_proj(
  1146. self,
  1147. qr: torch.Tensor | QuantizedActivation,
  1148. qr_scale: torch.Tensor | None = None,
  1149. ) -> torch.Tensor:
  1150. """Project normalized Q with the main attention's shared quantization."""
  1151. assert qr_scale is None, "ROCm-only path"
  1152. # ReplicatedLinear returns (output, bias); bias is None.
  1153. q, _ = self.wq_b(qr)
  1154. return q