main
py 359 lines 14.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from typing import TYPE_CHECKING, ClassVar, cast
5
6 import torch
7
8 from vllm.forward_context import get_forward_context
9 from vllm.models.deepseek_v4.nvidia.ops.o_proj import compute_fp8_einsum_recipe
10 from vllm.models.deepseek_v41.attention import DeepseekV4Attention
11 from vllm.models.deepseek_v41.common.ops import (
12 combine_topk_swa_indices,
13 compute_global_topk_indices_and_lens,
14 dequantize_and_gather_k_cache,
15 )
16 from vllm.models.deepseek_v41.nvidia.ops.o_proj import (
17 dsv41_o_proj,
18 register_dsv41_o_proj_warmup,
19 )
20 from vllm.models.deepseek_v41.sparse_mla import (
21 DeepseekV4FlashMLABackend,
22 DeepseekV4FlashMLAMetadata,
23 DeepseekV41SparseSWAMetadataBuilder,
24 )
25 from vllm.utils.math_utils import round_up
26 from vllm.v1.attention.backend import AttentionCGSupport
27 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWABackend
28 from vllm.v1.attention.ops.flashmla import (
29 flash_mla_sparse_fwd,
30 flash_mla_with_kvcache,
31 )
32 from vllm.v1.worker.workspace import current_workspace_manager
33
34 if TYPE_CHECKING:
35 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
36
37
38 class DeepseekSparseSWAFlashMLAMetadataBuilder(DeepseekV41SparseSWAMetadataBuilder):
39 """SWA metadata for the FlashMLA decode path, which allows varlen decode."""
40
41 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.ALWAYS
42
43
44 class DeepseekSparseSWAFlashMLABackend(DeepseekSparseSWABackend):
45 @staticmethod
46 def get_builder_cls() -> type[DeepseekSparseSWAFlashMLAMetadataBuilder]:
47 return DeepseekSparseSWAFlashMLAMetadataBuilder
48
49
50 class DeepseekV4FlashMLAAttention(DeepseekV4Attention):
51 """FlashMLA sparse MLA attention layer for DeepSeek V4.1 (CUDA)."""
52
53 backend_cls = DeepseekV4FlashMLABackend
54 swa_backend_cls = DeepseekSparseSWAFlashMLABackend
55
56 def __init__(self, *args, **kwargs) -> None:
57 super().__init__(*args, **kwargs)
58 self._einsum_recipe, self._tma_aligned_scales = compute_fp8_einsum_recipe(
59 self._o_proj_block_size
60 )
61 register_dsv41_o_proj_warmup(self)
62
63 def _o_proj(self, attn_out: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
64 return dsv41_o_proj(self, attn_out, positions)
65
66 @classmethod
67 def get_padded_num_q_heads(cls, num_heads: int) -> int:
68 # FP8 decode kernel only supports h_q = 64 or 128.
69 if num_heads > 128:
70 raise ValueError(
71 f"DeepseekV4 FlashMLA does not support {num_heads} heads "
72 "(FP8 decode kernel requires h_q in {64, 128})."
73 )
74 return 64 if num_heads <= 64 else 128
75
76 def forward_mqa(
77 self,
78 q: torch.Tensor,
79 kv: torch.Tensor,
80 positions: torch.Tensor,
81 output: torch.Tensor,
82 ) -> None:
83 assert output.shape == q.shape, (
84 f"output buffer shape {output.shape} must match q shape {q.shape}"
85 )
86 assert output.dtype == q.dtype, (
87 f"output buffer dtype {output.dtype} must match q dtype {q.dtype}"
88 )
89
90 # Get SWA and indexer metadata from forward context
91 forward_context = get_forward_context()
92 attn_metadata = forward_context.attn_metadata
93
94 if attn_metadata is None:
95 # Warmup dummy run: no real metadata. Reserve the same bf16
96 # gather workspace _forward_prefill would; the dequantize / topk
97 # / sparse_fwd kernels are skipped this step.
98 swa_only = self.compress_ratio == 0
99 N = (
100 0
101 if swa_only
102 else (self.max_model_len + self.compress_ratio - 1)
103 // self.compress_ratio
104 )
105 M = N + self.window_size + self.max_num_batched_tokens
106 if swa_only:
107 top_k = 0
108 else:
109 assert self.topk_indices_buffer is not None
110 top_k = self.topk_indices_buffer.shape[-1]
111 combined_topk = round_up(top_k + self.window_size, 128)
112 current_workspace_manager().get_simultaneous(
113 ((self.PREFILL_CHUNK_SIZE, M, q.shape[-1]), torch.bfloat16),
114 ((self.max_num_batched_tokens, combined_topk), torch.int32),
115 ((self.max_num_batched_tokens,), torch.int32),
116 )
117 output.zero_()
118 return
119
120 assert isinstance(attn_metadata, dict)
121 # Compressed-cache metadata lives on the kv-source layer's prefix;
122 # consumers share that cache and its block table.
123 flashmla_metadata = cast(
124 DeepseekV4FlashMLAMetadata | None,
125 attn_metadata.get(self.compressed_cache_prefix)
126 if self.compressed_cache_prefix is not None
127 else None,
128 )
129 swa_metadata = cast(
130 "DeepseekSparseSWAMetadata | None",
131 attn_metadata.get(self.swa_cache_layer.prefix),
132 )
133 assert swa_metadata is not None
134
135 swa_only = self.compress_ratio == 0
136 # SWA-only layers (compress_ratio == 0) don't have their own KV cache
137 # allocation; consumers read the kv source's cache, which is bound on
138 # the source layer.
139 self_kv_cache = None if swa_only else self._compressed_kv_cache()
140 swa_kv_cache = self.swa_cache_layer.kv_cache
141
142 # Split prefill and decode
143 num_decodes = swa_metadata.num_decodes
144 num_prefills = swa_metadata.num_prefills
145 num_decode_tokens = swa_metadata.num_decode_tokens
146
147 if num_prefills > 0:
148 self._forward_prefill(
149 q=q[num_decode_tokens:],
150 positions=positions[num_decode_tokens:],
151 compressed_k_cache=self_kv_cache,
152 swa_k_cache=swa_kv_cache,
153 output=output[num_decode_tokens:],
154 attn_metadata=flashmla_metadata,
155 swa_metadata=swa_metadata,
156 )
157 if num_decodes > 0:
158 self._forward_decode(
159 q=q[:num_decode_tokens],
160 kv_cache=self_kv_cache,
161 swa_metadata=swa_metadata,
162 attn_metadata=flashmla_metadata,
163 swa_only=swa_only,
164 output=output[:num_decode_tokens],
165 )
166
167 def _forward_decode(
168 self,
169 q: torch.Tensor,
170 kv_cache: torch.Tensor | None, # None for SWA-only layers
171 swa_metadata: "DeepseekSparseSWAMetadata",
172 attn_metadata: DeepseekV4FlashMLAMetadata | None,
173 swa_only: bool,
174 output: torch.Tensor,
175 ) -> None:
176 num_decodes = swa_metadata.num_decodes
177 num_decode_tokens = swa_metadata.num_decode_tokens
178
179 topk_indices = None
180 topk_lens = None
181 if not swa_only:
182 # Local indices filled by the index-source layer's indexer.
183 assert attn_metadata is not None
184 assert swa_metadata.is_valid_token is not None
185 assert self.topk_indices_buffer is not None
186 block_size = attn_metadata.block_size // self.compress_ratio
187 is_valid = swa_metadata.is_valid_token[:num_decode_tokens]
188 global_indices, topk_lens = compute_global_topk_indices_and_lens(
189 self.topk_indices_buffer[:num_decode_tokens],
190 swa_metadata.token_to_req_indices,
191 attn_metadata.block_table[:num_decodes],
192 block_size,
193 is_valid,
194 )
195 topk_indices = global_indices.view(num_decode_tokens, 1, -1)
196
197 swa_indices = swa_metadata.decode_swa_indices
198 swa_lens = swa_metadata.decode_swa_lens
199
200 # We treat queries in the same seq as different queries
201 # and later we only attend by generated indices.
202 # q arrives pre-padded to self.padded_heads by the outer wrapper.
203 q = q.unsqueeze(1)
204
205 # Prepare SWA cache (num_blocks, swa_block_size, 1, head_bytes)
206 # Use unsqueeze to preserve strides (handles padded blocks correctly)
207 swa_cache = self.swa_cache_layer.kv_cache.unsqueeze(-2)
208 # Reshape KV cache to (num_blocks, block_size, 1, head_bytes)
209 if kv_cache is not None:
210 kv_cache = kv_cache.unsqueeze(-2)
211
212 # One FlashMLASchedMeta per layer type, shared across all same-type
213 # layers within this decode step. The first forward call per type
214 # triggers the in-kernel planner (allocating tile_scheduler_metadata
215 # and num_splits via PyTorch's graph-aware allocator so CUDA graph
216 # capture reuses the same addresses on replay); subsequent same-type
217 # layers see have_initialized=True and skip the planner.
218 tile_metadata = {
219 0: swa_metadata.tile_sched_swaonly,
220 1: swa_metadata.tile_sched_c1a,
221 2: swa_metadata.tile_sched_c2a,
222 }[self.compress_ratio]
223 assert tile_metadata is not None, (
224 "swa_metadata missing tile_sched entry for "
225 f"compress_ratio={self.compress_ratio}; "
226 "DeepseekSparseSWAMetadataBuilder.build_tile_scheduler did not "
227 "allocate one for this layer type."
228 )
229
230 out, _ = flash_mla_with_kvcache(
231 q=q,
232 k_cache=swa_cache,
233 block_table=None,
234 head_dim_v=512,
235 tile_scheduler_metadata=tile_metadata,
236 cache_seqlens=None,
237 is_fp8_kvcache=True,
238 indices=swa_indices,
239 topk_length=swa_lens,
240 softmax_scale=self.scale,
241 attn_sink=self.attn_sink,
242 extra_k_cache=kv_cache if not swa_only else None,
243 extra_indices_in_kvcache=topk_indices,
244 extra_topk_length=topk_lens,
245 out=output.unsqueeze(1),
246 )
247
248 def _forward_prefill(
249 self,
250 q: torch.Tensor,
251 positions: torch.Tensor,
252 compressed_k_cache: torch.Tensor | None, # None for SWA-only layers
253 swa_k_cache: torch.Tensor,
254 output: torch.Tensor,
255 attn_metadata: DeepseekV4FlashMLAMetadata | None,
256 swa_metadata: "DeepseekSparseSWAMetadata",
257 ) -> None:
258 swa_only = self.compress_ratio == 0
259
260 num_prefill_tokens = swa_metadata.num_prefill_tokens
261 num_decodes = swa_metadata.num_decodes
262 num_decode_tokens = swa_metadata.num_decode_tokens
263
264 # Use pre-computed prefill metadata.
265 seq_lens = swa_metadata.prefill_seq_lens
266 gather_lens = swa_metadata.prefill_gather_lens
267 assert seq_lens is not None
268 assert gather_lens is not None
269
270 # Derive prefill-local token offsets from the full query_start_loc_cpu.
271 query_start_loc_cpu = swa_metadata.query_start_loc_cpu
272 query_start_loc = swa_metadata.query_start_loc
273 assert query_start_loc_cpu is not None
274 assert query_start_loc is not None
275 prefill_token_base = query_start_loc_cpu[num_decodes]
276
277 # Local indices filled by the index source; SWA-only layers pass
278 # top_k=0 and never read them.
279 assert self.topk_indices_buffer is not None
280 topk_indices = self.topk_indices_buffer[num_decode_tokens:]
281 topk_indices = topk_indices[:num_prefill_tokens]
282 top_k = 0 if swa_only else topk_indices.shape[-1]
283 chunk_plan = swa_metadata.get_prefill_chunk_plan(
284 compress_ratio=self.compress_ratio,
285 prefill_chunk_size=self.PREFILL_CHUNK_SIZE,
286 # v4.1: every cr>0 layer (including cr==1) gathers a full-length
287 # compressed region; only cr==0 layers are SWA-only.
288 has_compressed=not swa_only,
289 )
290 assert chunk_plan, "prefill chunk plan must be non-empty when num_prefills > 0"
291 workspace_manager = current_workspace_manager()
292 combined_topk = round_up(top_k + self.window_size, 128)
293 for chunk_start, chunk_end, chunk_N, chunk_M in chunk_plan:
294 chunk_size = chunk_end - chunk_start
295 workspace = workspace_manager.get_simultaneous(
296 ((chunk_size, chunk_M, q.shape[-1]), torch.bfloat16),
297 ((self.max_num_batched_tokens, combined_topk), torch.int32),
298 ((self.max_num_batched_tokens,), torch.int32),
299 )
300 kv, combined_indices_out, combined_lens_out = workspace
301 if not swa_only:
302 # Gather compressed KV
303 assert attn_metadata is not None
304 block_table = attn_metadata.block_table[num_decodes:]
305 dequantize_and_gather_k_cache(
306 kv[:chunk_size],
307 compressed_k_cache,
308 seq_lens=seq_lens[chunk_start:chunk_end] // self.compress_ratio,
309 gather_lens=None,
310 block_table=block_table[chunk_start:chunk_end],
311 block_size=attn_metadata.block_size // self.compress_ratio,
312 offset=0,
313 )
314
315 # Gather SWA KV
316 swa_block_table = swa_metadata.block_table[num_decodes:]
317 dequantize_and_gather_k_cache(
318 kv[:chunk_size],
319 swa_k_cache,
320 seq_lens=seq_lens[chunk_start:chunk_end],
321 gather_lens=gather_lens[chunk_start:chunk_end],
322 block_table=swa_block_table[chunk_start:chunk_end],
323 block_size=swa_metadata.block_size,
324 offset=chunk_N,
325 )
326
327 # Combine the topk indices and SWA indices for gathered KV cache
328 query_start = (
329 query_start_loc_cpu[num_decodes + chunk_start] - prefill_token_base
330 )
331 query_end = (
332 query_start_loc_cpu[num_decodes + chunk_end] - prefill_token_base
333 )
334 combined_indices_out = combined_indices_out[: query_end - query_start]
335 combined_lens_out = combined_lens_out[: query_end - query_start]
336
337 combined_indices, combined_lens = combine_topk_swa_indices(
338 topk_indices[query_start:query_end],
339 query_start_loc[
340 num_decodes + chunk_start : num_decodes + chunk_end + 1
341 ],
342 seq_lens[chunk_start:chunk_end],
343 gather_lens[chunk_start:chunk_end],
344 self.window_size,
345 self.compress_ratio,
346 top_k,
347 chunk_M,
348 chunk_N,
349 out=(combined_indices_out, combined_lens_out),
350 )
351 flash_mla_sparse_fwd(
352 q=q[query_start:query_end],
353 kv=kv.view(-1, 1, q.shape[-1]),
354 indices=combined_indices.unsqueeze(1),
355 sm_scale=self.scale,
356 attn_sink=self.attn_sink,
357 topk_length=combined_lens,
358 out=output[query_start:query_end],
359 )