compressor.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. from dataclasses import dataclass
  4. from typing import Any, ClassVar, cast
  5. import torch
  6. from torch import nn
  7. from vllm.config import VllmConfig, get_current_vllm_config
  8. from vllm.forward_context import get_forward_context
  9. from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
  10. from vllm.model_executor.layers.layernorm import RMSNorm
  11. from vllm.model_executor.layers.linear import MergedColumnParallelLinear
  12. from vllm.models.deepseek_v4_1.common.ops.fused_compress_quant_cache import (
  13. fused_save_compress_norm,
  14. rope_quant_insert,
  15. )
  16. from vllm.platforms import current_platform
  17. from vllm.triton_utils import tl, triton
  18. from vllm.v1.attention.backend import (
  19. AttentionBackend,
  20. AttentionCGSupport,
  21. AttentionMetadataBuilder,
  22. CommonAttentionMetadata,
  23. MultipleOf,
  24. )
  25. from vllm.v1.kv_cache_interface import CircularBufferSpec, KVCacheSpec
  26. class CompressorBackend(AttentionBackend):
  27. def __init__(self):
  28. super().__init__()
  29. @staticmethod
  30. def get_name() -> str:
  31. return "CompressorBackend"
  32. @staticmethod
  33. def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
  34. return [MultipleOf(1)]
  35. @classmethod
  36. def get_supported_head_sizes(cls) -> list[int]:
  37. return [512, 1024]
  38. @staticmethod
  39. def get_builder_cls() -> type["CompressorMetadataBuilder"]:
  40. return CompressorMetadataBuilder
  41. @dataclass
  42. class CompressorMetadata:
  43. # [num_tokens] ring slot of every token: block * capacity + pos % capacity.
  44. slot_mapping: torch.Tensor
  45. query_start_loc: torch.Tensor # [num_reqs + 1]
  46. token_to_req_indices: torch.Tensor # [num_tokens]
  47. @triton.jit
  48. def _ring_slot_mapping_kernel(
  49. slot_mapping_ptr,
  50. block_table_ptr,
  51. block_table_stride,
  52. token_to_req_ptr,
  53. positions_ptr,
  54. num_actual_tokens,
  55. num_tokens,
  56. CAPACITY: tl.constexpr,
  57. BLOCK: tl.constexpr,
  58. ):
  59. offsets = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
  60. valid = offsets < num_actual_tokens
  61. req = tl.load(token_to_req_ptr + offsets, mask=valid, other=0).to(tl.int64)
  62. block = tl.load(block_table_ptr + req * block_table_stride, mask=valid, other=0)
  63. pos = tl.load(positions_ptr + offsets, mask=valid, other=0)
  64. slot = block.to(tl.int64) * CAPACITY + pos % CAPACITY
  65. tl.store(
  66. slot_mapping_ptr + offsets, tl.where(valid, slot, -1), mask=offsets < num_tokens
  67. )
  68. class CompressorMetadataBuilder(AttentionMetadataBuilder):
  69. _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
  70. def __init__(self, *args, **kwargs):
  71. super().__init__(*args, **kwargs)
  72. assert isinstance(self.kv_cache_spec, CircularBufferSpec)
  73. self.capacity = self.kv_cache_spec.block_size
  74. max_num_batched_tokens = (
  75. self.vllm_config.scheduler_config.max_num_batched_tokens
  76. )
  77. self.token_to_req_indices = torch.zeros(
  78. max_num_batched_tokens, dtype=torch.int32, device=self.device
  79. )
  80. self.slot_mapping_buffer = torch.empty(
  81. max_num_batched_tokens, dtype=torch.int64, device=self.device
  82. )
  83. def build(
  84. self,
  85. common_prefix_len: int,
  86. common_attn_metadata: CommonAttentionMetadata,
  87. fast_build: bool = False,
  88. ) -> CompressorMetadata:
  89. num_tokens = common_attn_metadata.slot_mapping.numel()
  90. positions = common_attn_metadata.positions
  91. assert positions is not None
  92. token_to_req_indices = common_attn_metadata.token_to_req_indices(
  93. self.token_to_req_indices
  94. )
  95. slot_mapping = self.slot_mapping_buffer[:num_tokens]
  96. block_table = common_attn_metadata.block_table_tensor
  97. _ring_slot_mapping_kernel[(triton.cdiv(num_tokens, 256),)](
  98. slot_mapping,
  99. block_table,
  100. block_table.stride(0),
  101. token_to_req_indices,
  102. positions,
  103. common_attn_metadata.num_actual_tokens,
  104. num_tokens,
  105. CAPACITY=self.capacity,
  106. BLOCK=256,
  107. )
  108. return CompressorMetadata(
  109. slot_mapping=slot_mapping,
  110. query_start_loc=common_attn_metadata.query_start_loc,
  111. token_to_req_indices=token_to_req_indices,
  112. )
  113. class CompressorStateCache(torch.nn.Module, AttentionLayerBase):
  114. """Per-request ring of the open group's [kv, score] rows (ratio > 1)."""
  115. def __init__(self, state_dim: int, dtype: torch.dtype, prefix: str):
  116. super().__init__()
  117. self.state_dim = state_dim
  118. self.dtype = dtype
  119. self.prefix = prefix
  120. self.kv_cache = torch.tensor([])
  121. compilation_config = get_current_vllm_config().compilation_config
  122. if prefix in compilation_config.static_forward_context:
  123. raise ValueError(f"Duplicate layer name: {prefix}")
  124. compilation_config.static_forward_context[prefix] = self
  125. assert self.dtype == torch.float32
  126. # Drafts + bonus token + the previous row, as a power of two >= 8.
  127. rows_per_step = get_current_vllm_config().num_speculative_tokens + 2
  128. self.block_size = max(8, 1 << (rows_per_step - 1).bit_length())
  129. def bind_kv_cache(self, kv_cache: torch.Tensor) -> None:
  130. # [B, H=1, N, C] -> [B, N, C]
  131. self.kv_cache = kv_cache.squeeze(1)
  132. def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
  133. del vllm_config
  134. return CircularBufferSpec(
  135. block_size=self.block_size,
  136. num_kv_heads=1,
  137. head_size=self.state_dim,
  138. head_size_v=0,
  139. dtype=self.dtype,
  140. # Every DCP rank compresses every token: the ring is replicated.
  141. dcp_replicated=True,
  142. )
  143. def forward(self): ...
  144. def get_attn_backend(self) -> type[AttentionBackend]:
  145. return CompressorBackend
  146. class DeepseekCompressor(nn.Module):
  147. """DeepSeek V4.1 KV/score compressor.
  148. Pools ``compress_ratio`` consecutive tokens into one KV latent with a
  149. learned softmax gate (ratio 1 has no gate and no pooling). Owns the
  150. linear, norm and state cache. State saving, compression and RMSNorm share
  151. one Triton kernel. The emitted latent feeds independent main-cache and
  152. indexer K kernels, which the attention layer can schedule concurrently.
  153. """
  154. def __init__(
  155. self,
  156. vllm_config: VllmConfig,
  157. compress_ratio: int,
  158. hidden_size: int,
  159. head_dim: int,
  160. rotate: bool = False,
  161. prefix: str = "",
  162. k_cache_prefix="",
  163. ):
  164. super().__init__()
  165. if compress_ratio not in (1, 2):
  166. raise NotImplementedError(
  167. "DeepSeek V4.1 compressor supports compress_ratio 1 (full-length "
  168. f"compressed cache) and 2; got {compress_ratio}. The ratio-4/128 "
  169. "CuTe-DSL kernels are v4.0-specific and not wired here."
  170. )
  171. self.compress_ratio = compress_ratio
  172. self.hidden_size = hidden_size
  173. self.head_dim = head_dim
  174. self.rotate = rotate
  175. self.prefix = prefix
  176. self.k_cache_prefix = k_cache_prefix
  177. # Ratio 1 pools single tokens, so the checkpoint carries no gate.
  178. self.has_gate = compress_ratio > 1
  179. config = vllm_config.model_config.hf_config
  180. self.rope_head_dim = config.qk_rope_head_dim
  181. self.nope_head_dim = self.head_dim - self.rope_head_dim
  182. assert self.head_dim == 512 and self.rope_head_dim == 64
  183. self.rms_norm_eps = config.rms_norm_eps
  184. self.device = current_platform.device_type
  185. self.max_num_reqs = vllm_config.scheduler_config.max_num_seqs
  186. self.max_model_len = vllm_config.model_config.max_model_len
  187. wkv_wgate_sizes = (
  188. [self.head_dim, self.head_dim] if self.has_gate else [self.head_dim]
  189. )
  190. self.fused_wkv_wgate = MergedColumnParallelLinear(
  191. self.hidden_size,
  192. wkv_wgate_sizes,
  193. bias=False,
  194. return_bias=False,
  195. quant_config=None,
  196. disable_tp=True,
  197. prefix=f"{prefix}.fused_wkv_wgate",
  198. )
  199. self.norm = RMSNorm(self.head_dim, self.rms_norm_eps)
  200. self.state_cache = (
  201. CompressorStateCache(
  202. state_dim=2 * self.head_dim, # kv_state + score_state
  203. dtype=torch.float32,
  204. prefix=f"{prefix}.state_cache",
  205. )
  206. if compress_ratio > 1
  207. else None
  208. )
  209. # Save reference to static_forward_context for forward-time KV cache lookup.
  210. # get_current_vllm_config() is only available during __init__, not forward.
  211. self._static_forward_context = (
  212. vllm_config.compilation_config.static_forward_context
  213. )
  214. def forward(
  215. self,
  216. # [num_tokens, (2 if has_gate else 1) * self.head_dim]
  217. kv_score: torch.Tensor,
  218. # [num_tokens]
  219. positions: torch.Tensor,
  220. ) -> torch.Tensor | None:
  221. """Save states and return the BF16 latent for cache insertion and indexing.
  222. Only valid group-boundary rows are written.
  223. """
  224. attn_metadata = get_forward_context().attn_metadata
  225. if not isinstance(attn_metadata, dict):
  226. return None
  227. if self.state_cache is None:
  228. state_cache = query_start_loc = token_to_req_indices = None
  229. slot_mapping = cast(Any, attn_metadata[self.k_cache_prefix]).slot_mapping
  230. else:
  231. state_metadata = cast(
  232. CompressorMetadata, attn_metadata[self.state_cache.prefix]
  233. )
  234. state_cache = self.state_cache.kv_cache
  235. slot_mapping = state_metadata.slot_mapping
  236. query_start_loc = state_metadata.query_start_loc
  237. token_to_req_indices = state_metadata.token_to_req_indices
  238. latent = torch.empty(
  239. kv_score.shape[0],
  240. self.head_dim,
  241. dtype=torch.bfloat16,
  242. device=kv_score.device,
  243. )
  244. fused_save_compress_norm(
  245. kv_score,
  246. positions,
  247. state_cache,
  248. slot_mapping,
  249. query_start_loc,
  250. token_to_req_indices,
  251. self.norm.weight,
  252. self.rms_norm_eps,
  253. self.compress_ratio,
  254. latent,
  255. )
  256. return latent
  257. def insert_cache(
  258. self,
  259. latent: torch.Tensor | None,
  260. positions: torch.Tensor,
  261. rotary_emb,
  262. ) -> None:
  263. """Publish compressed main-cache rows after the latent becomes ready."""
  264. if latent is None:
  265. return
  266. attn_metadata = get_forward_context().attn_metadata
  267. assert isinstance(attn_metadata, dict)
  268. k_cache_metadata = cast(Any, attn_metadata[self.k_cache_prefix])
  269. k_cache_layer = self._static_forward_context[self.k_cache_prefix]
  270. kv_cache = k_cache_layer.kv_cache
  271. # Plain-row per-tensor fp8 caches (FlashInfer) carry the layer's scale;
  272. # fp8_ds_mla and bf16 rows need none.
  273. fp8_scale = (
  274. getattr(k_cache_layer, "_flashinfer_fp8_kv_scale", None)
  275. if kv_cache.dtype == torch.float8_e4m3fn
  276. else None
  277. )
  278. rope_quant_insert(
  279. latent,
  280. positions,
  281. rotary_emb.cos_sin_cache,
  282. kv_cache,
  283. k_cache_metadata.slot_mapping,
  284. self.compress_ratio,
  285. fp8_scale=fp8_scale,
  286. )