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