main
py 1,494 lines 60 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """DeepseekV4 MLA Attention Layer"""
4
5 import math
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
11 import regex as re
12 import torch
13 import torch.nn as nn
14 import torch.nn.functional as F
15 from transformers import DeepseekV2Config, DeepseekV3Config
16
17 import vllm.envs as envs
18 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
19 from vllm.model_executor.layers.fusion.quant_activation import QuantizedActivation
20 from vllm.model_executor.layers.linear import (
21 ColumnParallelLinear,
22 MergedColumnParallelLinear,
23 ReplicatedLinear,
24 RowParallelLinear,
25 )
26 from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
27 from vllm.model_executor.layers.sparse_mqa_indexer import SparseMQAIndexer
28 from vllm.models.common.ops import fused_q_kv_rmsnorm
29 from vllm.models.common.ops.sequence_parallel import sp_reduce_scatter
30 from vllm.models.deepseek_v41.common.ops import (
31 MXFP4_BLOCK_SIZE,
32 fused_indexer_q_rope_quant,
33 indexer_k_norm_rope_store,
34 )
35
36 if TYPE_CHECKING:
37 from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import GemmRsAr
38 from vllm.model_executor.layers.rocm_paged_mxfp4_indexer import (
39 RocmSparseMQAIndexer,
40 )
41 from vllm.v1.attention.backends.mla.sparse_swa import (
42 DeepseekSparseSWAMetadata,
43 )
44
45 from vllm.config import (
46 CacheConfig,
47 VllmConfig,
48 get_current_vllm_config,
49 )
50 from vllm.config.cache import CacheDType
51 from vllm.distributed import get_tensor_model_parallel_world_size
52 from vllm.forward_context import get_forward_context
53 from vllm.logger import init_logger
54 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
55 from vllm.model_executor.layers.layernorm import RMSNorm
56 from vllm.model_executor.layers.quantization import QuantizationConfig
57 from vllm.model_executor.models.utils import extract_layer_index
58 from vllm.models.deepseek_v41.common.rope import build_deepseek_v4_rope
59 from vllm.models.deepseek_v41.compressor import DeepseekCompressor
60 from vllm.platforms import current_platform
61 from vllm.triton_utils import tl, triton
62 from vllm.utils.multi_stream_utils import (
63 execute_in_parallel,
64 maybe_execute_in_parallel,
65 )
66 from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
67 from vllm.v1.attention.backends.mla.indexer import (
68 DeepseekV41IndexerBackend,
69 dsa_indexer_uses_fp4,
70 get_max_prefill_buffer_size,
71 )
72 from vllm.v1.attention.backends.mla.sparse_indexer import (
73 DeepseekV41SparseIndexerBackend,
74 )
75 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekV4SWACache
76 from vllm.v1.kv_cache_interface import (
77 KVCacheSpec,
78 MLAAttentionSpec,
79 get_kv_quant_mode,
80 )
81
82 logger = init_logger(__name__)
83
84
85 def _replace_layer_index(prefix: str, layer_id: int) -> str:
86 """Swap the layer index inside a ``...layers.<idx>...`` prefix."""
87 new_prefix, n = re.subn(r"\.layers\.\d+\.", f".layers.{layer_id}.", prefix)
88 assert n == 1, f"Cannot locate layer index in prefix {prefix}"
89 return new_prefix
90
91
92 def _indexer_k_cache_head_dim(index_head_dim: int, use_fp4_kv: bool) -> int:
93 """Per-token byte width of the paged indexer K cache row."""
94 if use_fp4_kv:
95 # MXFP4 stores two values per byte plus one UE8M0 byte per 32 values.
96 # head_dim bytes = 64 packed values + 4 UE8M0 scales = 68.
97 return index_head_dim // 2 + index_head_dim // MXFP4_BLOCK_SIZE
98 # NOTE(yifan): FP8 indexer cache uses the same layout as V3.2:
99 # head_dim bytes = 128 fp8 + 4 fp32 scale = 132.
100 return index_head_dim + index_head_dim // 128 * 4
101
102
103 @triton.jit
104 def _fill_short_context_topk_indices(
105 output,
106 positions,
107 TOP_K: tl.constexpr,
108 COMPRESS_RATIO: tl.constexpr,
109 PADDED_TOP_K: tl.constexpr,
110 ):
111 # small triton kernel that selects every candidate, -1 otherwise
112 row = tl.program_id(0)
113 offsets = tl.arange(0, PADDED_TOP_K)
114 num_compressed = (tl.load(positions + row) + 1) // COMPRESS_RATIO
115 tl.store(
116 output + row * TOP_K + offsets,
117 tl.where(offsets < num_compressed, offsets, -1),
118 mask=offsets < TOP_K,
119 )
120
121
122 # Which packed fp8_ds_mla record a V4.1 layer writes. FlashMLA decodes
123 # DeepSeek's V4.1 record -- all 512 dims (RoPE included) as fp8 e4m3 with one
124 # UE8M0 scale per 32 dims, 512 data bytes + 16 scale bytes per token, pages
125 # rounded to the kernel's 512 B TMA stride -- only in its SM100 sparse-decode
126 # kernels. Every other arch keeps the V4 record: 448 fp8 NoPE + 64 bf16 RoPE
127 # plus 7 UE8M0 scales of 64 dims and a pad byte (584 B, 576 B pages).
128 def _use_v41_mxfp8_kv_record() -> bool:
129 return current_platform.is_device_capability_family(100)
130
131
132 def _resolve_dsv4_kv_cache_dtype(
133 use_fp8_ds_mla_layout: bool,
134 kv_cache_dtype: CacheDType,
135 cache_config: CacheConfig | None,
136 packed_kv_cache_dtype: CacheDType = "fp8_ds_mla",
137 ) -> tuple[CacheDType, torch.dtype]:
138 """Map ``(layout, --kv-cache-dtype)`` to ``(cache_dtype_str, torch_dtype)``.
139
140 Both layouts are paged; they differ in the per-token block format. The
141 packed formats are ``uint8``-backed: ``fp8_ds_mla`` is UE8M0 block-scaled
142 fp8 throughout, ``nvfp4_ds_mla`` keeps that sliding-window record and
143 stores the compressed cache as NVFP4. An unspecific ``--kv-cache-dtype``
144 (``auto`` / ``fp8``) resolves to ``packed_kv_cache_dtype``, the record this
145 layer's kernel prefers, and the canonical string is written back onto
146 ``cache_config`` so the page-size specs pick the right per-token slot.
147 Plain-row backends store each token's KV row in its element dtype: bf16 or
148 per-tensor FP8 E4M3.
149 """
150 if use_fp8_ds_mla_layout:
151 if kv_cache_dtype in ("auto", "fp8"):
152 kv_cache_dtype = packed_kv_cache_dtype
153 elif not kv_cache_dtype.endswith("_ds_mla"):
154 raise ValueError(
155 "DeepseekV4 packed KV layouts only support fp8 kv-cache, got "
156 f"{kv_cache_dtype}. Please set `--kv-cache-dtype fp8` or "
157 "select a backend that supports bfloat16 KV cache."
158 )
159 if cache_config is not None and cache_config.cache_dtype != kv_cache_dtype:
160 cache_config.cache_dtype = kv_cache_dtype
161 logger.info_once("Using DeepSeek's %s KV cache format.", kv_cache_dtype)
162 return kv_cache_dtype, torch.uint8
163 if kv_cache_dtype.endswith("_ds_mla"):
164 raise ValueError(
165 f"{kv_cache_dtype} is a packed FlashMLA DeepSeek V4.1 KV cache "
166 "format; the selected backend stores plain KV rows."
167 )
168
169 # Plain bf16 / per-tensor fp8 KV row (FlashInfer).
170 if kv_cache_dtype.startswith("fp8"):
171 return kv_cache_dtype, torch.float8_e4m3fn
172 # auto / bfloat16 -> plain bf16 KV row.
173 return kv_cache_dtype, torch.bfloat16
174
175
176 class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
177 """DeepseekV4 MLA attention layer.
178
179 The platform-specific sparse-MLA forward (``forward_mqa`` /
180 ``get_padded_num_q_heads`` / ``_o_proj`` / ``backend_cls``) is provided by a
181 subclass — ``DeepseekV4FlashMLAAttention`` /
182 ``DeepseekV4FlashInferSM120Attention`` /
183 ``DeepseekV4FlashInferMLAAttention`` (CUDA) or
184 ``DeepseekV41ROCMAiterMLAAttention`` (ROCm) — selected by the platform-specific
185 deepseek_v41 model module. The base is never instantiated directly.
186 """
187
188 # Provided by the platform subclass.
189 backend_cls: ClassVar[type[AttentionBackend]]
190 # Backend for the SWA cache layer; None uses the default SWA backend.
191 swa_backend_cls: ClassVar[type[AttentionBackend] | None] = None
192 # KV-cache per-token block format (both layouts are paged). True (default)
193 # = fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8); False = plain
194 # bf16 / per-tensor fp8 KV row. Backends can override the instance hook when
195 # a single attention class dispatches across arch-specific layouts.
196 use_fp8_ds_mla_layout: ClassVar[bool] = True
197 # Prefill is processed in fixed-size chunks; this bounds the bf16 kv-gather
198 # workspace allocated in _forward_prefill and is also read by the dummy-run
199 # path to pre-reserve that workspace.
200 PREFILL_CHUNK_SIZE: ClassVar[int] = 4
201
202 # ---- attention-interface contract, declared by the platform subclass ----
203
204 @property
205 def accepts_unnormed_unroped_query(self) -> bool:
206 """Whether ``forward_mqa``'s ``q`` is the raw ``wq_b`` output.
207
208 True when the attention kernel applies the Q norm and RoPE itself, and
209 reads Q in its own chunk-interleaved layout, so the layer only
210 zero-pads Q to ``padded_heads`` and inserts KV before calling it.
211 """
212 return False
213
214 @property
215 def packed_kv_cache_dtype(self) -> CacheDType:
216 """The packed KV record this layer's kernel prefers.
217
218 What an unspecific ``--kv-cache-dtype`` (``auto`` / ``fp8``) resolves
219 to. Mega attention overrides it: its kernel is the one that can read
220 an NVFP4 compressed cache.
221 """
222 return "fp8_ds_mla"
223
224 @classmethod
225 @abstractmethod
226 def get_padded_num_q_heads(cls, num_heads: int) -> int:
227 """Q head count the q/output buffers are allocated at.
228
229 The layer allocates the q/output buffers at
230 ``[N, get_padded_num_q_heads(n_local_heads), head_dim]``. Must satisfy
231 ``result >= num_heads``. Backends with no padding constraint return
232 ``num_heads``.
233 """
234 raise NotImplementedError
235
236 @abstractmethod
237 def forward_mqa(
238 self,
239 q: torch.Tensor,
240 kv: torch.Tensor,
241 positions: torch.Tensor,
242 output: torch.Tensor,
243 ) -> None:
244 """Platform-specific sparse MLA forward; writes attention into ``output``."""
245 raise NotImplementedError
246
247 @abstractmethod
248 def _o_proj(
249 self,
250 attn_out: "torch.Tensor | QuantizedActivation",
251 positions: torch.Tensor,
252 ) -> torch.Tensor:
253 """Project whatever ``_alloc_attn_out`` produced through wo_a and wo_b.
254
255 Takes the buffer whole, so each layer owns the shape it allocated: the
256 bf16 layers slice off their padding heads and apply the inverse RoPE,
257 while a layer whose attention kernel already did the inverse RoPE and
258 the FP8 cast gets a QuantizedActivation and has only wo_a and wo_b
259 left.
260 """
261 raise NotImplementedError
262
263 def _uses_fp8_ds_mla_layout(self) -> bool:
264 """Return whether this instance stores fp8 KV in fp8_ds_mla layout."""
265 return self.use_fp8_ds_mla_layout
266
267 def _indexer_cls(
268 self, k_cache: "DeepseekV4IndexerCache | None"
269 ) -> type["DeepseekV4Indexer"]:
270 """The indexer for this layer's index K cache. A platform subclass
271 returns one that writes and scores its own cache layout."""
272 return DeepseekV4Indexer
273
274 def __init__(
275 self,
276 vllm_config: VllmConfig,
277 prefix: str,
278 topk_indices_buffer: torch.Tensor | None = None,
279 aux_stream_list: list[torch.cuda.Stream] | None = None,
280 candidate_block_buffer: torch.Tensor | None = None,
281 ) -> None:
282 super().__init__()
283 config = vllm_config.model_config.hf_config
284 quant_config = vllm_config.quant_config
285 cache_config = vllm_config.cache_config
286 tp_size = get_tensor_model_parallel_world_size()
287 layer_id = extract_layer_index(prefix)
288 self.layer_id = layer_id
289
290 self.prefix = prefix # Alias for compatibility with compressor
291 self.hidden_size = config.hidden_size
292 self.n_heads = config.num_attention_heads
293 assert self.n_heads % tp_size == 0
294 self.n_local_heads = self.n_heads // tp_size
295 self.q_lora_rank = config.q_lora_rank
296 self.o_lora_rank = config.o_lora_rank
297 self.head_dim = config.head_dim
298 self.rope_head_dim = config.qk_rope_head_dim
299 self.nope_head_dim = self.head_dim - self.rope_head_dim
300 self.n_groups = config.o_groups
301 self.n_local_groups = self.n_groups // tp_size
302 self.window_size = config.sliding_window
303 # ---- v4.1 sparse-attention topology ----
304 # compress_ratios has one entry per layer (MTP layers included):
305 # 0 = pure sliding window, 1 = full-length compressed cache,
306 # 2 = ratio-2 compressed. Compressors and compressed-KV caches live
307 # only on ``kv_source_layer_ids``; indexers only on
308 # ``index_source_layer_ids``. Consumers reuse the most recently
309 # published source below them.
310 compress_ratios = getattr(config, "compress_ratios", None)
311 if compress_ratios is not None and layer_id < len(compress_ratios):
312 self.compress_ratio = int(compress_ratios[layer_id])
313 else:
314 # MTP layers past the configured list are pure sliding-window.
315 self.compress_ratio = 0
316 if self.compress_ratio not in (0, 1, 2):
317 raise ValueError(
318 f"DeepSeek V4.1 layer {layer_id} has compress_ratio="
319 f"{self.compress_ratio}; only 0 (sliding window), 1 and 2 are "
320 "supported."
321 )
322 self.kv_source_layers = tuple(
323 getattr(config, "kv_source_layer_ids", None) or ()
324 )
325 self.index_source_layers = tuple(
326 getattr(config, "index_source_layer_ids", None) or ()
327 )
328 self.candidate_source_layer = getattr(config, "candidate_source_layer_id", -1)
329 self.candidate_topk_blocks = getattr(config, "candidate_topk_blocks", 0)
330 self.candidate_block_size = getattr(config, "candidate_block_size", 0)
331
332 is_backbone = layer_id < config.num_hidden_layers
333 self.is_kv_source = is_backbone and layer_id in self.kv_source_layers
334 self.is_index_source = is_backbone and layer_id in self.index_source_layers
335 if self.compress_ratio > 0:
336 if not self.kv_source_layers or not self.index_source_layers:
337 raise ValueError(
338 "DeepSeek V4.1 requires kv_source_layer_ids / "
339 "index_source_layer_ids in the config for compressed "
340 f"layers (layer {layer_id} has "
341 f"compress_ratio={self.compress_ratio})."
342 )
343 self.kv_source_layer_id = max(
344 s for s in self.kv_source_layers if s <= layer_id
345 )
346 self.index_source_layer_id = max(
347 s for s in self.index_source_layers if s <= layer_id
348 )
349 else:
350 self.kv_source_layer_id = None
351 self.index_source_layer_id = None
352 self.eps = config.rms_norm_eps
353 self.scale = self.head_dim**-0.5
354
355 # Padded Q head count is dictated by the platform subclass.
356 self.padded_heads = self.get_padded_num_q_heads(self.n_local_heads)
357 # Sink padded to the same head count, initialized to -inf (no sink
358 # effect). Weight loading fills the first n_local_heads slots.
359 self.attn_sink = nn.Parameter(
360 torch.full((self.padded_heads,), -float("inf"), dtype=torch.float32),
361 requires_grad=False,
362 )
363
364 self.fused_wqa_wkv = MergedColumnParallelLinear(
365 self.hidden_size,
366 [self.q_lora_rank, self.head_dim],
367 bias=False,
368 quant_config=quant_config,
369 prefix=f"{prefix}.fused_wqa_wkv",
370 disable_tp=True, # fused ReplicatedLinear
371 )
372 self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
373 self.wq_b = ColumnParallelLinear(
374 self.q_lora_rank,
375 self.n_heads * self.head_dim,
376 bias=False,
377 quant_config=quant_config,
378 return_bias=False,
379 prefix=f"{prefix}.wq_b",
380 )
381
382 self.kv_norm = RMSNorm(self.head_dim, self.eps)
383 self.wo_a = ColumnParallelLinear(
384 self.n_heads * self.head_dim // self.n_groups,
385 self.n_groups * self.o_lora_rank,
386 bias=False,
387 quant_config=quant_config,
388 return_bias=False,
389 prefix=f"{prefix}.wo_a",
390 )
391 self.wo_a.is_bmm = True
392 self.wo_a.bmm_batch_size = self.n_local_groups
393 self._o_proj_block_size = (
394 32 if getattr(self.wo_a, "weight_block_size", None) == [1, 32] else 128
395 )
396 self.wo_b = RowParallelLinear(
397 self.n_groups * self.o_lora_rank,
398 self.hidden_size,
399 bias=False,
400 quant_config=quant_config,
401 return_bias=False,
402 prefix=f"{prefix}.wo_b",
403 )
404 # Set by ``bind_gemm_rs`` when the decoder layer runs sequence
405 # parallel and the fused GEMM + reduce-scatter kernel accepts wo_b.
406 self.gemm_rs: GemmRsAr | None = None
407
408 # Initialize rotary embedding before the indexer/compressor consume it.
409 self.rotary_emb = build_deepseek_v4_rope(
410 config,
411 head_dim=self.head_dim,
412 rope_head_dim=self.rope_head_dim,
413 max_position_embeddings=config.max_position_embeddings,
414 compress_ratio=self.compress_ratio,
415 )
416 self.indexer_rotary_emb = self.rotary_emb
417 self.topk_indices_buffer = topk_indices_buffer
418 self.candidate_block_buffer = candidate_block_buffer
419
420 # Register with compilation context for metadata lookup. Done before
421 # indexer/compressor creation so consumers can resolve their source
422 # layers through it.
423 compilation_config = vllm_config.compilation_config
424 if prefix and prefix in compilation_config.static_forward_context:
425 raise ValueError(f"Duplicate layer name: {prefix}")
426 if prefix:
427 compilation_config.static_forward_context[prefix] = self
428 self.kv_cache = torch.tensor([])
429 self._static_forward_context = compilation_config.static_forward_context
430
431 self.indexer = None
432 if self.is_index_source:
433 index_k_cache: DeepseekV4IndexerCache | None
434 # Index K cache: owned only by kv-source layers (their indexer has
435 # wk/k_norm); non-owning index sources share the K cache of the
436 # latest kv source below them (its indexer produces the keys).
437 if self.is_kv_source:
438 index_k_cache = DeepseekV4IndexerCache(
439 head_dim=_indexer_k_cache_head_dim(
440 config.index_head_dim, dsa_indexer_uses_fp4(vllm_config)
441 ),
442 dtype=torch.uint8,
443 prefix=f"{prefix}.indexer.k_cache",
444 cache_config=cache_config,
445 compress_ratio=self.compress_ratio,
446 )
447 else:
448 assert self.kv_source_layer_id is not None
449 k_cache_prefix = (
450 f"{_replace_layer_index(prefix, self.kv_source_layer_id)}"
451 ".indexer.k_cache"
452 )
453 index_k_cache = self._static_forward_context.get(k_cache_prefix)
454 if index_k_cache is None:
455 raise NotImplementedError(
456 f"Indexer K cache source {k_cache_prefix} not found on "
457 "this rank; PP splits inside a v4.1 kv-sharing group "
458 "are not supported."
459 )
460 is_candidate_source = layer_id == self.candidate_source_layer
461 uses_candidates = 0 <= self.candidate_source_layer < layer_id
462 self.indexer = self._indexer_cls(index_k_cache)(
463 vllm_config,
464 config=config,
465 hidden_size=self.hidden_size,
466 q_lora_rank=self.q_lora_rank,
467 quant_config=quant_config,
468 cache_config=cache_config,
469 topk_indices_buffer=topk_indices_buffer,
470 compress_ratio=self.compress_ratio,
471 prefix=f"{prefix}.indexer",
472 owns_k=self.is_kv_source,
473 k_cache=index_k_cache,
474 main_head_dim=self.head_dim,
475 candidate_block_buffer=(
476 candidate_block_buffer
477 if (is_candidate_source or uses_candidates)
478 else None
479 ),
480 candidate_block_size=self.candidate_block_size,
481 candidate_write=is_candidate_source,
482 )
483
484 self._prepare_and_attn_fn = self._prepare_and_attn
485 if not vllm_config.use_v2_model_runner:
486 # MRV1's piecewise capture only tolerates the wide eager region: with
487 # the narrow one the attention input preparation stays in the captured
488 # graph and MRV1 produces garbage (#51430).
489 self._prepare_and_attn_fn = self._prepare_and_attn_eager
490
491 self.aux_stream_list = aux_stream_list
492 # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
493 # [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
494 # before post-GEMM starts.
495 self.ln_events = [torch.cuda.Event() for _ in range(4)]
496
497 assert cache_config is not None, "DeepseekV4 attention requires cache_config"
498 # ---- Attention / KV-cache setup ----
499 self.max_num_batched_tokens = (
500 vllm_config.scheduler_config.max_num_batched_tokens
501 )
502 self.max_model_len = vllm_config.model_config.max_model_len
503
504 # Resolve the kv-cache dtype from this backend's block format. The same
505 # resolution drives the SWA cache tensor dtype below.
506 self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
507 self._uses_fp8_ds_mla_layout(),
508 cache_config.cache_dtype,
509 cache_config,
510 self.packed_kv_cache_dtype,
511 )
512 self.kv_mxfp8 = _use_v41_mxfp8_kv_record()
513 self.swa_bytes_per_token = 528 if self.kv_mxfp8 else 584
514 # nvfp4_ds_mla keeps the MXFP8 sliding-window record and stores the
515 # compressed cache as NVFP4 (256 B of e2m1 pairs + 32 e4m3 scales).
516 self.compressed_bytes_per_token = (
517 288 if self.kv_cache_dtype == "nvfp4_ds_mla" else self.swa_bytes_per_token
518 )
519 # One alignment for every page in the block: the block stride is their
520 # sum, and 512 satisfies both TMA strides in play (512 for the V4.1
521 # fp8 record, 256 for NVFP4).
522 self.kv_page_alignment = 512 if self.kv_mxfp8 else 576
523 if self.kv_cache_dtype == "nvfp4_ds_mla" and not self.kv_mxfp8:
524 raise ValueError(
525 "nvfp4_ds_mla needs the V4.1 KV records, which FlashMLA "
526 "decodes only on SM100."
527 )
528
529 swa_bounded_replay = cache_config.swa_bounded_replay
530 if swa_bounded_replay and not vllm_config.use_v2_model_runner:
531 logger.warning_once(
532 "SWA bounded replay needs model runner V2 (only it skips the "
533 "paged-KV writes of replayed tokens); the sliding-window cache "
534 "takes part in prefix caching instead."
535 )
536 swa_bounded_replay = False
537 if (
538 swa_bounded_replay
539 and vllm_config.parallel_config.prefill_context_parallel_size > 1
540 ):
541 logger.warning_once(
542 "SWA bounded replay is off under prefill context parallelism "
543 "(the replayed tokens' slot padding knows the rank-local batch "
544 "only); the sliding-window cache takes part in prefix caching "
545 "instead."
546 )
547 swa_bounded_replay = False
548 if swa_bounded_replay and current_platform.is_rocm():
549 logger.warning_once(
550 "SWA bounded replay is off on ROCm (the sparse SWA metadata "
551 "builders forward replay_start, but the window clamp it relies "
552 "on lives in the FlashInfer and FlashMLA prefill kernels, so "
553 "the padded slots fault); the sliding-window cache takes part "
554 "in prefix caching instead."
555 )
556 swa_bounded_replay = False
557 self.swa_cache_layer = DeepseekV4SWACache(
558 head_dim=self.head_dim,
559 window_size=self.window_size,
560 dtype=self.kv_cache_torch_dtype,
561 prefix=f"{prefix}.swa_cache",
562 cache_config=cache_config,
563 backend_cls=self.swa_backend_cls,
564 block_size=32,
565 packed_bytes_per_token=self.swa_bytes_per_token,
566 packed_page_alignment=self.kv_page_alignment,
567 bounded_replay=swa_bounded_replay,
568 )
569
570 # The attention layer itself was already registered with the
571 # compilation context above (before indexer/compressor creation).
572
573 # Compressors live only on kv-source layers; consumers read the
574 # source's compressed cache through the forward context.
575 self.compressor = None
576 if self.is_kv_source:
577 self.compressor = DeepseekCompressor(
578 vllm_config=vllm_config,
579 compress_ratio=self.compress_ratio,
580 hidden_size=self.hidden_size,
581 head_dim=self.head_dim,
582 rotate=True,
583 prefix=f"{prefix}.compressor",
584 k_cache_prefix=self.prefix,
585 )
586 # Prefix of the attention layer owning this layer's compressed KV
587 # cache (self for kv sources).
588 if self.compress_ratio > 0:
589 assert self.kv_source_layer_id is not None
590 self.compressed_cache_prefix: str | None = _replace_layer_index(
591 prefix, self.kv_source_layer_id
592 )
593 if (
594 not self.is_kv_source
595 and self.compressed_cache_prefix not in self._static_forward_context
596 ):
597 raise NotImplementedError(
598 f"Compressed-KV source {self.compressed_cache_prefix} not "
599 "found on this rank; PP splits inside a v4.1 kv-sharing "
600 "group are not supported."
601 )
602 else:
603 self.compressed_cache_prefix = None
604
605 if vllm_config.kernel_config.enable_jit_warmup:
606 from vllm.v1.attention.backends.mla.sparse_swa import (
607 _COMPUTE_PREFILL_METADATA_KERNEL,
608 _COMPUTE_SWA_INDICES_AND_LENS_KERNEL,
609 )
610
611 _COMPUTE_PREFILL_METADATA_KERNEL.register_warmup()
612 _COMPUTE_SWA_INDICES_AND_LENS_KERNEL.register_warmup(
613 window_size=self.window_size,
614 block_size=self.swa_cache_layer.block_size,
615 )
616
617 if self.compress_ratio > 1:
618 from vllm.v1.attention.backends.mla.compressor_utils import (
619 _COMPRESSED_SLOT_MAPPING_KERNEL,
620 )
621
622 _COMPRESSED_SLOT_MAPPING_KERNEL.register_warmup()
623
624 if self.indexer is not None:
625 from vllm.v1.attention.backends.mla.indexer import (
626 _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
627 _PREPARE_UNIFORM_DECODE_KERNEL,
628 )
629
630 _PREPARE_UNIFORM_DECODE_KERNEL.register_warmup()
631 _BUILD_PREFILL_CHUNK_METADATA_KERNEL.register_warmup()
632
633 spec_config = vllm_config.speculative_config
634 if spec_config is not None and spec_config.use_dspark():
635 from vllm.v1.attention.backends.mla.sparse_swa import (
636 _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL,
637 )
638
639 _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL.register_warmup(
640 window_size=self.window_size,
641 num_speculative_tokens=spec_config.num_speculative_tokens,
642 block_size=self.swa_cache_layer.block_size,
643 )
644
645 # Every backend that gathers a chunk's KV through
646 # combine_topk_swa_indices needs its Triton kernel warmed, mega
647 # attention included -- it calls it from _forward_prefill_mega.
648 if self.backend_cls.get_name() in (
649 "FLASHMLA_SPARSE_DSV41",
650 "FLASHMLA_MEGA_ATTN_DSV41",
651 "ROCM_FLASHMLA_SPARSE_DSV4",
652 ):
653 from vllm.models.deepseek_v41.common.ops.cache_utils import (
654 _COMBINE_TOPK_SWA_INDICES_KERNEL,
655 )
656
657 _COMBINE_TOPK_SWA_INDICES_KERNEL.register_warmup()
658
659 def forward(
660 self,
661 positions: torch.Tensor,
662 hidden_states: torch.Tensor,
663 llama_4_scaling: torch.Tensor | None = None,
664 ) -> torch.Tensor:
665 # The eager attention region writes into a caller-owned buffer
666 # (breakable_cudagraph needs in-place outputs); its shape and how it is
667 # projected afterwards follow the interface contract above.
668 attn_out = self._alloc_attn_out(hidden_states.shape[0], hidden_states)
669
670 # Keep the attention input preparation in the captured graph. Only the
671 # sparse indexer and MLA attention run in the eager break below.
672 qr_kv, kv_score, indexer_weights = self._run_parallel_input_projections(
673 hidden_states
674 )
675 qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
676
677 self._prepare_and_attn_fn(
678 hidden_states,
679 qr,
680 kv,
681 qr_scale,
682 kv_score,
683 indexer_weights,
684 positions,
685 attn_out,
686 )
687 return self._o_proj(attn_out, positions)
688
689 def bind_gemm_rs(self) -> None:
690 """Fuse ``wo_b`` with the sequence-parallel TP reduce-scatter.
691
692 The decoder layer calls this after the model initialized the
693 process-wide GEMM-RS workspace and before weights load: the
694 eligibility check inspects the projection's linear kernel and weight
695 shape, which online quantization may later re-layout.
696 """
697 from vllm.model_executor.kernels.linear.cute_dsl.gemm_rs_ar import (
698 get_gemm_rs_ar,
699 )
700
701 # The layer owns the reduction under sequence parallel.
702 assert not self.wo_b.reduce_results
703 gemm_rs = get_gemm_rs_ar()
704 if gemm_rs.can_run(self.wo_b):
705 self.gemm_rs = gemm_rs
706 else:
707 gemm_rs.warn_incompatible_projection()
708
709 def _wo_b_proj(self, z: torch.Tensor | QuantizedActivation) -> torch.Tensor:
710 """Apply ``wo_b``; with GEMM-RS bound, also reduce-scatter the result.
711
712 Every ``_o_proj`` implementation projects through this so the decoder
713 layer sees one contract: when ``gemm_rs`` is bound the output is
714 already the local sequence-parallel shard, otherwise it is the
715 unreduced TP partial.
716 """
717 if self.gemm_rs is None:
718 return self.wo_b(z)
719 if isinstance(z, torch.Tensor) and self.gemm_rs.should_run(z):
720 return self.gemm_rs.apply(z, self.wo_b)
721 # Small batches stay on the unfused path, which is faster there.
722 return sp_reduce_scatter(self.wo_b(z))
723
724 def _alloc_attn_out(
725 self, num_tokens: int, hidden_states: torch.Tensor
726 ) -> "torch.Tensor | QuantizedActivation":
727 """The buffer ``forward_mqa`` fills, per the interface contract."""
728 return torch.empty(
729 (num_tokens, self.padded_heads, self.head_dim),
730 dtype=hidden_states.dtype,
731 device=hidden_states.device,
732 )
733
734 @cached_property
735 def _can_fuse_query_quant(self) -> bool:
736 from vllm.models.deepseek_v41.common.ops.query_quant import (
737 can_fuse_query_quant,
738 )
739
740 linears = [self.wq_b]
741 if self.indexer is not None:
742 linears.append(self.indexer.wq_b)
743 return can_fuse_query_quant(linears)
744
745 def _split_qkv_and_norm(
746 self, qr_kv: torch.Tensor
747 ) -> tuple[torch.Tensor | QuantizedActivation, torch.Tensor | None, torch.Tensor]:
748 """Split the fused q-lora / kv projection and RMSNorm both halves.
749
750 Compatible MXFP8 projections share the quantized Q and scales;
751 other projection backends consume the normalized Q directly.
752 """
753 qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
754 if self.q_lora_rank % 32 == 0 and self._can_fuse_query_quant:
755 from vllm.models.deepseek_v41.common.ops.query_quant import (
756 fused_q_kv_rmsnorm_quant,
757 )
758
759 qr_quant, kv = fused_q_kv_rmsnorm_quant(
760 qr,
761 kv,
762 self.q_norm.weight.data,
763 self.kv_norm.weight.data,
764 self.eps,
765 )
766 return qr_quant, None, kv
767 qr, kv = fused_q_kv_rmsnorm(
768 qr,
769 kv,
770 self.q_norm.weight.data,
771 self.kv_norm.weight.data,
772 self.eps,
773 )
774 return qr, None, kv
775
776 @eager_break_during_capture
777 def _prepare_and_attn_eager(
778 self,
779 hidden_states: torch.Tensor,
780 qr: torch.Tensor | QuantizedActivation,
781 kv: torch.Tensor,
782 qr_scale: torch.Tensor | None,
783 kv_score: torch.Tensor,
784 indexer_weights: torch.Tensor,
785 positions: torch.Tensor,
786 attn_out: "torch.Tensor | QuantizedActivation",
787 ) -> None:
788 """Wide eager region: the whole of ``_prepare_and_attn`` runs eagerly.
789
790 The nested ``_sparse_indexer_and_attn`` break runs inline, since
791 ``add_eager`` clears ``_capturing`` before invoking this.
792 """
793 self._prepare_and_attn(
794 hidden_states,
795 qr,
796 kv,
797 qr_scale,
798 kv_score,
799 indexer_weights,
800 positions,
801 attn_out,
802 )
803
804 def _prepare_and_attn(
805 self,
806 hidden_states: torch.Tensor,
807 qr: torch.Tensor | QuantizedActivation,
808 kv: torch.Tensor,
809 qr_scale: torch.Tensor | None,
810 kv_score: torch.Tensor,
811 indexer_weights: torch.Tensor,
812 positions: torch.Tensor,
813 attn_out: "torch.Tensor | QuantizedActivation",
814 ) -> None:
815 """Attention input preparation followed by the sparse indexer and MLA.
816
817 Only the latter runs in the eager break.
818
819 Q/SWA preparation overlaps state saving and compression. Once the
820 latent is ready, main-cache insertion overlaps indexer preparation;
821 both cache writes finish before sparse attention reads either cache.
822 """
823 attn_metadata = get_forward_context().attn_metadata
824 indexer = self.indexer
825 compressor = self.compressor
826 aux_streams = self.aux_stream_list
827
828 def project_query_and_cache_kv() -> torch.Tensor:
829 q = self._wq_b_proj(qr, qr_scale).view(
830 -1, self.n_local_heads, self.head_dim
831 )
832 return self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
833
834 index_q: torch.Tensor | None = None
835 index_q_scale: torch.Tensor | None = None
836 index_weights_out: torch.Tensor | None = None
837 latent: torch.Tensor | None = None
838 aux_stream = aux_streams[0] if aux_streams is not None else None
839
840 if compressor is not None:
841 # Q projection / KV insertion on the default stream overlaps the
842 # compressor on aux stream 0 (sequential on ROCm).
843 q, latent = maybe_execute_in_parallel(
844 project_query_and_cache_kv,
845 lambda: compressor(kv_score, positions),
846 self.ln_events[0],
847 self.ln_events[1],
848 aux_stream,
849 )
850 else:
851 q = project_query_and_cache_kv()
852
853 def prepare_indexer():
854 if indexer is None:
855 return None, None, None
856 return indexer(
857 qr,
858 latent,
859 indexer_weights,
860 positions,
861 self.indexer_rotary_emb,
862 qr_scale,
863 )
864
865 if compressor is not None:
866 indexer_result, _ = maybe_execute_in_parallel(
867 prepare_indexer,
868 lambda: compressor.insert_cache(latent, positions, self.rotary_emb),
869 self.ln_events[0],
870 self.ln_events[1],
871 aux_stream,
872 )
873 else:
874 indexer_result = prepare_indexer()
875 index_q, index_q_scale, index_weights_out = indexer_result
876
877 self._sparse_indexer_and_attn(
878 hidden_states,
879 index_q,
880 index_q_scale,
881 index_weights_out,
882 q,
883 kv,
884 positions,
885 attn_out,
886 )
887
888 def _fused_wqa_wkv_gemm(self, hidden_states: torch.Tensor) -> torch.Tensor:
889 # Override point: the ROCm layer preshuffles this weight in place, so
890 # it cannot go through fused_wqa_wkv directly.
891 # MergedColumnParallelLinear returns (output, bias); bias is None.
892 qr_kv, _ = self.fused_wqa_wkv(hidden_states)
893 return qr_kv
894
895 def _wq_b_proj(
896 self,
897 qr: torch.Tensor | QuantizedActivation,
898 qr_scale: torch.Tensor | None = None,
899 ) -> torch.Tensor:
900 """Project normalized Q, bypassing quantization when already fused."""
901 assert qr_scale is None, "ROCm-only path"
902 return self.wq_b(qr)
903
904 def _run_parallel_input_projections(
905 self, hidden_states: torch.Tensor
906 ) -> tuple[
907 torch.Tensor,
908 torch.Tensor | None,
909 torch.Tensor | None,
910 ]:
911 aux_streams = self.aux_stream_list
912 if aux_streams is not None:
913 aux_streams = aux_streams[:2]
914
915 # fused_wqa_wkv (heaviest) on default; the two lighter input GEMMs on
916 # aux streams 0/1 when their owning module exists. ln_events[0] is the
917 # fan-out start event; ln_events[1..2] are per-aux done events. The
918 # v4.1 indexer derives K from the kv-source compressor's latent, so
919 # unlike v4.0 there is no indexer K GEMM over hidden_states here.
920 aux_fns: list[Callable[[], Any] | None] = [None, None]
921
922 if self.compressor is not None:
923 # Local ref so the closure keeps a non-None type for mypy.
924 compressor = self.compressor
925
926 def compressor_kv_score() -> torch.Tensor:
927 return torch.mm(
928 hidden_states,
929 compressor.fused_wkv_wgate.weight.T,
930 out_dtype=torch.float32,
931 )
932
933 aux_fns[0] = compressor_kv_score
934
935 if self.indexer is not None:
936 indexer = self.indexer
937
938 def indexer_weights_proj() -> torch.Tensor:
939 # ReplicatedLinear returns (output, bias); bias is None.
940 weights, _ = indexer.weights_proj(hidden_states)
941 return weights
942
943 aux_fns[1] = indexer_weights_proj
944
945 qr_kv, (kv_score, indexer_weights) = execute_in_parallel(
946 lambda: self._fused_wqa_wkv_gemm(hidden_states),
947 aux_fns,
948 self.ln_events[0],
949 self.ln_events[1:3],
950 aux_streams,
951 enable=hidden_states.shape[0]
952 <= envs.VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD,
953 )
954
955 return qr_kv, kv_score, indexer_weights
956
957 @eager_break_during_capture
958 def _sparse_indexer_and_attn(
959 self,
960 hidden_states: torch.Tensor,
961 index_q: torch.Tensor | None,
962 index_q_scale: torch.Tensor | None,
963 index_weights: torch.Tensor | None,
964 q: torch.Tensor,
965 kv: torch.Tensor,
966 positions: torch.Tensor,
967 out: torch.Tensor,
968 ) -> None:
969 if self.indexer is not None and index_q is not None:
970 assert index_weights is not None
971 q_quant = (index_q, index_q_scale) if index_q_scale is not None else index_q
972 self.indexer.indexer_op(
973 hidden_states,
974 q_quant,
975 None,
976 index_weights,
977 )
978
979 # MLA attention writes into the pre-allocated `out` buffer
980 # ([num_tokens, padded_heads, head_dim]).
981 self.forward_mqa(q, kv, positions, out)
982
983 def _fused_qnorm_rope_kv_insert(
984 self,
985 q: torch.Tensor,
986 kv: torch.Tensor,
987 positions: torch.Tensor,
988 attn_metadata: (
989 dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None
990 ),
991 ) -> torch.Tensor:
992 """Ready ``q`` for the attention kernel and publish this step's KV.
993
994 One launch does both. With ``accepts_unnormed_unroped_query`` the
995 attention kernel norms and rotates Q itself and reads it in its own
996 chunk-interleaved layout, so the Q half of the launch is a zero-pad to
997 ``padded_heads`` -- and nothing at all once the shard is that wide.
998 """
999 if not isinstance(attn_metadata, dict):
1000 # Profile run: kernel doesn't fire; produce a padded tensor so
1001 # downstream FlashMLA gets the right shape.
1002 if self.n_local_heads >= self.padded_heads:
1003 return q
1004 if self.accepts_unnormed_unroped_query:
1005 # Padding heads sit at the tail of every head-dim chunk in
1006 # that layout, so no head-major pad of `q` reproduces it --
1007 # and nothing reads it on a profile run.
1008 return q.new_zeros((q.shape[0], self.padded_heads, q.shape[2]))
1009 return F.pad(
1010 q,
1011 (0, 0, 0, self.padded_heads - self.n_local_heads),
1012 value=0.0,
1013 )
1014
1015 swa_metadata = cast(
1016 "DeepseekSparseSWAMetadata | None",
1017 attn_metadata.get(self.swa_cache_layer.prefix),
1018 )
1019 assert swa_metadata is not None
1020
1021 swa_kv_cache = self.swa_cache_layer.kv_cache
1022 # The fused insert ops require int64 position_ids; the runner's positions
1023 # buffer is already int64, so no cast is needed.
1024 assert positions.dtype == torch.int64
1025 cos_sin_cache = self.rotary_emb.cos_sin_cache
1026 cache_dtype = swa_kv_cache.dtype
1027
1028 # kv is unchanged; attention reads kv solely via swa_kv_cache.
1029 if cache_dtype == torch.uint8:
1030 # fp8_ds_mla UE8M0 paged path. Horizontally fused:
1031 # Q side: GPT-J RoPE, zero-filling the padding head slots; the
1032 # kernel allocates and returns the padded q tensor. An
1033 # interleaved Q skips the RoPE its attention kernel owns
1034 # and keeps only the pad, which q_head_padded=0 drops
1035 # too once the shard is already padded_heads wide.
1036 # KV side: GPT-J RoPE + UE8M0 FP8 quant + paged cache insert.
1037 swa_kv_cache_2d = swa_kv_cache.view(swa_kv_cache.shape[0], -1)
1038 pad_to = (
1039 0
1040 if self.accepts_unnormed_unroped_query
1041 and self.n_local_heads == self.padded_heads
1042 else self.padded_heads
1043 )
1044 q_padded = torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
1045 q,
1046 kv,
1047 swa_kv_cache_2d,
1048 swa_metadata.slot_mapping,
1049 positions,
1050 cos_sin_cache,
1051 pad_to,
1052 self.eps,
1053 swa_metadata.block_size,
1054 False, # apply_q_norm: qr is normed before wq_b
1055 self.kv_mxfp8,
1056 not self.accepts_unnormed_unroped_query, # apply_q_rope
1057 self.accepts_unnormed_unroped_query, # is_q_interleaved
1058 )
1059 return q if pad_to == 0 else q_padded
1060
1061 assert not self.accepts_unnormed_unroped_query, (
1062 "the chunk-interleaved Q layout only pairs with a packed KV record"
1063 )
1064 # Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
1065 # row in its element dtype (no Q padding). bf16 rewrites q in place;
1066 # per-tensor fp8 writes a separately-allocated fp8 q and quantizes the
1067 # KV row.
1068 block_size = swa_metadata.block_size
1069 assert swa_kv_cache.shape[1:] == (block_size, self.head_dim)
1070 swa_kv_cache_3d = swa_kv_cache
1071 if cache_dtype == torch.bfloat16:
1072 torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert(
1073 q,
1074 kv,
1075 swa_kv_cache_3d,
1076 swa_metadata.slot_mapping,
1077 positions,
1078 cos_sin_cache,
1079 self.eps,
1080 block_size,
1081 False,
1082 )
1083 return q
1084
1085 # per-tensor fp8 (torch.float8_e4m3fn)
1086 q_fp8 = torch.empty_like(q, dtype=torch.float8_e4m3fn)
1087 torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_fp8_insert(
1088 q,
1089 kv,
1090 q_fp8,
1091 swa_kv_cache_3d,
1092 swa_metadata.slot_mapping,
1093 positions,
1094 cos_sin_cache,
1095 self._flashinfer_fp8_kv_scale,
1096 self._flashinfer_fp8_q_scale_inv,
1097 self.eps,
1098 block_size,
1099 False,
1100 )
1101 return q_fp8
1102
1103 def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
1104 # [B, H=1, N, C] -> [B, N, C]
1105 self.kv_cache = kv_cache.squeeze(1)
1106
1107 def get_attn_backend(self) -> type[AttentionBackend]:
1108 return self.backend_cls
1109
1110 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
1111 # Only kv-source layers own a compressed-KV cache; consumers read the
1112 # source's cache through the forward context, and cr==0 layers are
1113 # pure SWA. The SWA cache is allocated separately as
1114 # DeepseekV4SWACache.
1115 if not self.is_kv_source:
1116 return None
1117 # fp8_ds_mla is a UE8M0 block-scaled uint8 layout whose page rounds up
1118 # to the decode kernel's TMA stride; plain bf16 / per-tensor fp8 rows
1119 # use natural element-size pages.
1120 uses_fp8_ds_mla_layout = self.kv_cache_dtype in ("fp8_ds_mla", "nvfp4_ds_mla")
1121 return MLAAttentionSpec(
1122 block_size=vllm_config.cache_config.block_size,
1123 num_kv_heads=1,
1124 head_size=self.head_dim,
1125 dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
1126 tokens_per_state=self.compress_ratio,
1127 cache_dtype_str=self.kv_cache_dtype,
1128 alignment=self.kv_page_alignment if uses_fp8_ds_mla_layout else 512,
1129 model_version="deepseek_v4",
1130 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
1131 # Packed record width; head_size stays semantic (512).
1132 state_content_bytes=(
1133 self.compressed_bytes_per_token if uses_fp8_ds_mla_layout else None
1134 ),
1135 )
1136
1137 def _compressed_kv_cache(self) -> torch.Tensor:
1138 """The compressed-KV cache tensor of this layer's kv source (own
1139 cache for kv-source layers)."""
1140 if self.is_kv_source:
1141 return self.kv_cache
1142 assert self.compressed_cache_prefix is not None
1143 source = self._static_forward_context[self.compressed_cache_prefix]
1144 return source.kv_cache
1145
1146
1147 class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
1148 def __init__(
1149 self,
1150 head_dim: int,
1151 dtype: torch.dtype,
1152 prefix: str,
1153 cache_config: CacheConfig,
1154 compress_ratio: int = 1,
1155 ):
1156 super().__init__()
1157 self.kv_cache = torch.tensor([])
1158 self.head_dim = head_dim
1159 self.prefix = prefix
1160 self.cache_config = cache_config
1161 self.dtype = dtype
1162 self.compress_ratio = compress_ratio
1163 vllm_config = get_current_vllm_config()
1164 self.sparse_logits = vllm_config.attention_config.indexer_sparse_logits
1165 # aiter's paged MXFP4 kernels write this cache and read it in place.
1166 self.rocm_mxfp4 = current_platform.is_rocm() and dsa_indexer_uses_fp4(
1167 vllm_config
1168 )
1169 compilation_config = vllm_config.compilation_config
1170 if prefix in compilation_config.static_forward_context:
1171 raise ValueError(f"Duplicate layer name: {prefix}")
1172 compilation_config.static_forward_context[prefix] = self
1173
1174 def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
1175 # [B, H=1, N, C] -> [B, N, C]
1176 self.kv_cache = kv_cache.squeeze(1)
1177
1178 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
1179 # head_dim already carries the fp8 scale padding
1180 # tokens_per_state=1 for V3.2, >1 for DeepseekV4; same cache layout.
1181 # nvfp4_ds_mla is packed too: its compressed record is NVFP4 but the
1182 # sliding-window record stays the V4.1 MXFP8 one, so the indexer page
1183 # takes the same alignment either way.
1184 uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype in (
1185 "fp8_ds_mla",
1186 "nvfp4_ds_mla",
1187 )
1188 page_alignment = (
1189 576 if uses_fp8_ds_mla_layout and not _use_v41_mxfp8_kv_record() else 512
1190 )
1191 return MLAAttentionSpec(
1192 block_size=self.cache_config.block_size,
1193 num_kv_heads=1,
1194 head_size=self.head_dim,
1195 dtype=self.dtype,
1196 tokens_per_state=self.compress_ratio,
1197 # The indexer page shares a physical block with the main KV cache
1198 # (the block stride is the sum of every page in the group), so it
1199 # has to carry the same padding: the packed record's TMA stride,
1200 # else 512B for FlashInfer sparse (#44577).
1201 alignment=page_alignment,
1202 # DeepGEMM's paged sparse MQA logits address pages by the block
1203 # stride, which packs every layer's page in this layout, and need
1204 # it 512B-aligned; the main KV record read from the same blocks
1205 # needs its own TMA stride, so keep both.
1206 block_stride_alignment=(
1207 math.lcm(512, page_alignment)
1208 if self.sparse_logits and not self.rocm_mxfp4
1209 else None
1210 ),
1211 )
1212
1213 def forward(self): ...
1214
1215 def get_attn_backend(self) -> type[AttentionBackend]:
1216 if self.rocm_mxfp4:
1217 from vllm.v1.attention.backends.mla.rocm_paged_mxfp4_indexer import (
1218 DeepseekV41RocmMxfp4IndexerBackend,
1219 )
1220
1221 return DeepseekV41RocmMxfp4IndexerBackend
1222 if self.sparse_logits:
1223 return DeepseekV41SparseIndexerBackend
1224 return DeepseekV41IndexerBackend
1225
1226
1227 class DeepseekV4Indexer(nn.Module):
1228 """DeepSeek V4.1 sparse-attention indexer.
1229
1230 Exists only on ``index_source_layer_ids``; consumers reuse the topk
1231 indices it publishes into the shared ``topk_indices_buffer``. Unlike v4.0
1232 the index key is derived from the kv-source layer's compressor latent
1233 (``k = k_norm(wk(latent))``, ``owns_k``) instead of an indexer-local
1234 compressor over hidden states, so there is no hidden-state K GEMM here.
1235 Non-owning index sources share the kv source's paged K cache.
1236
1237 Two-level candidate filtering: the indexer at ``candidate_source_layer_id``
1238 additionally publishes the top ``candidate_topk_blocks`` blocks of
1239 ``candidate_block_size`` compressed positions (``candidate_write``);
1240 later indexers mask their scores to those blocks before their own top-k.
1241 """
1242
1243 # The scoring layers; a platform subclass swaps in its own.
1244 mqa_cls: ClassVar[type["SparseMQAIndexer | RocmSparseMQAIndexer"]] = (
1245 SparseMQAIndexer
1246 )
1247 attn_cls: ClassVar[type[SparseAttnIndexer]] = SparseAttnIndexer
1248
1249 def __init__(
1250 self,
1251 vllm_config: VllmConfig,
1252 config: DeepseekV2Config | DeepseekV3Config,
1253 hidden_size: int,
1254 q_lora_rank: int,
1255 quant_config: QuantizationConfig | None,
1256 cache_config: CacheConfig | None,
1257 topk_indices_buffer: torch.Tensor | None,
1258 compress_ratio: int,
1259 prefix: str,
1260 *,
1261 owns_k: bool,
1262 k_cache: DeepseekV4IndexerCache,
1263 main_head_dim: int,
1264 candidate_block_buffer: torch.Tensor | None = None,
1265 candidate_block_size: int = 0,
1266 candidate_write: bool = False,
1267 ):
1268 super().__init__()
1269 self.vllm_config = vllm_config
1270 self.config = config
1271 self.quant_config = quant_config
1272 self.topk_tokens = config.index_topk
1273 self.n_head = config.index_n_heads # 32
1274 self.head_dim = config.index_head_dim # 128
1275 self.rope_dim = config.qk_rope_head_dim # 64
1276 self.q_lora_rank = q_lora_rank # 1280
1277 self.compress_ratio = compress_ratio
1278 self.owns_k = owns_k
1279 self.use_fp4_kv = dsa_indexer_uses_fp4(vllm_config)
1280 logger.info_once(
1281 "Using %s indexer cache for Lightning Indexer.",
1282 "MXFP4" if self.use_fp4_kv else "FP8",
1283 )
1284
1285 # no tensor parallel, just replicated
1286 self.wq_b = ReplicatedLinear(
1287 self.q_lora_rank,
1288 self.head_dim * self.n_head,
1289 bias=False,
1290 quant_config=quant_config,
1291 prefix=f"{prefix}.wq_b",
1292 )
1293 self.weights_proj = ReplicatedLinear(
1294 hidden_size,
1295 self.n_head,
1296 bias=False,
1297 quant_config=None,
1298 prefix=f"{prefix}.weights_proj",
1299 )
1300 self.softmax_scale = self.head_dim**-0.5
1301
1302 self.scale_fmt = "ue8m0"
1303 self.quant_block_size = 128 # TODO: get from config
1304 self.topk_indices_buffer = topk_indices_buffer
1305
1306 self.max_model_len = (
1307 vllm_config.model_config.max_model_len // self.compress_ratio
1308 )
1309 self.prefix = prefix
1310
1311 self.max_total_seq_len = (
1312 get_max_prefill_buffer_size(vllm_config) // self.compress_ratio
1313 )
1314
1315 assert cache_config is not None, "Deepseek V4 indexer requires cache_config"
1316 if owns_k:
1317 # wk maps the main compressor's (pre-RoPE) latent to index keys.
1318 # The checkpoint stores it in bf16 with no quantization scales.
1319 self.wk = ReplicatedLinear(
1320 main_head_dim,
1321 self.head_dim,
1322 bias=False,
1323 quant_config=None,
1324 prefix=f"{prefix}.wk",
1325 )
1326 self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
1327 self.k_cache = k_cache
1328
1329 # Candidate consumers can score only the candidate blocks (opt-in);
1330 # the candidate source and pre-candidate indexers stay dense.
1331 use_sparse_logits = (
1332 vllm_config.attention_config.indexer_sparse_logits
1333 and candidate_block_buffer is not None
1334 and not candidate_write
1335 )
1336 self.indexer_op: SparseAttnIndexer | SparseMQAIndexer | RocmSparseMQAIndexer
1337 if use_sparse_logits:
1338 if not self.use_fp4_kv:
1339 raise ValueError(
1340 "attention_config.indexer_sparse_logits requires the MXFP4 "
1341 "indexer cache (attention_config.indexer_kv_dtype='mxfp4')."
1342 )
1343 assert candidate_block_buffer is not None
1344 assert topk_indices_buffer is not None
1345 self.indexer_op = self.mqa_cls(
1346 self.k_cache,
1347 self.topk_tokens,
1348 self.head_dim,
1349 self.max_total_seq_len,
1350 topk_indices_buffer,
1351 candidate_block_buffer,
1352 candidate_block_size,
1353 )
1354 else:
1355 self.indexer_op = self.attn_cls(
1356 self.k_cache,
1357 self.quant_block_size,
1358 self.scale_fmt,
1359 self.topk_tokens,
1360 self.head_dim,
1361 self.max_model_len,
1362 self.max_total_seq_len,
1363 self.topk_indices_buffer,
1364 skip_k_cache_insert=True,
1365 use_fp4_cache=self.use_fp4_kv,
1366 compress_ratio=self.compress_ratio,
1367 candidate_blocks=candidate_block_buffer,
1368 candidate_block_size=candidate_block_size,
1369 candidate_write=candidate_write,
1370 )
1371 # The fused Q kernel writes the per-head weights in the dtype the
1372 # scoring kernels take, so no cast runs per step.
1373 self.indexer_weights_dtype = (
1374 self.mqa_cls.weights_dtype if use_sparse_logits else torch.float32
1375 )
1376
1377 def _produce_k(
1378 self,
1379 latent: torch.Tensor | None,
1380 positions: torch.Tensor,
1381 rotary_emb: nn.Module,
1382 ) -> None:
1383 """Turn the compressor's pre-RoPE latent into paged index keys.
1384
1385 ``k_norm(wk(latent))`` at group-boundary tokens, RoPE'd at the group
1386 position and MXFP4/FP8-quantized into the indexer K cache.
1387 """
1388 attn_metadata = get_forward_context().attn_metadata
1389 if not isinstance(attn_metadata, dict) or latent is None:
1390 # Profile run: the indexer K cache is not bound yet and the
1391 # compressor skipped its latent output.
1392 return
1393 assert self.owns_k
1394 indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
1395 # ReplicatedLinear returns (output, bias); bias is None. Rows at
1396 # non-boundary tokens hold garbage latent and are skipped by the
1397 # store kernel.
1398 k_pre, _ = self.wk(latent)
1399 indexer_k_norm_rope_store(
1400 k_pre,
1401 positions,
1402 rotary_emb.cos_sin_cache,
1403 self.k_norm.weight,
1404 self.k_norm.variance_epsilon,
1405 self.k_cache.kv_cache,
1406 indexer_metadata.slot_mapping,
1407 self.compress_ratio,
1408 self.use_fp4_kv,
1409 )
1410
1411 def forward_q(
1412 self,
1413 qr: torch.Tensor | QuantizedActivation,
1414 qr_scale: torch.Tensor | None,
1415 indexer_weights: torch.Tensor,
1416 positions: torch.Tensor,
1417 rotary_emb: nn.Module,
1418 ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
1419 """Build the indexer queries: wq_b over qr plus fused RoPE/quant.
1420
1421 Split out so the ROCm layer can schedule it apart from
1422 ``_produce_k`` (e.g. on an aux stream once ``qr`` is ready).
1423 """
1424 q = self._wq_b_proj(qr, qr_scale)
1425 q = q.view(-1, self.n_head, self.head_dim)
1426 q_quant, weights = fused_indexer_q_rope_quant(
1427 positions,
1428 q,
1429 rotary_emb.cos_sin_cache,
1430 indexer_weights,
1431 self.softmax_scale,
1432 self.n_head**-0.5,
1433 use_fp4=self.use_fp4_kv,
1434 weights_out_dtype=self.indexer_weights_dtype,
1435 )
1436 if isinstance(q_quant, tuple):
1437 q, q_scale = q_quant
1438 else:
1439 q, q_scale = q_quant, None
1440 return q, q_scale, weights
1441
1442 def forward(
1443 self,
1444 qr: torch.Tensor | QuantizedActivation,
1445 latent: torch.Tensor | None,
1446 indexer_weights: torch.Tensor,
1447 positions: torch.Tensor,
1448 rotary_emb: nn.Module,
1449 qr_scale: torch.Tensor | None = None,
1450 ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
1451 attn_metadata = get_forward_context().attn_metadata
1452 if isinstance(attn_metadata, dict):
1453 indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
1454 if (
1455 indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens
1456 and not torch.cuda.is_current_stream_capturing()
1457 ):
1458 # candidates num smaller than topk, every candidate is selected
1459 # but we still need to build k cache
1460 if self.owns_k:
1461 self._produce_k(latent, positions, rotary_emb)
1462 assert self.topk_indices_buffer is not None
1463 num_tokens = (
1464 indexer_metadata.num_decode_tokens
1465 + indexer_metadata.num_prefill_tokens
1466 )
1467 if num_tokens > 0:
1468 _fill_short_context_topk_indices[(num_tokens,)](
1469 self.topk_indices_buffer,
1470 positions,
1471 TOP_K=self.topk_tokens,
1472 COMPRESS_RATIO=self.compress_ratio,
1473 PADDED_TOP_K=triton.next_power_of_2(self.topk_tokens),
1474 num_warps=8,
1475 )
1476 return None, None, None
1477
1478 if self.owns_k:
1479 # K write must land before indexer_op reads the cache
1480 # (skip_k_cache_insert=True).
1481 self._produce_k(latent, positions, rotary_emb)
1482
1483 return self.forward_q(qr, qr_scale, indexer_weights, positions, rotary_emb)
1484
1485 def _wq_b_proj(
1486 self,
1487 qr: torch.Tensor | QuantizedActivation,
1488 qr_scale: torch.Tensor | None = None,
1489 ) -> torch.Tensor:
1490 """Project normalized Q with the main attention's shared quantization."""
1491 assert qr_scale is None, "ROCm-only path"
1492 # ReplicatedLinear returns (output, bias); bias is None.
1493 q, _ = self.wq_b(qr)
1494 return q