attention.py 49 KB

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