main
py 649 lines 24.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """CPU DeepSeek-V4 attention subclass.
4
5 ``forward_mqa`` resolves SWA/compressed top-k indices to paged-cache slot
6 ids and calls the fused ``flash_mla_with_kvcache_cpu`` kernel. ``_o_proj``
7 stays eager -- not on the attention hot path.
8 """
9
10 from typing import Any, cast
11
12 import torch
13 import torch.nn as nn
14 import torch.nn.functional as F
15
16 from vllm import _custom_ops as ops
17 from vllm.forward_context import get_forward_context
18 from vllm.models.deepseek_v4.attention import (
19 DeepseekV4Attention,
20 DeepseekV4Indexer,
21 DeepseekV4IndexerCache,
22 )
23 from vllm.models.deepseek_v4.cpu.cpu_compressor import DeepseekV4CPUCompressor
24 from vllm.models.deepseek_v4.cpu.cpu_mla import (
25 DeepseekV4CPUIndexerBackend,
26 DeepseekV4CPUSparseBackend,
27 )
28 from vllm.models.deepseek_v4.cpu.cpu_utils import map_local_to_global_slots_cpu
29 from vllm.models.deepseek_v4.sparse_mla import DeepseekV4FlashMLAMetadata
30 from vllm.platforms import current_platform
31 from vllm.utils.torch_utils import (
32 LayerNameType,
33 _encode_layer_name,
34 _resolve_layer_name,
35 direct_register_custom_op,
36 )
37 from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
38 from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
39
40 _ROPE_DIM = 64
41
42
43 def _deepseek_v4_cpu_prepare_and_attn(
44 hidden_states: torch.Tensor,
45 qr: torch.Tensor,
46 kv: torch.Tensor,
47 qr_scale: torch.Tensor | None,
48 kv_score: torch.Tensor | None,
49 indexer_kv_score: torch.Tensor | None,
50 indexer_weights: torch.Tensor | None,
51 positions: torch.Tensor,
52 o_padded: torch.Tensor,
53 layer_name: LayerNameType,
54 ) -> torch.Tensor:
55 """Opaque custom-op boundary around attention-prep + sparse-indexer +
56 MLA attention + output projection -- CPU-only.
57
58 Wraps cache writes with no explicit tensor args (SWA/compressor/indexer
59 state), which torch.compile can't otherwise track as mutations -- one
60 opaque call sidesteps that. ``_o_proj`` is folded in too because CPU's
61 ``DYNAMO_TRACE_ONCE`` mode traces exactly once, during warmup when
62 ``attn_metadata`` is ``None``; tracing ``_o_proj`` separately would bake
63 that warmup branch into the compiled graph permanently, dead-code-
64 eliminating the whole attention computation upstream.
65 """
66 layer_name_str = _resolve_layer_name(layer_name)
67 self = get_forward_context().no_compile_layers[layer_name_str]
68 self._prepare_and_attn_fn(
69 hidden_states,
70 qr,
71 kv,
72 qr_scale,
73 kv_score,
74 indexer_kv_score,
75 indexer_weights,
76 positions,
77 o_padded,
78 )
79 o = o_padded[:, : self.n_local_heads, :]
80 return self._o_proj(o, positions)
81
82
83 def _deepseek_v4_cpu_prepare_and_attn_fake(
84 hidden_states: torch.Tensor,
85 qr: torch.Tensor,
86 kv: torch.Tensor,
87 qr_scale: torch.Tensor | None,
88 kv_score: torch.Tensor | None,
89 indexer_kv_score: torch.Tensor | None,
90 indexer_weights: torch.Tensor | None,
91 positions: torch.Tensor,
92 o_padded: torch.Tensor,
93 layer_name: LayerNameType,
94 ) -> torch.Tensor:
95 # hidden_states' last dim already equals _o_proj's output width, so we
96 # don't need to resolve layer_name (an opaque object) here.
97 return hidden_states.new_empty((hidden_states.shape[0], hidden_states.shape[-1]))
98
99
100 direct_register_custom_op(
101 op_name="deepseek_v4_cpu_prepare_and_attn",
102 op_func=_deepseek_v4_cpu_prepare_and_attn,
103 mutates_args=["o_padded"],
104 fake_impl=_deepseek_v4_cpu_prepare_and_attn_fake,
105 )
106
107
108 def _dequant_linear_weight(layer: torch.nn.Module) -> torch.Tensor:
109 """Dequantize a linear layer's weight, including FP8 block scales.
110
111 ``_o_proj`` reads ``wo_a.weight`` directly (not via
112 ``quant_method.apply()``), so the per-block FP8 scale must be applied
113 here explicitly.
114 """
115 weight = layer.weight
116 scale = getattr(layer, "weight_scale_inv", None)
117 if scale is None:
118 scale = getattr(layer, "weight_scale", None)
119 if scale is None:
120 return weight.float()
121
122 if scale.dtype == torch.float8_e8m0fnu:
123 from vllm.model_executor.layers.quantization.utils.fp8_utils import (
124 _upcast_e8m0_to_fp32,
125 )
126
127 scale = _upcast_e8m0_to_fp32(scale)
128 scale = scale.to(torch.float32)
129
130 if scale.numel() == 1:
131 return weight.float() * scale
132
133 block_size = getattr(layer, "weight_block_size", None)
134 if block_size is None:
135 # Per-channel scale: one row-scale per output row.
136 return weight.float() * scale.view(-1, 1)
137
138 block_m, block_k = block_size
139 m, k = weight.shape
140 scale = scale.repeat_interleave(block_m, dim=0).repeat_interleave(block_k, dim=1)
141 return weight.float() * scale[:m, :k]
142
143
144 def _fused_indexer_q_rope_quant_cpu(
145 positions: torch.Tensor,
146 index_q: torch.Tensor,
147 index_q_cos_sin_cache: torch.Tensor,
148 index_weights: torch.Tensor,
149 index_weights_softmax_scale: float,
150 index_weights_head_scale: float,
151 ) -> tuple[torch.Tensor, torch.Tensor]:
152 """CPU-only equivalent of ``fused_indexer_q_rope_quant``'s FP8 path
153 (CPU never reaches its MXFP4 arm -- Blackwell-only). Returns
154 ``(q_fp8, weights_out)``, folding the per-token q scale into
155 ``index_weights``. ``positions``/``index_q_cos_sin_cache`` are already
156 contiguous int64/fp32, so no conversion is needed here.
157 """
158 assert positions.ndim == 1
159 assert index_q.ndim == 3
160 assert index_q_cos_sin_cache.ndim == 2
161
162 index_weights_out = torch.empty_like(index_weights, dtype=torch.float32)
163 index_q_fp8 = torch.empty_like(index_q, dtype=current_platform.fp8_dtype())
164 ops.fused_indexer_q_rope_quant_cpu(
165 positions,
166 index_q.to(torch.float32),
167 index_q_cos_sin_cache,
168 index_q_fp8,
169 index_weights.to(torch.float32),
170 index_weights_softmax_scale,
171 index_weights_head_scale,
172 index_weights_out,
173 )
174 return index_q_fp8, index_weights_out
175
176
177 class _NoOpEvent:
178 """Stand-in for ``torch.cuda.Event`` on CPU: since CPU never sets an aux
179 stream list, these events are constructed but never actually used."""
180
181
182 class DeepseekV4CPUIndexerCache(DeepseekV4IndexerCache):
183 """CPU indexer K-cache descriptor: same fields as the shared base, just
184 pointing ``get_attn_backend()`` at ``DeepseekV4CPUIndexerBackend``
185 instead of the shared, CUDA/XPU-oriented ``DeepseekV4IndexerBackend``."""
186
187 def get_attn_backend(self) -> type[AttentionBackend]:
188 return DeepseekV4CPUIndexerBackend
189
190
191 class DeepseekV4CPUIndexer(DeepseekV4Indexer):
192 """CPU indexer: the C4A short-context fallback runs as eager PyTorch
193 instead of Triton. Never constructed directly -- ``__class__`` is
194 swapped onto an existing ``DeepseekV4Indexer`` instance post-construction.
195 """
196
197 def forward(
198 self,
199 hidden_states: torch.Tensor,
200 qr: torch.Tensor,
201 compressed_kv_score: torch.Tensor | None,
202 indexer_weights: torch.Tensor,
203 positions: torch.Tensor,
204 rotary_emb: nn.Module,
205 qr_scale: torch.Tensor | None = None,
206 skip_compressor: bool = False,
207 ) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
208 """CPU override: no aux streams, so wq_b_and_q_quant and the
209 compressor run straight-line instead of through
210 ``maybe_execute_in_parallel``; the short-context check drops the
211 CUDA-only ``is_current_stream_capturing()`` guard (always False
212 here)."""
213 compressor = self.compressor
214
215 attn_metadata = get_forward_context().attn_metadata
216 if isinstance(attn_metadata, dict):
217 indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
218 if indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens:
219 # Fewer candidates than topk: all are selected, but the K
220 # cache still needs to be built.
221 compressor(compressed_kv_score, positions, rotary_emb)
222 assert self.topk_indices_buffer is not None
223 num_tokens = (
224 indexer_metadata.num_decode_tokens
225 + indexer_metadata.num_prefill_tokens
226 )
227 if num_tokens > 0:
228 self._fill_short_context_topk(positions, num_tokens)
229 return None, None, None
230
231 def wq_b_and_q_quant():
232 q = self._wq_b_proj(qr, qr_scale)
233 q = q.view(-1, self.n_head, self.head_dim)
234 return _fused_indexer_q_rope_quant_cpu(
235 positions,
236 q,
237 rotary_emb.cos_sin_cache,
238 indexer_weights,
239 self.softmax_scale,
240 self.n_head**-0.5,
241 )
242
243 # Order matches the base class's parallel-join contract: compressor
244 # must complete before indexer_op runs (skip_k_cache_insert=True).
245 q, weights = wq_b_and_q_quant()
246 compressor(compressed_kv_score, positions, rotary_emb)
247 # No q_scale: FP8-only path (MXFP4 needs Blackwell).
248 return q, None, weights
249
250 def _fill_short_context_topk(
251 self, positions: torch.Tensor, num_tokens: int
252 ) -> None:
253 assert self.topk_indices_buffer is not None
254 device = positions.device
255 arange = torch.arange(self.topk_tokens, device=device)
256 num_compressed = torch.div(
257 positions[:num_tokens] + 1, self.compress_ratio, rounding_mode="floor"
258 )
259 valid = arange.unsqueeze(0) < num_compressed.unsqueeze(-1)
260 filled = torch.where(
261 valid, arange.unsqueeze(0), torch.full_like(arange.unsqueeze(0), -1)
262 )
263 self.topk_indices_buffer[:num_tokens, : self.topk_tokens] = filled.to(
264 self.topk_indices_buffer.dtype
265 )
266
267
268 class DeepseekV4CPUAttention(DeepseekV4Attention):
269 """CPU sparse MLA attention layer for DeepSeek V4."""
270
271 backend_cls = DeepseekV4CPUSparseBackend
272
273 def __init__(self, *args, **kwargs) -> None:
274 super().__init__(*args, **kwargs)
275
276 # Swap classes post-construction (no class-swap hook on the shared
277 # base) so forward()/get_attn_backend() dispatch to CPU kernels.
278 # SWA-only layers (compress_ratio <= 1) never build a
279 # compressor/indexer.
280 if self.compressor is not None:
281 self.compressor.__class__ = DeepseekV4CPUCompressor
282 if self.indexer is not None:
283 self.indexer.__class__ = DeepseekV4CPUIndexer
284 self.indexer.k_cache.__class__ = DeepseekV4CPUIndexerCache
285 self.indexer.compressor.__class__ = DeepseekV4CPUCompressor
286
287 self._wo_a_packed: torch.Tensor | None = None
288 self._wrap_wo_a_process_weights_after_loading()
289
290 def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
291 """Cache fp32-contiguous copies of tensors CPU kernels want that way
292 but that arrive bf16 (rotary cos/sin table, compressor RMSNorm
293 weights) -- cast once here instead of per forward call.
294
295 Runs after every quantized layer's own
296 ``process_weights_after_loading`` (see ``is_deferred_attention_layer``);
297 ``wo_a``'s packing must happen *before* that phase instead, so it's a
298 separate monkeypatch in ``__init__``.
299 """
300 self.rotary_emb.cos_sin_cache = self.rotary_emb.cos_sin_cache.to(
301 torch.float32
302 ).contiguous()
303 if self.compressor is not None:
304 self.compressor.cache_norm_weight_fp32()
305 if self.indexer is not None:
306 self.indexer.compressor.cache_norm_weight_fp32()
307
308 def _wrap_wo_a_process_weights_after_loading(self) -> None:
309 """Snapshot and pack ``wo_a``'s weight into a bf16 copy for
310 ``bmm_cpu`` before the FP8 kernel's own
311 ``process_weights_after_loading`` VNNI-repacks it in place for
312 row-major reads ``_o_proj`` never does."""
313 wo_a = self.wo_a
314 quant_method = wo_a.quant_method
315 orig_pwal = quant_method.process_weights_after_loading
316 heads_per_group = self.n_local_heads // self.n_local_groups
317 k_dim = heads_per_group * self.head_dim
318
319 def _capture_and_pack(layer: torch.nn.Module) -> None:
320 dequant = _dequant_linear_weight(layer).to(torch.bfloat16)
321 dequant = dequant.view(self.n_local_groups, self.o_lora_rank, k_dim)
322 self._wo_a_packed = torch.ops._C.convert_weight_packed(dequant.contiguous())
323 orig_pwal(layer)
324 # orig_pwal VNNI-repacks layer.weight/scale for ordinary-linear
325 # use that _o_proj never needs -- free them, same pattern as
326 # dispatch_cpu_unquantized_gemm's remove_weight=True.
327 layer.weight = torch.nn.Parameter(torch.empty(0), requires_grad=False)
328 scale_attr = (
329 "weight_scale_inv"
330 if hasattr(layer, "weight_scale_inv")
331 else "weight_scale"
332 )
333 if hasattr(layer, scale_attr):
334 setattr(
335 layer,
336 scale_attr,
337 torch.nn.Parameter(torch.empty(0), requires_grad=False),
338 )
339
340 quant_method.process_weights_after_loading = ( # type: ignore[method-assign]
341 _capture_and_pack
342 )
343
344 def forward(
345 self,
346 positions: torch.Tensor,
347 hidden_states: torch.Tensor,
348 llama_4_scaling: torch.Tensor | None = None,
349 ) -> torch.Tensor:
350 """CPU override: wraps attention-prep + MLA + output projection in the
351 CPU-only opaque custom op (see ``_deepseek_v4_cpu_prepare_and_attn``).
352 """
353 num_tokens = hidden_states.shape[0]
354 o_padded = torch.empty(
355 (num_tokens, self.padded_heads, self.head_dim),
356 dtype=hidden_states.dtype,
357 device=hidden_states.device,
358 )
359
360 qr_kv = self._fused_wqa_wkv_gemm(hidden_states)
361
362 kv_score: torch.Tensor | None = None
363 if self.compressor is not None:
364 # Must go through the module's own forward (not a raw matmul on
365 # its weight): the packed-GEMM kernel frees the raw weight after
366 # packing, same as wo_a's.
367 kv_score = self.compressor.fused_wkv_wgate(hidden_states).to(torch.float32)
368
369 indexer_weights: torch.Tensor | None = None
370 indexer_kv_score: torch.Tensor | None = None
371 if self.indexer is not None:
372 indexer_weights, _ = self.indexer.weights_proj(hidden_states)
373 indexer_kv_score = self.indexer.compressor.fused_wkv_wgate(
374 hidden_states
375 ).to(torch.float32)
376
377 qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
378
379 return torch.ops.vllm.deepseek_v4_cpu_prepare_and_attn(
380 hidden_states,
381 qr,
382 kv,
383 qr_scale,
384 kv_score,
385 indexer_kv_score,
386 indexer_weights,
387 positions,
388 o_padded,
389 _encode_layer_name(self.prefix),
390 )
391
392 def _fused_qnorm_rope_kv_insert(
393 self,
394 q: torch.Tensor,
395 kv: torch.Tensor,
396 positions: torch.Tensor,
397 attn_metadata: (
398 dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None
399 ),
400 ) -> torch.Tensor:
401 """CPU override: only the fp8_ds_mla (uint8) SWA cache layout is
402 supported here, so the base method's bf16/per-tensor-fp8 branches
403 are dropped."""
404 if not isinstance(attn_metadata, dict):
405 # Profile run: kernel doesn't fire; produce a padded tensor so
406 # downstream FlashMLA gets the right shape.
407 if self.n_local_heads < self.padded_heads:
408 return F.pad(
409 q,
410 (0, 0, 0, self.padded_heads - self.n_local_heads),
411 value=0.0,
412 )
413 return q
414
415 swa_metadata = cast(
416 "DeepseekSparseSWAMetadata | None",
417 attn_metadata.get(self.swa_cache_layer.prefix),
418 )
419 assert swa_metadata is not None
420
421 swa_kv_cache = self.swa_cache_layer.kv_cache
422 # positions is already int64 (the runner's buffer), no cast needed.
423 assert positions.dtype == torch.int64
424 assert swa_kv_cache.dtype == torch.uint8, (
425 "DeepseekV4CPUAttention only supports the fp8_ds_mla cache layout"
426 )
427 # Fused: Q side does per-head weight-free RMSNorm + GPT-J RoPE
428 # (zero-filling padded head slots); KV side does GPT-J RoPE + UE8M0
429 # FP8 quant + paged insert.
430 swa_kv_cache_2d = swa_kv_cache.view(swa_kv_cache.shape[0], -1)
431 return ops.fused_qnorm_rope_kv_insert_cpu(
432 q,
433 kv,
434 positions,
435 swa_kv_cache_2d,
436 swa_metadata.slot_mapping,
437 self.rotary_emb.cos_sin_cache,
438 self.padded_heads,
439 self.eps,
440 swa_metadata.block_size,
441 )
442
443 def _split_qkv_and_norm(
444 self, qr_kv: torch.Tensor
445 ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor]:
446 """CPU override: two ``RMSNorm`` calls instead of the shared
447 ``fused_q_kv_rmsnorm``, whose raw ``@triton.jit`` kernel fails to
448 link under triton-cpu (``undefined symbol: __truncdfbf2``)."""
449 qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
450 return self.q_norm(qr), None, self.kv_norm(kv)
451
452 def _prepare_and_attn(
453 self,
454 hidden_states: torch.Tensor,
455 qr: torch.Tensor,
456 kv: torch.Tensor,
457 qr_scale: torch.Tensor | None,
458 kv_score: torch.Tensor,
459 indexer_kv_score: torch.Tensor,
460 indexer_weights: torch.Tensor,
461 positions: torch.Tensor,
462 o_padded: torch.Tensor,
463 ) -> None:
464 """CPU override: no aux streams, so query-projection+KV-insert, the
465 indexer, and the compressor run straight-line instead of through
466 execute_in_parallel/maybe_execute_in_parallel."""
467 attn_metadata = get_forward_context().attn_metadata
468 indexer = self.indexer
469 compressor = self.compressor
470
471 q = self._wq_b_proj(qr, qr_scale).view(-1, self.n_local_heads, self.head_dim)
472 q = self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
473
474 index_q: torch.Tensor | None = None
475 index_q_scale: torch.Tensor | None = None
476 index_weights_out: torch.Tensor | None = None
477 if indexer is not None:
478 assert compressor is not None
479 index_q, index_q_scale, index_weights_out = indexer(
480 hidden_states,
481 qr,
482 indexer_kv_score,
483 indexer_weights,
484 positions,
485 self.indexer_rotary_emb,
486 qr_scale,
487 )
488 compressor(kv_score, positions, self.rotary_emb)
489 elif compressor is not None:
490 compressor(kv_score, positions, self.rotary_emb)
491
492 self._sparse_indexer_and_attn(
493 hidden_states,
494 index_q,
495 index_q_scale,
496 index_weights_out,
497 q,
498 kv,
499 positions,
500 o_padded,
501 )
502
503 def _sparse_indexer_and_attn(
504 self,
505 hidden_states: torch.Tensor,
506 index_q: torch.Tensor | None,
507 index_q_scale: torch.Tensor | None,
508 index_weights: torch.Tensor | None,
509 q: torch.Tensor,
510 kv: torch.Tensor,
511 positions: torch.Tensor,
512 out: torch.Tensor,
513 ) -> None:
514 """CPU override: identical body, minus ``@eager_break_during_capture``
515 (this platform never captures a CUDA graph, so it was a no-op)."""
516 if self.indexer is not None and index_q is not None:
517 assert index_weights is not None
518 # index_q_scale is always None on CPU (FP8-only path).
519 assert index_q_scale is None
520 self.indexer.indexer_op(hidden_states, index_q, None, index_weights)
521
522 self.forward_mqa(q, kv, positions, out)
523
524 @classmethod
525 def get_padded_num_q_heads(cls, num_heads: int) -> int:
526 # No head-count padding constraint for the (not yet ported) CPU kernel.
527 return num_heads
528
529 def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
530 if get_forward_context().attn_metadata is None:
531 # Warmup/profile run: no real metadata, o is a zeroed placeholder.
532 return torch.zeros(
533 (o.shape[0], self.hidden_size), dtype=o.dtype, device=o.device
534 )
535
536 num_tokens, num_heads, _ = o.shape
537 cos_sin_cache = self.rotary_emb.cos_sin_cache
538 # Undo the query's own GPT-J RoPE rotation: the attention output is
539 # a weighted sum of KV rows at different positions, so R(-pos_q)
540 # recovers the translation-invariant quantity wo_a/wo_b expect.
541 o_derot = ops.inverse_gptj_rope_o_proj_cpu(
542 o, positions, cos_sin_cache, _ROPE_DIM
543 )
544
545 heads_per_group = num_heads // self.n_local_groups
546 o_grouped = o_derot.reshape(
547 num_tokens, self.n_local_groups, heads_per_group * self.head_dim
548 ).to(torch.bfloat16)
549 assert self._wo_a_packed is not None
550 z = o_grouped.new_empty((self.n_local_groups, num_tokens, self.o_lora_rank))
551 ops.bmm_cpu(
552 z,
553 o_grouped.transpose(0, 1).contiguous(),
554 self._wo_a_packed,
555 True,
556 None,
557 )
558 z = z.transpose(0, 1).to(o.dtype)
559 return self.wo_b(z.flatten(1))
560
561 def forward_mqa(
562 self,
563 q: torch.Tensor,
564 kv: torch.Tensor,
565 positions: torch.Tensor,
566 output: torch.Tensor,
567 ) -> None:
568 forward_context = get_forward_context()
569 attn_metadata = forward_context.attn_metadata
570 if attn_metadata is None:
571 # Warmup/profile run: reserve nothing extra, skip real kernels.
572 output.zero_()
573 return
574
575 assert isinstance(attn_metadata, dict)
576 swa_metadata = cast(
577 DeepseekSparseSWAMetadata,
578 attn_metadata.get(self.swa_cache_layer.prefix),
579 )
580 assert swa_metadata is not None
581 assert swa_metadata.token_to_req_indices is not None
582 req_idx = swa_metadata.token_to_req_indices
583
584 swa_only = self.compress_ratio <= 1
585 flashmla_metadata = cast(
586 DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
587 )
588
589 num_tokens = q.shape[0]
590 device = q.device
591
592 # ---- SWA window: causal sliding window over the raw per-token cache
593 window = self.window_size
594 win_offsets = torch.arange(window, device=device)
595 start_pos = (positions - window + 1).clamp(min=0)
596 local_pos = start_pos.unsqueeze(-1) + win_offsets
597 valid_win = local_pos <= positions.unsqueeze(-1)
598 win_local = torch.where(valid_win, local_pos, torch.full_like(local_pos, -1))
599 window_slots = map_local_to_global_slots_cpu(
600 win_local, req_idx, swa_metadata.block_table, swa_metadata.block_size
601 )
602 window_cache_2d = self.swa_cache_layer.kv_cache.view(
603 self.swa_cache_layer.kv_cache.shape[0], -1
604 )
605
606 # ---- Compressed KV: C4A's learned top-k, or C128A's dense top-k ----
607 if swa_only:
608 compressed_slots = window_slots.new_full((num_tokens, 0), -1)
609 # Unused when num_compressed == 0 (only dtype/2D-ness matter);
610 # reuse the window cache to avoid an allocation.
611 compressed_cache_2d = window_cache_2d
612 compressed_block_size = 1
613 else:
614 assert flashmla_metadata is not None
615 main_block_size = flashmla_metadata.block_size // self.compress_ratio
616 if self.compress_ratio == 4:
617 assert self.topk_indices_buffer is not None
618 local_topk = self.topk_indices_buffer[:num_tokens]
619 else:
620 width = -(-self.max_model_len // self.compress_ratio)
621 num_compressed = torch.div(
622 positions + 1, self.compress_ratio, rounding_mode="floor"
623 ).clamp(max=width)
624 arange_w = torch.arange(width, device=device)
625 local_topk = torch.where(
626 arange_w.unsqueeze(0) < num_compressed.unsqueeze(-1),
627 arange_w.unsqueeze(0).expand(num_tokens, width),
628 torch.full(
629 (num_tokens, width), -1, device=device, dtype=torch.int64
630 ),
631 )
632 compressed_slots = map_local_to_global_slots_cpu(
633 local_topk, req_idx, flashmla_metadata.block_table, main_block_size
634 )
635 compressed_cache_2d = self.kv_cache.view(self.kv_cache.shape[0], -1)
636 compressed_block_size = main_block_size
637
638 torch.ops._C.flash_mla_with_kvcache_cpu(
639 output,
640 q,
641 window_cache_2d,
642 window_slots,
643 swa_metadata.block_size,
644 compressed_cache_2d,
645 compressed_slots,
646 compressed_block_size,
647 self.attn_sink,
648 self.scale,
649 )