| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278 |
- # SPDX-License-Identifier: Apache-2.0
- # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
- """
- DeepseekV4 MLA Attention Layer
- """
- from abc import ABC, abstractmethod
- from collections.abc import Callable
- from functools import cached_property
- from typing import TYPE_CHECKING, Any, ClassVar, cast
- import regex as re
- import torch
- import torch.nn as nn
- import torch.nn.functional as F
- from transformers import DeepseekV2Config, DeepseekV3Config
- import vllm.envs as envs
- from vllm.compilation.breakable_cudagraph import eager_break_during_capture
- from vllm.model_executor.layers.fusion.quant_activation import QuantizedActivation
- from vllm.model_executor.layers.linear import (
- ColumnParallelLinear,
- MergedColumnParallelLinear,
- ReplicatedLinear,
- RowParallelLinear,
- )
- from vllm.model_executor.layers.sparse_attn_indexer import SparseAttnIndexer
- from vllm.models.common.ops import fused_q_kv_rmsnorm
- from vllm.models.deepseek_v4_1.common.ops import (
- MXFP4_BLOCK_SIZE,
- fused_indexer_q_rope_quant,
- indexer_k_norm_rope_store,
- )
- if TYPE_CHECKING:
- from vllm.v1.attention.backends.mla.sparse_swa import (
- DeepseekSparseSWAMetadata,
- )
- from vllm.config import (
- CacheConfig,
- VllmConfig,
- get_current_vllm_config,
- )
- from vllm.distributed import get_dcp_group, get_tensor_model_parallel_world_size
- from vllm.forward_context import get_forward_context
- from vllm.logger import init_logger
- from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
- from vllm.model_executor.layers.layernorm import RMSNorm
- from vllm.model_executor.layers.quantization import QuantizationConfig
- from vllm.model_executor.models.utils import extract_layer_index
- from vllm.models.deepseek_v4_1.common.rope import build_deepseek_v4_rope
- from vllm.models.deepseek_v4_1.compressor import DeepseekCompressor
- from vllm.triton_utils import tl, triton
- from vllm.utils.multi_stream_utils import (
- execute_in_parallel,
- maybe_execute_in_parallel,
- )
- from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
- from vllm.v1.attention.backends.mla.indexer import (
- DeepseekV4IndexerBackend,
- dsa_indexer_uses_fp4,
- get_max_prefill_buffer_size,
- )
- from vllm.v1.attention.backends.mla.sparse_swa import DeepseekV4SWACache
- from vllm.v1.kv_cache_interface import (
- KVCacheSpec,
- MLAAttentionSpec,
- get_kv_quant_mode,
- )
- logger = init_logger(__name__)
- def _replace_layer_index(prefix: str, layer_id: int) -> str:
- """Swap the layer index inside a ``...layers.<idx>...`` prefix."""
- new_prefix, n = re.subn(r"\.layers\.\d+\.", f".layers.{layer_id}.", prefix)
- assert n == 1, f"Cannot locate layer index in prefix {prefix}"
- return new_prefix
- def _indexer_k_cache_head_dim(index_head_dim: int, use_fp4_kv: bool) -> int:
- """Per-token byte width of the paged indexer K cache row."""
- if use_fp4_kv:
- # MXFP4 stores two values per byte plus one UE8M0 byte per 32 values.
- # head_dim bytes = 64 packed values + 4 UE8M0 scales = 68.
- return index_head_dim // 2 + index_head_dim // MXFP4_BLOCK_SIZE
- # NOTE(yifan): FP8 indexer cache uses the same layout as V3.2:
- # head_dim bytes = 128 fp8 + 4 fp32 scale = 132.
- return index_head_dim + index_head_dim // 128 * 4
- @triton.jit
- def _fill_short_context_topk_indices(
- output,
- positions,
- TOP_K: tl.constexpr,
- COMPRESS_RATIO: tl.constexpr,
- PADDED_TOP_K: tl.constexpr,
- ):
- # small triton kernel that selects every candidate, -1 otherwise
- row = tl.program_id(0)
- offsets = tl.arange(0, PADDED_TOP_K)
- num_compressed = (tl.load(positions + row) + 1) // COMPRESS_RATIO
- tl.store(
- output + row * TOP_K + offsets,
- tl.where(offsets < num_compressed, offsets, -1),
- mask=offsets < TOP_K,
- )
- def _resolve_dsv4_kv_cache_dtype(
- use_fp8_ds_mla_layout: bool,
- kv_cache_dtype: str,
- cache_config: CacheConfig | None,
- ) -> tuple[str, torch.dtype]:
- """Map ``(layout, --kv-cache-dtype)`` to ``(cache_dtype_str, torch_dtype)``.
- Both layouts are paged; they differ in the per-token block format. The
- ``fp8_ds_mla`` format is UE8M0 block-scaled fp8 packed as ``uint8`` (the
- canonical ``fp8_ds_mla`` string is written back onto ``cache_config`` so the
- page-size specs pick the 576B per-token slot). Plain-row backends store each
- token's KV row in its element dtype: bf16 or per-tensor FP8 E4M3.
- """
- if use_fp8_ds_mla_layout:
- # fp8_ds_mla block format: UE8M0 block-scaled fp8 packed as uint8.
- if kv_cache_dtype == "auto":
- kv_cache_dtype = "fp8"
- if not kv_cache_dtype.startswith("fp8"):
- raise ValueError(
- "DeepseekV4 fp8_ds_mla layout only supports fp8 "
- f"kv-cache, got {kv_cache_dtype}. Please set "
- "`--kv-cache-dtype fp8` or select a backend that supports "
- "bfloat16 KV cache."
- )
- if kv_cache_dtype != "fp8_ds_mla":
- if cache_config is not None:
- cache_config.cache_dtype = "fp8_ds_mla"
- kv_cache_dtype = "fp8_ds_mla"
- logger.info_once("Using DeepSeek's fp8_ds_mla KV cache format.")
- return kv_cache_dtype, torch.uint8
- # Plain bf16 / per-tensor fp8 KV row (FlashInfer).
- if kv_cache_dtype.startswith("fp8"):
- return kv_cache_dtype, torch.float8_e4m3fn
- # auto / bfloat16 -> plain bf16 KV row.
- return kv_cache_dtype, torch.bfloat16
- class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
- """DeepseekV4 MLA attention layer.
- The platform-specific sparse-MLA forward (``forward_mqa`` /
- ``get_padded_num_q_heads`` / ``_o_proj`` / ``backend_cls``) is provided by a
- subclass — ``DeepseekV4FlashMLAAttention`` /
- ``DeepseekV4FlashInferSM120Attention`` /
- ``DeepseekV4FlashInferMLAAttention`` (CUDA) or
- ``DeepseekV41ROCMAiterMLAAttention`` (ROCm) — selected by the platform-specific
- deepseek_v4_1 model module. The base is never instantiated directly.
- """
- # Provided by the platform subclass.
- backend_cls: ClassVar[type[AttentionBackend]]
- # Backend for the SWA cache layer; None uses the default SWA backend.
- swa_backend_cls: ClassVar[type[AttentionBackend] | None] = None
- # KV-cache per-token block format (both layouts are paged). True (default)
- # = fp8_ds_mla (UE8M0 block-scaled fp8 packed as uint8); False = plain
- # bf16 / per-tensor fp8 KV row. Backends can override the instance hook when
- # a single attention class dispatches across arch-specific layouts.
- use_fp8_ds_mla_layout: ClassVar[bool] = True
- # Prefill is processed in fixed-size chunks; this bounds the bf16 kv-gather
- # workspace allocated in _forward_prefill and is also read by the dummy-run
- # path to pre-reserve that workspace.
- PREFILL_CHUNK_SIZE: ClassVar[int] = 4
- # DCP contract, mirroring AttentionImpl (v1/attention/backend.py): the
- # V4.1 layer runs its own forward (no AttentionImpl), so
- # check_attention_cp_compatibility reads these from the layer. A platform
- # subclass that implements the per-rank partial attention + LSE combine
- # sets can_return_lse_for_decode = True (SM120 FlashInfer).
- can_return_lse_for_decode: ClassVar[bool] = False
- supports_mtp_with_cp_non_trivial_interleave_size: ClassVar[bool] = False
- @classmethod
- @abstractmethod
- def get_padded_num_q_heads(cls, num_heads: int) -> int:
- """Q head count the q/output buffers are allocated at.
- The layer allocates the q/output buffers at
- ``[N, get_padded_num_q_heads(n_local_heads), head_dim]``. Must satisfy
- ``result >= num_heads``. Backends with no padding constraint return
- ``num_heads``.
- """
- raise NotImplementedError
- @abstractmethod
- def forward_mqa(
- self,
- q: torch.Tensor,
- kv: torch.Tensor,
- positions: torch.Tensor,
- output: torch.Tensor,
- ) -> None:
- """Platform-specific sparse MLA forward; writes attention into ``output``."""
- raise NotImplementedError
- @abstractmethod
- def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
- """Inverse-RoPE + wo_a + wo_b output projection (platform-specific)."""
- raise NotImplementedError
- def _uses_fp8_ds_mla_layout(self) -> bool:
- """Return whether this instance stores fp8 KV in fp8_ds_mla layout."""
- return self.use_fp8_ds_mla_layout
- def __init__(
- self,
- vllm_config: VllmConfig,
- prefix: str,
- topk_indices_buffer: torch.Tensor | None = None,
- aux_stream_list: list[torch.cuda.Stream] | None = None,
- candidate_block_buffer: torch.Tensor | None = None,
- ) -> None:
- super().__init__()
- config = vllm_config.model_config.hf_config
- quant_config = vllm_config.quant_config
- cache_config = vllm_config.cache_config
- tp_size = get_tensor_model_parallel_world_size()
- layer_id = extract_layer_index(prefix)
- self.layer_id = layer_id
- self.prefix = prefix # Alias for compatibility with compressor
- self.hidden_size = config.hidden_size
- self.n_heads = config.num_attention_heads
- assert self.n_heads % tp_size == 0
- self.n_local_heads = self.n_heads // tp_size
- # Decode context parallelism: compressed/indexer caches are sharded over
- # compressed states across the DCP group (a subset of the TP ranks),
- # SWA caches are replicated; the platform subclass gathers queries and
- # combines per-rank partial attention (DCP2-PLAN.md).
- parallel_config = vllm_config.parallel_config
- self.dcp_world_size = parallel_config.decode_context_parallel_size
- self.dcp_rank = get_dcp_group().rank_in_group if self.dcp_world_size > 1 else 0
- self.cp_kv_cache_interleave_size = parallel_config.cp_kv_cache_interleave_size
- self.need_to_return_lse_for_decode = (
- self.dcp_world_size > 1 and self.can_return_lse_for_decode
- )
- if self.dcp_world_size > 1 and self.cp_kv_cache_interleave_size != 1:
- raise NotImplementedError(
- "DeepSeek-V4.1 DCP supports cp_kv_cache_interleave_size=1 only "
- f"(got {self.cp_kv_cache_interleave_size})."
- )
- self.q_lora_rank = config.q_lora_rank
- self.o_lora_rank = config.o_lora_rank
- self.head_dim = config.head_dim
- self.rope_head_dim = config.qk_rope_head_dim
- self.nope_head_dim = self.head_dim - self.rope_head_dim
- self.n_groups = config.o_groups
- self.n_local_groups = self.n_groups // tp_size
- self.window_size = config.sliding_window
- # Vision variant: image spans are visible bidirectionally, widening
- # prefill SWA index rows by up to max_image_tokens columns.
- self.max_image_tokens = (
- getattr(config, "vision_max_n_token", 0)
- if getattr(config, "vision_n_layers", 0) > 0
- else 0
- )
- # ---- v4.1 sparse-attention topology ----
- # compress_ratios has one entry per layer (MTP layers included):
- # 0 = pure sliding window, 1 = full-length compressed cache,
- # 2 = ratio-2 compressed. Compressors and compressed-KV caches live
- # only on ``kv_source_layer_ids``; indexers only on
- # ``index_source_layer_ids``. Consumers reuse the most recently
- # published source below them.
- compress_ratios = getattr(config, "compress_ratios", None)
- if compress_ratios is not None and layer_id < len(compress_ratios):
- self.compress_ratio = int(compress_ratios[layer_id])
- else:
- # MTP layers past the configured list are pure sliding-window.
- self.compress_ratio = 0
- if self.compress_ratio not in (0, 1, 2):
- raise ValueError(
- f"DeepSeek V4.1 layer {layer_id} has compress_ratio="
- f"{self.compress_ratio}; only 0 (sliding window), 1 and 2 are "
- "supported."
- )
- self.kv_source_layers = tuple(
- getattr(config, "kv_source_layer_ids", None) or ()
- )
- self.index_source_layers = tuple(
- getattr(config, "index_source_layer_ids", None) or ()
- )
- self.candidate_source_layer = getattr(config, "candidate_source_layer_id", -1)
- self.candidate_topk_blocks = getattr(config, "candidate_topk_blocks", 0)
- self.candidate_block_size = getattr(config, "candidate_block_size", 0)
- is_backbone = layer_id < config.num_hidden_layers
- self.is_kv_source = is_backbone and layer_id in self.kv_source_layers
- self.is_index_source = is_backbone and layer_id in self.index_source_layers
- if self.compress_ratio > 0:
- if not self.kv_source_layers or not self.index_source_layers:
- raise ValueError(
- "DeepSeek V4.1 requires kv_source_layer_ids / "
- "index_source_layer_ids in the config for compressed "
- f"layers (layer {layer_id} has "
- f"compress_ratio={self.compress_ratio})."
- )
- self.kv_source_layer_id = max(
- s for s in self.kv_source_layers if s <= layer_id
- )
- self.index_source_layer_id = max(
- s for s in self.index_source_layers if s <= layer_id
- )
- else:
- self.kv_source_layer_id = None
- self.index_source_layer_id = None
- self.eps = config.rms_norm_eps
- self.scale = self.head_dim**-0.5
- # Padded Q head count is dictated by the platform subclass.
- self.padded_heads = self.get_padded_num_q_heads(self.n_local_heads)
- # Sink padded to the same head count, initialized to -inf (no sink
- # effect). Weight loading fills the first n_local_heads slots.
- self.attn_sink = nn.Parameter(
- torch.full((self.padded_heads,), -float("inf"), dtype=torch.float32),
- requires_grad=False,
- )
- self.fused_wqa_wkv = MergedColumnParallelLinear(
- self.hidden_size,
- [self.q_lora_rank, self.head_dim],
- bias=False,
- quant_config=quant_config,
- prefix=f"{prefix}.fused_wqa_wkv",
- disable_tp=True, # fused ReplicatedLinear
- )
- self.q_norm = RMSNorm(self.q_lora_rank, self.eps)
- self.wq_b = ColumnParallelLinear(
- self.q_lora_rank,
- self.n_heads * self.head_dim,
- bias=False,
- quant_config=quant_config,
- return_bias=False,
- prefix=f"{prefix}.wq_b",
- )
- self.kv_norm = RMSNorm(self.head_dim, self.eps)
- self.wo_a = ColumnParallelLinear(
- self.n_heads * self.head_dim // self.n_groups,
- self.n_groups * self.o_lora_rank,
- bias=False,
- quant_config=quant_config,
- return_bias=False,
- prefix=f"{prefix}.wo_a",
- )
- self.wo_a.is_bmm = True
- self.wo_a.bmm_batch_size = self.n_local_groups
- self._o_proj_block_size = (
- 32 if getattr(self.wo_a, "weight_block_size", None) == [1, 32] else 128
- )
- self.wo_b = RowParallelLinear(
- self.n_groups * self.o_lora_rank,
- self.hidden_size,
- bias=False,
- quant_config=quant_config,
- return_bias=False,
- prefix=f"{prefix}.wo_b",
- )
- # Initialize rotary embedding before the indexer/compressor consume it.
- self.rotary_emb = build_deepseek_v4_rope(
- config,
- head_dim=self.head_dim,
- rope_head_dim=self.rope_head_dim,
- max_position_embeddings=config.max_position_embeddings,
- compress_ratio=self.compress_ratio,
- )
- self.indexer_rotary_emb = self.rotary_emb
- self.topk_indices_buffer = topk_indices_buffer
- self.candidate_block_buffer = candidate_block_buffer
- # Register with compilation context for metadata lookup. Done before
- # indexer/compressor creation so consumers can resolve their source
- # layers through it.
- compilation_config = vllm_config.compilation_config
- if prefix and prefix in compilation_config.static_forward_context:
- raise ValueError(f"Duplicate layer name: {prefix}")
- if prefix:
- compilation_config.static_forward_context[prefix] = self
- self.kv_cache = torch.tensor([])
- self._static_forward_context = compilation_config.static_forward_context
- self.indexer = None
- if self.is_index_source:
- index_k_cache: DeepseekV4IndexerCache | None
- # Index K cache: owned only by kv-source layers (their indexer has
- # wk/k_norm); non-owning index sources share the K cache of the
- # latest kv source below them (its indexer produces the keys).
- if self.is_kv_source:
- index_k_cache = DeepseekV4IndexerCache(
- head_dim=_indexer_k_cache_head_dim(
- config.index_head_dim, dsa_indexer_uses_fp4(vllm_config)
- ),
- dtype=torch.uint8,
- prefix=f"{prefix}.indexer.k_cache",
- cache_config=cache_config,
- compress_ratio=self.compress_ratio,
- )
- else:
- assert self.kv_source_layer_id is not None
- k_cache_prefix = (
- f"{_replace_layer_index(prefix, self.kv_source_layer_id)}"
- ".indexer.k_cache"
- )
- index_k_cache = self._static_forward_context.get(k_cache_prefix)
- if index_k_cache is None:
- raise NotImplementedError(
- f"Indexer K cache source {k_cache_prefix} not found on "
- "this rank; PP splits inside a v4.1 kv-sharing group "
- "are not supported."
- )
- is_candidate_source = layer_id == self.candidate_source_layer
- uses_candidates = 0 <= self.candidate_source_layer < layer_id
- self.indexer = DeepseekV4Indexer(
- vllm_config,
- config=config,
- hidden_size=self.hidden_size,
- q_lora_rank=self.q_lora_rank,
- quant_config=quant_config,
- cache_config=cache_config,
- topk_indices_buffer=topk_indices_buffer,
- compress_ratio=self.compress_ratio,
- prefix=f"{prefix}.indexer",
- owns_k=self.is_kv_source,
- k_cache=index_k_cache,
- main_head_dim=self.head_dim,
- candidate_block_buffer=(
- candidate_block_buffer
- if (is_candidate_source or uses_candidates)
- else None
- ),
- candidate_block_size=self.candidate_block_size,
- candidate_write=is_candidate_source,
- )
- self._prepare_and_attn_fn = self._prepare_and_attn
- if not vllm_config.use_v2_model_runner:
- # MRV1's piecewise capture only tolerates the wide eager region: with
- # the narrow one the attention input preparation stays in the captured
- # graph and MRV1 produces garbage (#51430).
- self._prepare_and_attn_fn = self._prepare_and_attn_eager
- # Will be None on ROCm for now.
- self.aux_stream_list = aux_stream_list
- # [0]: GEMM start / post-GEMM event0. [1..3]: GEMM done events;
- # [1] doubles as post-GEMM event1. Reuse is safe: GEMM fully joins
- # before post-GEMM starts.
- self.ln_events = [torch.cuda.Event() for _ in range(4)]
- assert cache_config is not None, "DeepseekV4 attention requires cache_config"
- # ---- Attention / KV-cache setup ----
- self.max_num_batched_tokens = (
- vllm_config.scheduler_config.max_num_batched_tokens
- )
- self.max_model_len = vllm_config.model_config.max_model_len
- # Resolve the kv-cache dtype from this backend's block format. The same
- # resolution drives the SWA cache tensor dtype below.
- self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
- self._uses_fp8_ds_mla_layout(), cache_config.cache_dtype, cache_config
- )
- self.swa_cache_layer = DeepseekV4SWACache(
- head_dim=self.head_dim,
- window_size=self.window_size,
- dtype=self.kv_cache_torch_dtype,
- prefix=f"{prefix}.swa_cache",
- cache_config=cache_config,
- backend_cls=self.swa_backend_cls,
- # Tech2Wild/Kai 2026-09-10: page size is a property of the SWA decode
- # kernel (FlashInfer SM120 sparse-MLA kernels are 64-token pages).
- block_size=getattr(self.swa_backend_cls, "get_swa_block_size", lambda: 32)(),
- )
- # The attention layer itself was already registered with the
- # compilation context above (before indexer/compressor creation).
- # Compressors live only on kv-source layers; consumers read the
- # source's compressed cache through the forward context.
- self.compressor = None
- if self.is_kv_source:
- self.compressor = DeepseekCompressor(
- vllm_config=vllm_config,
- compress_ratio=self.compress_ratio,
- hidden_size=self.hidden_size,
- head_dim=self.head_dim,
- rotate=True,
- prefix=f"{prefix}.compressor",
- k_cache_prefix=self.prefix,
- )
- # Prefix of the attention layer owning this layer's compressed KV
- # cache (self for kv sources).
- if self.compress_ratio > 0:
- assert self.kv_source_layer_id is not None
- self.compressed_cache_prefix: str | None = _replace_layer_index(
- prefix, self.kv_source_layer_id
- )
- if (
- not self.is_kv_source
- and self.compressed_cache_prefix not in self._static_forward_context
- ):
- raise NotImplementedError(
- f"Compressed-KV source {self.compressed_cache_prefix} not "
- "found on this rank; PP splits inside a v4.1 kv-sharing "
- "group are not supported."
- )
- else:
- self.compressed_cache_prefix = None
- if vllm_config.kernel_config.enable_jit_warmup:
- from vllm.v1.attention.backends.mla.sparse_swa import (
- _COMPUTE_PREFILL_METADATA_KERNEL,
- _COMPUTE_SWA_INDICES_AND_LENS_KERNEL,
- )
- _COMPUTE_PREFILL_METADATA_KERNEL.register_warmup()
- _COMPUTE_SWA_INDICES_AND_LENS_KERNEL.register_warmup(
- window_size=self.window_size,
- block_size=self.swa_cache_layer.block_size,
- max_image_tokens=self.max_image_tokens,
- )
- if self.compress_ratio > 1:
- from vllm.v1.attention.backends.mla.compressor_utils import (
- _COMPRESSED_SLOT_MAPPING_KERNEL,
- )
- _COMPRESSED_SLOT_MAPPING_KERNEL.register_warmup()
- if self.indexer is not None:
- from vllm.v1.attention.backends.mla.indexer import (
- _BUILD_PREFILL_CHUNK_METADATA_KERNEL,
- _PREPARE_UNIFORM_DECODE_KERNEL,
- )
- _PREPARE_UNIFORM_DECODE_KERNEL.register_warmup()
- _BUILD_PREFILL_CHUNK_METADATA_KERNEL.register_warmup()
- spec_config = vllm_config.speculative_config
- if spec_config is not None and spec_config.use_dspark():
- from vllm.v1.attention.backends.mla.sparse_swa import (
- _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL,
- )
- _COMPUTE_DSPARK_NONCAUSAL_SWA_INDICES_KERNEL.register_warmup(
- window_size=self.window_size,
- num_speculative_tokens=spec_config.num_speculative_tokens,
- block_size=self.swa_cache_layer.block_size,
- )
- if self.backend_cls.get_name() in (
- "FLASHMLA_SPARSE_DSV41",
- "ROCM_FLASHMLA_SPARSE_DSV4",
- ):
- from vllm.models.deepseek_v4_1.common.ops.cache_utils import (
- _COMBINE_TOPK_SWA_INDICES_KERNEL,
- )
- _COMBINE_TOPK_SWA_INDICES_KERNEL.register_warmup()
- def forward(
- self,
- positions: torch.Tensor,
- hidden_states: torch.Tensor,
- llama_4_scaling: torch.Tensor | None = None,
- ) -> torch.Tensor:
- # Pre-allocate attention output with FlashMLA-padded head count.
- # The op writes into `o_padded`; we slice to n_local_heads after.
- num_tokens = hidden_states.shape[0]
- o_padded = torch.empty(
- (num_tokens, self.padded_heads, self.head_dim),
- dtype=hidden_states.dtype,
- device=hidden_states.device,
- )
- # Keep the attention input preparation in the captured graph. Only the
- # sparse indexer and MLA attention run in the eager break below.
- qr_kv, kv_score, indexer_weights = self._run_parallel_input_projections(
- hidden_states
- )
- qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
- self._prepare_and_attn_fn(
- hidden_states,
- qr,
- kv,
- qr_scale,
- kv_score,
- indexer_weights,
- positions,
- o_padded,
- )
- o = o_padded[:, : self.n_local_heads, :]
- # Inverse-RoPE + wo_a + wo_b output projection (platform-specific).
- return self._o_proj(o, positions)
- @cached_property
- def _can_fuse_query_quant(self) -> bool:
- from vllm.models.deepseek_v4_1.common.ops.query_quant import (
- can_fuse_query_quant,
- )
- linears = [self.wq_b]
- if self.indexer is not None:
- linears.append(self.indexer.wq_b)
- return can_fuse_query_quant(linears)
- def _split_qkv_and_norm(
- self, qr_kv: torch.Tensor
- ) -> tuple[torch.Tensor | QuantizedActivation, torch.Tensor | None, torch.Tensor]:
- """Split the fused q-lora / kv projection and RMSNorm both halves.
- Compatible MXFP8 projections share the quantized Q and scales;
- other projection backends consume the normalized Q directly.
- """
- qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
- if self.q_lora_rank % 32 == 0 and self._can_fuse_query_quant:
- from vllm.models.deepseek_v4_1.common.ops.query_quant import (
- fused_q_kv_rmsnorm_quant,
- )
- qr_quant, kv = fused_q_kv_rmsnorm_quant(
- qr,
- kv,
- self.q_norm.weight.data,
- self.kv_norm.weight.data,
- self.eps,
- )
- return qr_quant, None, kv
- qr, kv = fused_q_kv_rmsnorm(
- qr,
- kv,
- self.q_norm.weight.data,
- self.kv_norm.weight.data,
- self.eps,
- )
- return qr, None, kv
- @eager_break_during_capture
- def _prepare_and_attn_eager(
- self,
- hidden_states: torch.Tensor,
- qr: torch.Tensor | QuantizedActivation,
- kv: torch.Tensor,
- qr_scale: torch.Tensor | None,
- kv_score: torch.Tensor,
- indexer_weights: torch.Tensor,
- positions: torch.Tensor,
- o_padded: torch.Tensor,
- ) -> None:
- """Wide eager region: the whole of ``_prepare_and_attn`` runs eagerly.
- The nested ``_sparse_indexer_and_attn`` break runs inline, since
- ``add_eager`` clears ``_capturing`` before invoking this.
- """
- self._prepare_and_attn(
- hidden_states,
- qr,
- kv,
- qr_scale,
- kv_score,
- indexer_weights,
- positions,
- o_padded,
- )
- def _prepare_and_attn(
- self,
- hidden_states: torch.Tensor,
- qr: torch.Tensor | QuantizedActivation,
- kv: torch.Tensor,
- qr_scale: torch.Tensor | None,
- kv_score: torch.Tensor,
- indexer_weights: torch.Tensor,
- positions: torch.Tensor,
- o_padded: torch.Tensor,
- ) -> None:
- """Attention input preparation followed by the sparse indexer and MLA.
- Only the latter runs in the eager break.
- Q/SWA preparation overlaps state saving and compression. Once the
- latent is ready, main-cache insertion overlaps indexer preparation;
- both cache writes finish before sparse attention reads either cache.
- """
- attn_metadata = get_forward_context().attn_metadata
- indexer = self.indexer
- compressor = self.compressor
- aux_streams = self.aux_stream_list
- def project_query_and_cache_kv() -> torch.Tensor:
- q = self._wq_b_proj(qr, qr_scale).view(
- -1, self.n_local_heads, self.head_dim
- )
- return self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
- index_q: torch.Tensor | None = None
- index_q_scale: torch.Tensor | None = None
- index_weights_out: torch.Tensor | None = None
- latent: torch.Tensor | None = None
- aux_stream = aux_streams[0] if aux_streams is not None else None
- if compressor is not None:
- # Q projection / KV insertion on the default stream overlaps the
- # compressor on aux stream 0 (sequential on ROCm).
- q, latent = maybe_execute_in_parallel(
- project_query_and_cache_kv,
- lambda: compressor(kv_score, positions),
- self.ln_events[0],
- self.ln_events[1],
- aux_stream,
- )
- else:
- q = project_query_and_cache_kv()
- def prepare_indexer():
- if indexer is None:
- return None, None, None
- return indexer(
- qr,
- latent,
- indexer_weights,
- positions,
- self.indexer_rotary_emb,
- qr_scale,
- )
- if compressor is not None:
- indexer_result, _ = maybe_execute_in_parallel(
- prepare_indexer,
- lambda: compressor.insert_cache(latent, positions, self.rotary_emb),
- self.ln_events[0],
- self.ln_events[1],
- aux_stream,
- )
- else:
- indexer_result = prepare_indexer()
- index_q, index_q_scale, index_weights_out = indexer_result
- self._sparse_indexer_and_attn(
- hidden_states,
- index_q,
- index_q_scale,
- index_weights_out,
- q,
- kv,
- positions,
- o_padded,
- )
- def _fused_wqa_wkv_gemm(self, hidden_states: torch.Tensor) -> torch.Tensor:
- # Override point: the ROCm layer preshuffles this weight in place, so
- # it cannot go through fused_wqa_wkv directly.
- # MergedColumnParallelLinear returns (output, bias); bias is None.
- qr_kv, _ = self.fused_wqa_wkv(hidden_states)
- return qr_kv
- def _wq_b_proj(
- self,
- qr: torch.Tensor | QuantizedActivation,
- qr_scale: torch.Tensor | None = None,
- ) -> torch.Tensor:
- """Project normalized Q, bypassing quantization when already fused."""
- assert qr_scale is None, "ROCm-only path"
- return self.wq_b(qr)
- def _run_parallel_input_projections(
- self, hidden_states: torch.Tensor
- ) -> tuple[
- torch.Tensor,
- torch.Tensor | None,
- torch.Tensor | None,
- ]:
- aux_streams = self.aux_stream_list
- if aux_streams is not None:
- aux_streams = aux_streams[:2]
- # fused_wqa_wkv (heaviest) on default; the two lighter input GEMMs on
- # aux streams 0/1 when their owning module exists. ln_events[0] is the
- # fan-out start event; ln_events[1..2] are per-aux done events. The
- # v4.1 indexer derives K from the kv-source compressor's latent, so
- # unlike v4.0 there is no indexer K GEMM over hidden_states here.
- aux_fns: list[Callable[[], Any] | None] = [None, None]
- if self.compressor is not None:
- # Local ref so the closure keeps a non-None type for mypy.
- compressor = self.compressor
- def compressor_kv_score() -> torch.Tensor:
- return torch.mm(
- hidden_states,
- compressor.fused_wkv_wgate.weight.T,
- out_dtype=torch.float32,
- )
- aux_fns[0] = compressor_kv_score
- if self.indexer is not None:
- indexer = self.indexer
- def indexer_weights_proj() -> torch.Tensor:
- # ReplicatedLinear returns (output, bias); bias is None.
- weights, _ = indexer.weights_proj(hidden_states)
- return weights
- aux_fns[1] = indexer_weights_proj
- qr_kv, (kv_score, indexer_weights) = execute_in_parallel(
- lambda: self._fused_wqa_wkv_gemm(hidden_states),
- aux_fns,
- self.ln_events[0],
- self.ln_events[1:3],
- aux_streams,
- enable=hidden_states.shape[0]
- <= envs.VLLM_MULTI_STREAM_GEMM_TOKEN_THRESHOLD,
- )
- return qr_kv, kv_score, indexer_weights
- @eager_break_during_capture
- def _sparse_indexer_and_attn(
- self,
- hidden_states: torch.Tensor,
- index_q: torch.Tensor | None,
- index_q_scale: torch.Tensor | None,
- index_weights: torch.Tensor | None,
- q: torch.Tensor,
- kv: torch.Tensor,
- positions: torch.Tensor,
- out: torch.Tensor,
- ) -> None:
- if self.indexer is not None and index_q is not None:
- assert index_weights is not None
- q_quant = (index_q, index_q_scale) if index_q_scale is not None else index_q
- self.indexer.indexer_op(
- hidden_states,
- q_quant,
- None,
- index_weights,
- )
- # MLA attention writes into the pre-allocated `out` buffer
- # ([num_tokens, padded_heads, head_dim]).
- self.forward_mqa(q, kv, positions, out)
- def _fused_qnorm_rope_kv_insert(
- self,
- q: torch.Tensor,
- kv: torch.Tensor,
- positions: torch.Tensor,
- attn_metadata: (
- dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None
- ),
- ) -> torch.Tensor:
- if not isinstance(attn_metadata, dict):
- # Profile run: kernel doesn't fire; produce a padded tensor so
- # downstream FlashMLA gets the right shape.
- if self.n_local_heads < self.padded_heads:
- return F.pad(
- q,
- (0, 0, 0, self.padded_heads - self.n_local_heads),
- value=0.0,
- )
- return q
- swa_metadata = cast(
- "DeepseekSparseSWAMetadata | None",
- attn_metadata.get(self.swa_cache_layer.prefix),
- )
- assert swa_metadata is not None
- swa_kv_cache = self.swa_cache_layer.kv_cache
- # The fused insert ops require int64 position_ids; the runner's positions
- # buffer is already int64, so no cast is needed.
- assert positions.dtype == torch.int64
- cos_sin_cache = self.rotary_emb.cos_sin_cache
- cache_dtype = swa_kv_cache.dtype
- # kv is unchanged; attention reads kv solely via swa_kv_cache.
- if cache_dtype == torch.uint8:
- # fp8_ds_mla UE8M0 paged path. Horizontally fused:
- # Q side: GPT-J RoPE, zero-filling the padding head slots; the
- # kernel allocates and returns the padded q tensor.
- # KV side: GPT-J RoPE + UE8M0 FP8 quant + paged cache insert.
- swa_kv_cache_2d = swa_kv_cache.view(swa_kv_cache.shape[0], -1)
- return torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
- q,
- kv,
- swa_kv_cache_2d,
- swa_metadata.slot_mapping,
- positions,
- cos_sin_cache,
- self.padded_heads,
- self.eps,
- swa_metadata.block_size,
- False,
- )
- # Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
- # row in its element dtype (no Q padding). bf16 rewrites q in place;
- # per-tensor fp8 writes a separately-allocated fp8 q and quantizes the
- # KV row.
- block_size = swa_metadata.block_size
- assert swa_kv_cache.shape[1:] == (block_size, self.head_dim)
- swa_kv_cache_3d = swa_kv_cache
- if cache_dtype == torch.bfloat16:
- torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert(
- q,
- kv,
- swa_kv_cache_3d,
- swa_metadata.slot_mapping,
- positions,
- cos_sin_cache,
- self.eps,
- block_size,
- False,
- )
- return q
- # per-tensor fp8 (torch.float8_e4m3fn)
- q_fp8 = torch.empty_like(q, dtype=torch.float8_e4m3fn)
- torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_fp8_insert(
- q,
- kv,
- q_fp8,
- swa_kv_cache_3d,
- swa_metadata.slot_mapping,
- positions,
- cos_sin_cache,
- self._flashinfer_fp8_kv_scale,
- self._flashinfer_fp8_q_scale_inv,
- self.eps,
- block_size,
- False,
- )
- return q_fp8
- def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
- # [B, H=1, N, C] -> [B, N, C]
- self.kv_cache = kv_cache.squeeze(1)
- def get_attn_backend(self) -> type[AttentionBackend]:
- return self.backend_cls
- def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
- # Only kv-source layers own a compressed-KV cache; consumers read the
- # source's cache through the forward context, and cr==0 layers are
- # pure SWA. The SWA cache is allocated separately as
- # DeepseekV4SWACache.
- if not self.is_kv_source:
- return None
- # fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B
- # alignment; plain bf16 / per-tensor fp8 rows use natural element-size
- # pages.
- uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla"
- # Tech2Wild/Kai 2026-09-10: SM120 FlashInfer sparse-MLA kernels are
- # instantiated for 64-STATE compressed pages, so a kv-source layer's page
- # is 64 * compress_ratio tokens there (ratio 1 -> 64, ratio 2 -> 128).
- _hook = getattr(self.backend_cls, "get_compressed_block_size", None)
- _bs = _hook(self.compress_ratio) if _hook is not None else None
- return MLAAttentionSpec(
- block_size=_bs or vllm_config.cache_config.block_size,
- num_kv_heads=1,
- head_size=self.head_dim,
- dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
- tokens_per_state=self.compress_ratio,
- cache_dtype_str=self.kv_cache_dtype,
- alignment=576 if uses_fp8_ds_mla_layout else 512,
- model_version="deepseek_v4",
- kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
- # DeepseekV4: 448B NoPE + 128B RoPE + 8B fp8 scale = 584B per token;
- # head_size stays semantic (512).
- state_content_bytes=584 if uses_fp8_ds_mla_layout else None,
- )
- def _compressed_kv_cache(self) -> torch.Tensor:
- """The compressed-KV cache tensor of this layer's kv source (own
- cache for kv-source layers)."""
- if self.is_kv_source:
- return self.kv_cache
- assert self.compressed_cache_prefix is not None
- source = self._static_forward_context[self.compressed_cache_prefix]
- return source.kv_cache
- def _dsv41_indexer_sm12x() -> bool:
- from vllm.platforms import current_platform
- return current_platform.is_cuda() and current_platform.is_device_capability_family(120)
- class DeepseekV4IndexerSM12xBackend(DeepseekV4IndexerBackend):
- """Tech2Wild 2026-09-10: SM12x indexer pages hold 64 states (64 tokens at
- compress ratio 1, 128 at ratio 2), so both sizes are kernel block sizes."""
- @staticmethod
- def get_supported_kernel_block_sizes() -> list[int]:
- return [128, 64]
- class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
- def __init__(
- self,
- head_dim: int,
- dtype: torch.dtype,
- prefix: str,
- cache_config: CacheConfig,
- compress_ratio: int = 1,
- ):
- super().__init__()
- self.kv_cache = torch.tensor([])
- self.head_dim = head_dim
- self.prefix = prefix
- self.cache_config = cache_config
- self.dtype = dtype
- self.compress_ratio = compress_ratio
- compilation_config = get_current_vllm_config().compilation_config
- if prefix in compilation_config.static_forward_context:
- raise ValueError(f"Duplicate layer name: {prefix}")
- compilation_config.static_forward_context[prefix] = self
- def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
- # [B, H=1, N, C] -> [B, N, C]
- self.kv_cache = kv_cache.squeeze(1)
- def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
- # head_dim already carries the fp8 scale padding
- # tokens_per_state=1 for V3.2, >1 for DeepseekV4; same cache layout.
- uses_fp8_ds_mla_layout = vllm_config.cache_config.cache_dtype == "fp8_ds_mla"
- # Tech2Wild 2026-09-10 (SM12x): DeepGEMM's paged MQA logits (indexer decode)
- # takes 32 or 64 states per block, and the indexer metadata builder passes
- # this spec's num_states, so pages hold 64 states = 64 * compress_ratio tokens.
- block_size = self.cache_config.block_size
- if _dsv41_indexer_sm12x():
- block_size = 64 * max(1, self.compress_ratio)
- return MLAAttentionSpec(
- block_size=block_size,
- num_kv_heads=1,
- head_size=self.head_dim,
- dtype=self.dtype,
- tokens_per_state=self.compress_ratio,
- # 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
- alignment=576 if uses_fp8_ds_mla_layout else 512,
- )
- def forward(self): ...
- def get_attn_backend(self) -> type[AttentionBackend]:
- if _dsv41_indexer_sm12x():
- return DeepseekV4IndexerSM12xBackend
- return DeepseekV4IndexerBackend
- class DeepseekV4Indexer(nn.Module):
- """DeepSeek V4.1 sparse-attention indexer.
- Exists only on ``index_source_layer_ids``; consumers reuse the topk
- indices it publishes into the shared ``topk_indices_buffer``. Unlike v4.0
- the index key is derived from the kv-source layer's compressor latent
- (``k = k_norm(wk(latent))``, ``owns_k``) instead of an indexer-local
- compressor over hidden states, so there is no hidden-state K GEMM here.
- Non-owning index sources share the kv source's paged K cache.
- Two-level candidate filtering: the indexer at ``candidate_source_layer_id``
- additionally publishes the top ``candidate_topk_blocks`` blocks of
- ``candidate_block_size`` compressed positions (``candidate_write``);
- later indexers mask their scores to those blocks before their own top-k.
- """
- def __init__(
- self,
- vllm_config: VllmConfig,
- config: DeepseekV2Config | DeepseekV3Config,
- hidden_size: int,
- q_lora_rank: int,
- quant_config: QuantizationConfig | None,
- cache_config: CacheConfig | None,
- topk_indices_buffer: torch.Tensor | None,
- compress_ratio: int,
- prefix: str,
- *,
- owns_k: bool,
- k_cache: DeepseekV4IndexerCache,
- main_head_dim: int,
- candidate_block_buffer: torch.Tensor | None = None,
- candidate_block_size: int = 0,
- candidate_write: bool = False,
- ):
- super().__init__()
- self.vllm_config = vllm_config
- self.config = config
- self.quant_config = quant_config
- self.topk_tokens = config.index_topk
- self.n_head = config.index_n_heads # 32
- self.head_dim = config.index_head_dim # 128
- self.rope_dim = config.qk_rope_head_dim # 64
- self.q_lora_rank = q_lora_rank # 1280
- self.compress_ratio = compress_ratio
- self.owns_k = owns_k
- self.use_fp4_kv = dsa_indexer_uses_fp4(vllm_config)
- logger.info_once(
- "Using %s indexer cache for Lightning Indexer.",
- "MXFP4" if self.use_fp4_kv else "FP8",
- )
- # no tensor parallel, just replicated
- self.wq_b = ReplicatedLinear(
- self.q_lora_rank,
- self.head_dim * self.n_head,
- bias=False,
- quant_config=quant_config,
- prefix=f"{prefix}.wq_b",
- )
- self.weights_proj = ReplicatedLinear(
- hidden_size,
- self.n_head,
- bias=False,
- quant_config=None,
- prefix=f"{prefix}.weights_proj",
- )
- self.softmax_scale = self.head_dim**-0.5
- self.scale_fmt = "ue8m0"
- self.quant_block_size = 128 # TODO: get from config
- self.topk_indices_buffer = topk_indices_buffer
- self.max_model_len = (
- vllm_config.model_config.max_model_len // self.compress_ratio
- )
- self.prefix = prefix
- self.max_total_seq_len = (
- get_max_prefill_buffer_size(vllm_config) // self.compress_ratio
- )
- assert cache_config is not None, "Deepseek V4 indexer requires cache_config"
- if owns_k:
- # wk maps the main compressor's (pre-RoPE) latent to index keys.
- # The checkpoint stores it in bf16 with no quantization scales.
- self.wk = ReplicatedLinear(
- main_head_dim,
- self.head_dim,
- bias=False,
- quant_config=None,
- prefix=f"{prefix}.wk",
- )
- self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
- self.k_cache = k_cache
- self.indexer_op = SparseAttnIndexer(
- self.k_cache,
- self.quant_block_size,
- self.scale_fmt,
- self.topk_tokens,
- self.head_dim,
- self.max_model_len,
- self.max_total_seq_len,
- self.topk_indices_buffer,
- skip_k_cache_insert=True,
- use_fp4_cache=self.use_fp4_kv,
- compress_ratio=self.compress_ratio,
- candidate_blocks=candidate_block_buffer,
- candidate_block_size=candidate_block_size,
- candidate_write=candidate_write,
- )
- def _produce_k(
- self,
- latent: torch.Tensor | None,
- positions: torch.Tensor,
- rotary_emb: nn.Module,
- ) -> None:
- """Turn the compressor's pre-RoPE latent into paged index keys.
- ``k_norm(wk(latent))`` at group-boundary tokens, RoPE'd at the group
- position and MXFP4/FP8-quantized into the indexer K cache.
- """
- attn_metadata = get_forward_context().attn_metadata
- if not isinstance(attn_metadata, dict) or latent is None:
- # Profile run: the indexer K cache is not bound yet and the
- # compressor skipped its latent output.
- return
- assert self.owns_k
- indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
- # ReplicatedLinear returns (output, bias); bias is None. Rows at
- # non-boundary tokens hold garbage latent and are skipped by the
- # store kernel.
- k_pre, _ = self.wk(latent)
- indexer_k_norm_rope_store(
- k_pre,
- positions,
- rotary_emb.cos_sin_cache,
- self.k_norm.weight,
- self.k_norm.variance_epsilon,
- self.k_cache.kv_cache,
- indexer_metadata.slot_mapping,
- self.compress_ratio,
- self.use_fp4_kv,
- )
- def forward(
- self,
- qr: torch.Tensor | QuantizedActivation,
- latent: torch.Tensor | None,
- indexer_weights: torch.Tensor,
- positions: torch.Tensor,
- rotary_emb: nn.Module,
- qr_scale: torch.Tensor | None = None,
- ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
- attn_metadata = get_forward_context().attn_metadata
- if isinstance(attn_metadata, dict):
- indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
- if (
- indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens
- and not torch.cuda.is_current_stream_capturing()
- ):
- # candidates num smaller than topk, every candidate is selected
- # but we still need to build k cache
- if self.owns_k:
- self._produce_k(latent, positions, rotary_emb)
- assert self.topk_indices_buffer is not None
- num_tokens = (
- indexer_metadata.num_decode_tokens
- + indexer_metadata.num_prefill_tokens
- )
- if num_tokens > 0:
- _fill_short_context_topk_indices[(num_tokens,)](
- self.topk_indices_buffer,
- positions,
- TOP_K=self.topk_tokens,
- COMPRESS_RATIO=self.compress_ratio,
- PADDED_TOP_K=triton.next_power_of_2(self.topk_tokens),
- num_warps=8,
- )
- return None, None, None
- if self.owns_k:
- # K write must land before indexer_op reads the cache
- # (skip_k_cache_insert=True).
- self._produce_k(latent, positions, rotary_emb)
- q = self._wq_b_proj(qr, qr_scale)
- q = q.view(-1, self.n_head, self.head_dim)
- q_quant, weights = fused_indexer_q_rope_quant(
- positions,
- q,
- rotary_emb.cos_sin_cache,
- indexer_weights,
- self.softmax_scale,
- self.n_head**-0.5,
- use_fp4=self.use_fp4_kv,
- )
- if isinstance(q_quant, tuple):
- q, q_scale = q_quant
- else:
- q, q_scale = q_quant, None
- return q, q_scale, weights
- def _wq_b_proj(
- self,
- qr: torch.Tensor | QuantizedActivation,
- qr_scale: torch.Tensor | None = None,
- ) -> torch.Tensor:
- """Project normalized Q with the main attention's shared quantization."""
- assert qr_scale is None, "ROCm-only path"
- # ReplicatedLinear returns (output, bias); bias is None.
- q, _ = self.wq_b(qr)
- return q
|