compressor.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. )
  141. def forward(self): ...
  142. def get_attn_backend(self) -> type[AttentionBackend]:
  143. return CompressorBackend
  144. class DeepseekCompressor(nn.Module):
  145. """DeepSeek V4.1 KV/score compressor.
  146. Pools ``compress_ratio`` consecutive tokens into one KV latent with a
  147. learned softmax gate (ratio 1 has no gate and no pooling). Owns the
  148. linear, norm and state cache. State saving, compression and RMSNorm share
  149. one Triton kernel. The emitted latent feeds independent main-cache and
  150. indexer K kernels, which the attention layer can schedule concurrently.
  151. """
  152. def __init__(
  153. self,
  154. vllm_config: VllmConfig,
  155. compress_ratio: int,
  156. hidden_size: int,
  157. head_dim: int,
  158. rotate: bool = False,
  159. prefix: str = "",
  160. k_cache_prefix="",
  161. ):
  162. super().__init__()
  163. if compress_ratio not in (1, 2):
  164. raise NotImplementedError(
  165. "DeepSeek V4.1 compressor supports compress_ratio 1 (full-length "
  166. f"compressed cache) and 2; got {compress_ratio}. The ratio-4/128 "
  167. "CuTe-DSL kernels are v4.0-specific and not wired here."
  168. )
  169. self.compress_ratio = compress_ratio
  170. self.hidden_size = hidden_size
  171. self.head_dim = head_dim
  172. self.rotate = rotate
  173. self.prefix = prefix
  174. self.k_cache_prefix = k_cache_prefix
  175. # Ratio 1 pools single tokens, so the checkpoint carries no gate.
  176. self.has_gate = compress_ratio > 1
  177. config = vllm_config.model_config.hf_config
  178. self.rope_head_dim = config.qk_rope_head_dim
  179. self.nope_head_dim = self.head_dim - self.rope_head_dim
  180. assert self.head_dim == 512 and self.rope_head_dim == 64
  181. self.rms_norm_eps = config.rms_norm_eps
  182. self.device = current_platform.device_type
  183. self.max_num_reqs = vllm_config.scheduler_config.max_num_seqs
  184. self.max_model_len = vllm_config.model_config.max_model_len
  185. wkv_wgate_sizes = (
  186. [self.head_dim, self.head_dim] if self.has_gate else [self.head_dim]
  187. )
  188. self.fused_wkv_wgate = MergedColumnParallelLinear(
  189. self.hidden_size,
  190. wkv_wgate_sizes,
  191. bias=False,
  192. return_bias=False,
  193. quant_config=None,
  194. disable_tp=True,
  195. prefix=f"{prefix}.fused_wkv_wgate",
  196. )
  197. self.norm = RMSNorm(self.head_dim, self.rms_norm_eps)
  198. self.state_cache = (
  199. CompressorStateCache(
  200. state_dim=2 * self.head_dim, # kv_state + score_state
  201. dtype=torch.float32,
  202. prefix=f"{prefix}.state_cache",
  203. )
  204. if compress_ratio > 1
  205. else None
  206. )
  207. # Save reference to static_forward_context for forward-time KV cache lookup.
  208. # get_current_vllm_config() is only available during __init__, not forward.
  209. self._static_forward_context = (
  210. vllm_config.compilation_config.static_forward_context
  211. )
  212. def forward(
  213. self,
  214. # [num_tokens, (2 if has_gate else 1) * self.head_dim]
  215. kv_score: torch.Tensor,
  216. # [num_tokens]
  217. positions: torch.Tensor,
  218. ) -> torch.Tensor | None:
  219. """Save states and return the BF16 latent for cache insertion and indexing.
  220. Only valid group-boundary rows are written.
  221. """
  222. attn_metadata = get_forward_context().attn_metadata
  223. if not isinstance(attn_metadata, dict):
  224. return None
  225. if self.state_cache is None:
  226. state_cache = query_start_loc = token_to_req_indices = None
  227. slot_mapping = cast(Any, attn_metadata[self.k_cache_prefix]).slot_mapping
  228. else:
  229. state_metadata = cast(
  230. CompressorMetadata, attn_metadata[self.state_cache.prefix]
  231. )
  232. state_cache = self.state_cache.kv_cache
  233. slot_mapping = state_metadata.slot_mapping
  234. query_start_loc = state_metadata.query_start_loc
  235. token_to_req_indices = state_metadata.token_to_req_indices
  236. latent = torch.empty(
  237. kv_score.shape[0],
  238. self.head_dim,
  239. dtype=torch.bfloat16,
  240. device=kv_score.device,
  241. )
  242. fused_save_compress_norm(
  243. kv_score,
  244. positions,
  245. state_cache,
  246. slot_mapping,
  247. query_start_loc,
  248. token_to_req_indices,
  249. self.norm.weight,
  250. self.rms_norm_eps,
  251. self.compress_ratio,
  252. latent,
  253. )
  254. return latent
  255. def insert_cache(
  256. self,
  257. latent: torch.Tensor | None,
  258. positions: torch.Tensor,
  259. rotary_emb,
  260. ) -> None:
  261. """Publish compressed main-cache rows after the latent becomes ready."""
  262. if latent is None:
  263. return
  264. attn_metadata = get_forward_context().attn_metadata
  265. assert isinstance(attn_metadata, dict)
  266. k_cache_metadata = cast(Any, attn_metadata[self.k_cache_prefix])
  267. k_cache_layer = self._static_forward_context[self.k_cache_prefix]
  268. kv_cache = k_cache_layer.kv_cache
  269. # Plain-row per-tensor fp8 caches (FlashInfer) carry the layer's scale;
  270. # fp8_ds_mla and bf16 rows need none.
  271. fp8_scale = (
  272. getattr(k_cache_layer, "_flashinfer_fp8_kv_scale", None)
  273. if kv_cache.dtype == torch.float8_e4m3fn
  274. else None
  275. )
  276. rope_quant_insert(
  277. latent,
  278. positions,
  279. rotary_emb.cos_sin_cache,
  280. kv_cache,
  281. k_cache_metadata.slot_mapping,
  282. self.compress_ratio,
  283. fp8_scale=fp8_scale,
  284. )