main
py 830 lines 28.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Dots3 NOTE sliding-window MLA attention backends for Hopper.
4
5 Prefill and mixed batches expand the latent cache and use FlashAttention-3
6 varlen MHA. Decode-only batches use the Triton absorbed-MQA kernel.
7 """
8
9 from dataclasses import dataclass
10 from typing import ClassVar
11
12 import torch
13
14 import vllm._custom_ops as ops
15 from vllm.model_executor.layers.attention.mla_attention import (
16 MLACommonDecodeMetadata,
17 MLACommonMetadata,
18 QueryLenSupport,
19 )
20 from vllm.platforms import current_platform
21 from vllm.triton_utils import tl, triton
22 from vllm.utils.torch_utils import (
23 PIN_MEMORY,
24 async_tensor_h2d,
25 is_quantized_kv_cache,
26 )
27 from vllm.v1.attention.backend import (
28 AttentionCGSupport,
29 AttentionLayer,
30 CommonAttentionMetadata,
31 )
32 from vllm.v1.attention.backends.mla.flashattn_mla_sparse import (
33 FlashAttnMLASparseBackend,
34 FlashAttnMLASparseImpl,
35 FlashAttnMLASparseMetadata,
36 )
37 from vllm.v1.attention.backends.mla.prefill.base import MLADimensions
38 from vllm.v1.attention.backends.mla.prefill.flash_attn import (
39 FlashAttnPrefillBackend,
40 )
41 from vllm.v1.attention.backends.mla.sparse_utils import (
42 triton_convert_req_index_to_global_index,
43 )
44 from vllm.v1.attention.backends.mla.triton_mla import (
45 TritonMLABackend,
46 TritonMLAImpl,
47 TritonMLAMetadataBuilder,
48 )
49 from vllm.v1.kv_cache_interface import KVCacheLayout
50 from vllm.v1.worker.workspace import (
51 current_workspace_manager,
52 is_workspace_manager_initialized,
53 )
54 from vllm.vllm_flash_attn.flash_attn_interface import flash_attn_varlen_func
55
56
57 @triton.jit
58 def _gather_swa_kv_kernel(
59 kv_cache_ptr,
60 block_table_ptr,
61 seq_lens_ptr,
62 kv_out_ptr,
63 valid_out_ptr,
64 k_scale_ptr,
65 kv_cache_stride_block,
66 kv_cache_stride_token,
67 kv_cache_stride_dim,
68 block_table_stride_req,
69 kv_out_stride_req,
70 kv_out_stride_token,
71 kv_out_stride_dim,
72 valid_out_stride_req,
73 valid_out_stride_token,
74 GATHER_LEN: tl.constexpr,
75 KV_DIM: tl.constexpr,
76 BLOCK_T: tl.constexpr,
77 BLOCK_D: tl.constexpr,
78 PAGE_SIZE: tl.constexpr,
79 ):
80 req_idx = tl.program_id(0)
81 token_block_idx = tl.program_id(1)
82 dim_block_idx = tl.program_id(2)
83
84 seq_len = tl.load(seq_lens_ptr + req_idx).to(tl.int32)
85 gather_start = tl.maximum(seq_len - GATHER_LEN, 0)
86 token_offsets = token_block_idx * BLOCK_T + tl.arange(0, BLOCK_T)
87 logical_tokens = gather_start + token_offsets
88 logical_valid = (token_offsets < GATHER_LEN) & (logical_tokens < seq_len)
89
90 logical_pages = logical_tokens // PAGE_SIZE
91 page_offsets = logical_tokens - logical_pages * PAGE_SIZE
92 physical_pages = tl.load(
93 block_table_ptr + req_idx * block_table_stride_req + logical_pages,
94 mask=logical_valid,
95 other=-1,
96 ).to(tl.int64)
97 physical_valid = logical_valid & (physical_pages >= 0)
98
99 dim_offsets = dim_block_idx * BLOCK_D + tl.arange(0, BLOCK_D)
100 values = tl.load(
101 kv_cache_ptr
102 + physical_pages[:, None] * kv_cache_stride_block
103 + page_offsets[:, None] * kv_cache_stride_token
104 + dim_offsets[None, :] * kv_cache_stride_dim,
105 mask=physical_valid[:, None] & (dim_offsets[None, :] < KV_DIM),
106 other=0.0,
107 )
108 if values.dtype.is_fp8():
109 values = values.to(tl.float32) * tl.load(k_scale_ptr)
110 tl.store(
111 kv_out_ptr
112 + req_idx * kv_out_stride_req
113 + token_offsets[:, None] * kv_out_stride_token
114 + dim_offsets[None, :] * kv_out_stride_dim,
115 values,
116 mask=(token_offsets[:, None] < GATHER_LEN) & (dim_offsets[None, :] < KV_DIM),
117 )
118 tl.store(
119 valid_out_ptr
120 + req_idx * valid_out_stride_req
121 + token_offsets * valid_out_stride_token,
122 physical_valid,
123 mask=(dim_block_idx == 0) & (token_offsets < GATHER_LEN),
124 )
125
126
127 @triton.jit
128 def _apply_swa_score_mask_kernel(
129 scores_ptr,
130 seq_lens_ptr,
131 valid_ptr,
132 scores_stride_req,
133 scores_stride_head,
134 scores_stride_query,
135 scores_stride_token,
136 valid_stride_req,
137 valid_stride_token,
138 GATHER_LEN: tl.constexpr,
139 WINDOW_SIZE: tl.constexpr,
140 QUERY_LEN: tl.constexpr,
141 BLOCK_T: tl.constexpr,
142 ):
143 req_idx = tl.program_id(0)
144 head_idx = tl.program_id(1)
145 query_idx = tl.program_id(2)
146
147 token_offsets = tl.arange(0, BLOCK_T)
148 seq_len = tl.load(seq_lens_ptr + req_idx).to(tl.int32)
149 gather_start = tl.maximum(seq_len - GATHER_LEN, 0)
150 kv_positions = gather_start + token_offsets
151 query_position = seq_len - QUERY_LEN + query_idx
152 page_valid = tl.load(
153 valid_ptr + req_idx * valid_stride_req + token_offsets * valid_stride_token,
154 mask=token_offsets < GATHER_LEN,
155 other=0,
156 ).to(tl.int1)
157 valid = (
158 (token_offsets < GATHER_LEN)
159 & page_valid
160 & (kv_positions <= query_position)
161 & (kv_positions >= query_position - WINDOW_SIZE + 1)
162 & (query_position >= 0)
163 )
164 tl.store(
165 scores_ptr
166 + req_idx * scores_stride_req
167 + head_idx * scores_stride_head
168 + query_idx * scores_stride_query
169 + token_offsets * scores_stride_token,
170 tl.full((BLOCK_T,), -3.4028234663852886e38, tl.float32),
171 mask=(token_offsets < GATHER_LEN) & ~valid,
172 )
173
174
175 @dataclass
176 class Dots3NoteDecodeMetadata(MLACommonDecodeMetadata):
177 query_len: int
178
179
180 @dataclass
181 class _SlidingWindowChunk:
182 req_start: int
183 req_end: int
184 query_start: int
185 query_end: int
186 cu_seq_lens_q: torch.Tensor
187 cu_seq_lens_k: torch.Tensor
188 starts: torch.Tensor
189 token_to_seq: torch.Tensor
190 num_kv_tokens: int
191 max_seq_len_q: int
192 max_seq_len_k: int
193
194
195 @dataclass
196 class _SlidingWindowMetadata:
197 chunks: list[_SlidingWindowChunk]
198 workspace: torch.Tensor
199
200
201 def _build_sliding_window_metadata(
202 *,
203 seq_lens_cpu: torch.Tensor,
204 query_start_loc_cpu: torch.Tensor,
205 sliding_window: int,
206 workspace: torch.Tensor,
207 workspace_size: int,
208 device: torch.device,
209 ) -> _SlidingWindowMetadata:
210 """Plan per-request latent-cache gathers for SWA varlen attention."""
211 query_lens_cpu = (query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]).to(
212 dtype=torch.int32
213 )
214 seq_lens_cpu = seq_lens_cpu.to(dtype=torch.int32)
215 kv_lens_cpu = torch.minimum(seq_lens_cpu, query_lens_cpu + sliding_window - 1)
216 starts_cpu = seq_lens_cpu - kv_lens_cpu
217
218 chunks: list[_SlidingWindowChunk] = []
219 req_start = 0
220 while req_start < query_lens_cpu.numel():
221 req_end = req_start
222 num_kv_tokens = 0
223 while req_end < query_lens_cpu.numel():
224 next_len = int(kv_lens_cpu[req_end].item())
225 if num_kv_tokens and num_kv_tokens + next_len > workspace_size:
226 break
227 if next_len > workspace_size:
228 raise ValueError(
229 "Dots3 NOTE SWA prefill window exceeds the MLA workspace: "
230 f"{next_len} > {workspace_size}"
231 )
232 num_kv_tokens += next_len
233 req_end += 1
234
235 query_lens = query_lens_cpu[req_start:req_end]
236 kv_lens = kv_lens_cpu[req_start:req_end]
237 num_reqs = req_end - req_start
238 cu_seq_lens_q_cpu = torch.zeros(
239 num_reqs + 1, dtype=torch.int32, pin_memory=PIN_MEMORY
240 )
241 cu_seq_lens_k_cpu = torch.zeros(
242 num_reqs + 1, dtype=torch.int32, pin_memory=PIN_MEMORY
243 )
244 torch.cumsum(query_lens, 0, out=cu_seq_lens_q_cpu[1:])
245 torch.cumsum(kv_lens, 0, out=cu_seq_lens_k_cpu[1:])
246 token_to_seq_cpu = torch.repeat_interleave(
247 torch.arange(num_reqs, dtype=torch.int32), kv_lens
248 )
249 query_start = int(query_start_loc_cpu[req_start].item())
250 query_end = int(query_start_loc_cpu[req_end].item())
251 chunks.append(
252 _SlidingWindowChunk(
253 req_start=req_start,
254 req_end=req_end,
255 query_start=query_start,
256 query_end=query_end,
257 cu_seq_lens_q=cu_seq_lens_q_cpu.to(device, non_blocking=True),
258 cu_seq_lens_k=cu_seq_lens_k_cpu.to(device, non_blocking=True),
259 starts=async_tensor_h2d(starts_cpu[req_start:req_end], device),
260 token_to_seq=async_tensor_h2d(token_to_seq_cpu, device),
261 num_kv_tokens=num_kv_tokens,
262 max_seq_len_q=int(query_lens.max().item()),
263 max_seq_len_k=int(kv_lens.max().item()),
264 )
265 )
266 req_start = req_end
267
268 return _SlidingWindowMetadata(chunks=chunks, workspace=workspace)
269
270
271 class Dots3NoteFlashAttnPrefillBackend(FlashAttnPrefillBackend):
272 """FA3 varlen prefill for the NOTE SWA MLA dimensions."""
273
274 @classmethod
275 def supports_mla_dimensions(cls, dims: MLADimensions) -> bool:
276 return dims == MLADimensions(
277 qk_nope_head_dim=192,
278 qk_rope_head_dim=64,
279 v_head_dim=128,
280 )
281
282 def __init__(self, *args, **kwargs) -> None:
283 super().__init__(*args, **kwargs)
284 # FA3 on SM90 does not support NOTE's Q/K=256, V=128 combination via
285 # the different-head-dimension path. Padding V selects its supported
286 # equal-dimension varlen kernel.
287 self.requires_v_padding = True
288
289 def supports_quant_output(self, quant_key) -> bool:
290 return False
291
292 def run_sliding_window(
293 self,
294 q: torch.Tensor,
295 k: torch.Tensor,
296 v: torch.Tensor,
297 cu_seq_lens_q: torch.Tensor,
298 cu_seq_lens_k: torch.Tensor,
299 max_seq_len_q: int,
300 max_seq_len_k: int,
301 sliding_window: int,
302 ) -> torch.Tensor:
303 output = self._flash_attn_varlen_diff_headdims(
304 q=q,
305 k=k,
306 v=v,
307 cu_seqlens_q=cu_seq_lens_q,
308 cu_seqlens_k=cu_seq_lens_k,
309 max_seqlen_q=max_seq_len_q,
310 max_seqlen_k=max_seq_len_k,
311 softmax_scale=self.scale,
312 causal=True,
313 window_size=(sliding_window - 1, 0),
314 return_softmax_lse=False,
315 )
316 assert isinstance(output, torch.Tensor)
317 return output
318
319
320 class Dots3NoteMLAMetadataBuilder(TritonMLAMetadataBuilder):
321 """Keep decode on MQA and route prefill/mixed batches through FA3."""
322
323 _cudagraph_support: ClassVar[AttentionCGSupport] = AttentionCGSupport.UNIFORM_BATCH
324 query_len_support = QueryLenSupport.UNIFORM
325
326 def __init__(self, kv_cache_spec, layer_names, vllm_config, device):
327 self.sliding_window = kv_cache_spec.sliding_window
328 super().__init__(kv_cache_spec, layer_names, vllm_config, device)
329
330 def _reserve_attn_logits_workspace(self) -> None:
331 if not is_workspace_manager_initialized():
332 return
333 batch = self.vllm_config.scheduler_config.max_num_seqs
334 max_query_len = self.reorder_batch_threshold or 1
335 gather_len = (self.sliding_window + max_query_len - 1 + 7) // 8 * 8
336 current_workspace_manager().get_simultaneous(
337 (
338 (
339 batch,
340 gather_len,
341 self.mla_dims.kv_lora_rank + self.mla_dims.qk_rope_head_dim,
342 ),
343 self.model_config.dtype,
344 ),
345 ((batch, gather_len), torch.bool),
346 )
347
348 def _build_decode(
349 self,
350 block_table_tensor: torch.Tensor,
351 seq_lens_device: torch.Tensor,
352 max_seq_len: int,
353 query_start_loc_cpu: torch.Tensor,
354 query_start_loc_device: torch.Tensor,
355 num_decode_tokens: int,
356 max_query_len: int,
357 dcp_tot_seq_lens_device: torch.Tensor | None,
358 ) -> Dots3NoteDecodeMetadata:
359 del max_seq_len, query_start_loc_device, num_decode_tokens, max_query_len
360 query_len = int(query_start_loc_cpu[1] - query_start_loc_cpu[0])
361 return Dots3NoteDecodeMetadata(
362 block_table=block_table_tensor,
363 seq_lens=seq_lens_device,
364 dcp_tot_seq_lens=dcp_tot_seq_lens_device,
365 query_len=query_len,
366 )
367
368 def build(
369 self,
370 common_prefix_len: int,
371 common_attn_metadata: CommonAttentionMetadata,
372 fast_build: bool = False,
373 ) -> MLACommonMetadata:
374 metadata = super().build(
375 common_prefix_len,
376 common_attn_metadata,
377 fast_build=fast_build,
378 )
379 if metadata.prefill is None:
380 return metadata
381 if self.dcp_world_size > 1:
382 raise NotImplementedError(
383 "Dots3 NOTE SWA prefill does not support decode context parallelism"
384 )
385
386 reqs_start = metadata.num_decodes
387 seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
388 assert seq_lens_cpu is not None
389 query_start_loc_cpu = common_attn_metadata.query_start_loc_cpu[reqs_start:]
390 query_start_loc_cpu = query_start_loc_cpu - query_start_loc_cpu[0]
391 sliding_metadata = _build_sliding_window_metadata(
392 seq_lens_cpu=seq_lens_cpu[reqs_start:],
393 query_start_loc_cpu=query_start_loc_cpu,
394 sliding_window=self.sliding_window,
395 workspace=self.chunked_prefill_workspace,
396 workspace_size=self.chunked_prefill_workspace_size,
397 device=self.device,
398 )
399 metadata.prefill.chunked_context = None
400 metadata.prefill.sliding_window = sliding_metadata # type: ignore[attr-defined]
401 if metadata.num_decodes > 0 and metadata.num_prefills > 0:
402 query_lens_cpu = query_start_loc_cpu[1:] - query_start_loc_cpu[:-1]
403 for chunk in sliding_metadata.chunks:
404 req_start, req_end = chunk.req_start, chunk.req_end
405 seq_lens = common_attn_metadata.seq_lens[
406 reqs_start + req_start : reqs_start + req_end
407 ]
408 query_lens = async_tensor_h2d(
409 query_lens_cpu[req_start:req_end], self.device
410 )
411 kv_lens = torch.minimum(seq_lens, query_lens + self.sliding_window - 1)
412 chunk.cu_seq_lens_k.zero_()
413 torch.cumsum(
414 kv_lens,
415 dim=0,
416 out=chunk.cu_seq_lens_k[1:],
417 dtype=torch.int32,
418 )
419 chunk.starts = seq_lens - kv_lens
420 token_ids = torch.arange(
421 chunk.num_kv_tokens,
422 dtype=torch.int32,
423 device=self.device,
424 )
425 chunk.token_to_seq = torch.searchsorted(
426 chunk.cu_seq_lens_k[1:], token_ids, right=True
427 ).to(torch.int32)
428 chunk.token_to_seq.clamp_max_(req_end - req_start - 1)
429 assert metadata.prefill.prefill_backend is not None
430 metadata.prefill.prefill_backend.prepare_metadata(metadata.prefill)
431 return metadata
432
433
434 class Dots3NoteTritonMLABackend(TritonMLABackend):
435 """Internal NOTE SWA specialization; not a user-selectable backend."""
436
437 @classmethod
438 def get_supported_head_sizes(cls) -> list[int]:
439 return [1088]
440
441 @classmethod
442 def supports_sliding_window(cls) -> bool:
443 return True
444
445 @staticmethod
446 def get_impl_cls() -> type["Dots3NoteTritonMLAImpl"]:
447 return Dots3NoteTritonMLAImpl
448
449 @staticmethod
450 def get_builder_cls() -> type[Dots3NoteMLAMetadataBuilder]:
451 return Dots3NoteMLAMetadataBuilder
452
453
454 class Dots3NoteTritonMLAImpl(TritonMLAImpl):
455 def __init__(
456 self,
457 num_heads: int,
458 head_size: int,
459 scale: float,
460 num_kv_heads: int,
461 alibi_slopes: list[float] | None,
462 sliding_window: int | None,
463 kv_cache_dtype: str,
464 logits_soft_cap: float | None,
465 attn_type: str,
466 kv_sharing_target_layer_name: str | None,
467 **mla_args,
468 ) -> None:
469 assert sliding_window is not None
470 super().__init__(
471 num_heads=num_heads,
472 head_size=head_size,
473 scale=scale,
474 num_kv_heads=num_kv_heads,
475 alibi_slopes=alibi_slopes,
476 sliding_window=None,
477 kv_cache_dtype=kv_cache_dtype,
478 logits_soft_cap=logits_soft_cap,
479 attn_type=attn_type,
480 kv_sharing_target_layer_name=kv_sharing_target_layer_name,
481 **mla_args,
482 )
483 self.sliding_window = sliding_window
484
485 def _forward_swa_mqa(
486 self,
487 q: torch.Tensor,
488 kv_cache: torch.Tensor,
489 decode: Dots3NoteDecodeMetadata,
490 k_scale: torch.Tensor,
491 ) -> tuple[torch.Tensor, None]:
492 query_len = decode.query_len
493 num_tokens, num_heads, head_size = q.shape
494 assert num_tokens % query_len == 0
495 assert head_size == self.head_size
496 num_reqs = num_tokens // query_len
497 block_table = decode.block_table[:num_reqs]
498 seq_lens = decode.seq_lens[:num_reqs]
499 gather_len = (self.sliding_window + query_len - 1 + 7) // 8 * 8
500
501 workspace_specs = (
502 ((num_reqs, gather_len, head_size), q.dtype),
503 ((num_reqs, gather_len), torch.bool),
504 )
505 if is_workspace_manager_initialized():
506 kv_latent, kv_valid = current_workspace_manager().get_simultaneous(
507 *workspace_specs
508 )
509 else:
510 kv_latent = torch.empty(
511 workspace_specs[0][0], dtype=q.dtype, device=q.device
512 )
513 kv_valid = torch.empty(
514 workspace_specs[1][0], dtype=torch.bool, device=q.device
515 )
516
517 block_t = 8
518 block_d = 128
519 _gather_swa_kv_kernel[
520 (
521 num_reqs,
522 triton.cdiv(gather_len, block_t),
523 triton.cdiv(head_size, block_d),
524 )
525 ](
526 kv_cache,
527 block_table,
528 seq_lens,
529 kv_latent,
530 kv_valid,
531 k_scale,
532 kv_cache.stride(0),
533 kv_cache.stride(1),
534 kv_cache.stride(2),
535 block_table.stride(0),
536 kv_latent.stride(0),
537 kv_latent.stride(1),
538 kv_latent.stride(2),
539 kv_valid.stride(0),
540 kv_valid.stride(1),
541 GATHER_LEN=gather_len,
542 KV_DIM=head_size,
543 BLOCK_T=block_t,
544 BLOCK_D=block_d,
545 PAGE_SIZE=kv_cache.shape[1],
546 num_warps=4,
547 )
548
549 q = q.view(num_reqs, query_len, num_heads, head_size)
550 scores = torch.bmm(
551 q.reshape(num_reqs, query_len * num_heads, head_size),
552 kv_latent.transpose(1, 2),
553 ).view(num_reqs, query_len, num_heads, gather_len)
554 scores = scores.float()
555 scores.mul_(self.scale)
556 scores_by_head = scores.transpose(1, 2)
557 _apply_swa_score_mask_kernel[(num_reqs, num_heads, query_len)](
558 scores_by_head,
559 seq_lens,
560 kv_valid,
561 scores_by_head.stride(0),
562 scores_by_head.stride(1),
563 scores_by_head.stride(2),
564 scores_by_head.stride(3),
565 kv_valid.stride(0),
566 kv_valid.stride(1),
567 GATHER_LEN=gather_len,
568 WINDOW_SIZE=self.sliding_window,
569 QUERY_LEN=query_len,
570 BLOCK_T=triton.next_power_of_2(gather_len),
571 num_warps=4,
572 )
573 probs = torch.softmax(scores, dim=-1).to(q.dtype)
574 output = torch.bmm(
575 probs.reshape(num_reqs, query_len * num_heads, gather_len),
576 kv_latent[..., : self.kv_lora_rank],
577 )
578 return output.view(num_tokens, num_heads, self.kv_lora_rank), None
579
580 def forward_mha(
581 self,
582 q: torch.Tensor,
583 kv_c_normed: torch.Tensor,
584 k_pe: torch.Tensor,
585 kv_c_and_k_pe_cache: torch.Tensor,
586 attn_metadata: MLACommonMetadata,
587 k_scale: torch.Tensor,
588 output: torch.Tensor,
589 output_scale: torch.Tensor | None = None,
590 ) -> None:
591 prefill = attn_metadata.prefill
592 sliding = getattr(prefill, "sliding_window", None)
593 if prefill is None or sliding is None:
594 return super().forward_mha(
595 q,
596 kv_c_normed,
597 k_pe,
598 kv_c_and_k_pe_cache,
599 attn_metadata,
600 k_scale,
601 output,
602 output_scale,
603 )
604 assert output_scale is None
605 assert isinstance(prefill.prefill_backend, Dots3NoteFlashAttnPrefillBackend)
606 use_fp8_prefill = prefill.q_data_type == current_platform.fp8_dtype()
607 output = output.view(-1, self.num_heads, self.v_head_dim)
608
609 for chunk in sliding.chunks:
610 toks = chunk.num_kv_tokens
611 workspace = sliding.workspace
612 block_table = prefill.block_table[chunk.req_start : chunk.req_end]
613 if is_quantized_kv_cache(self.kv_cache_dtype) and not use_fp8_prefill:
614 ops.gather_and_maybe_dequant_cache(
615 src_cache=kv_c_and_k_pe_cache,
616 dst=workspace,
617 block_table=block_table,
618 cu_seq_lens=chunk.cu_seq_lens_k,
619 token_to_seq=chunk.token_to_seq,
620 num_tokens=toks,
621 kv_cache_dtype=self.kv_cache_dtype,
622 scale=k_scale,
623 seq_starts=chunk.starts,
624 )
625 else:
626 ops.cp_gather_cache(
627 src_cache=kv_c_and_k_pe_cache,
628 dst=workspace,
629 block_table=block_table,
630 cu_seq_lens=chunk.cu_seq_lens_k,
631 batch_size=chunk.req_end - chunk.req_start,
632 seq_starts=chunk.starts,
633 )
634
635 # Slicing the latent part from the packed latent+RoPE cache keeps
636 # the full cache-entry row stride. FP8 linear kernels require
637 # contiguous rows and would otherwise read them at wrong offsets.
638 kv_c = workspace[:toks, : self.kv_lora_rank].contiguous()
639 weight_dtype = (
640 self.kv_b_proj.weight.dtype
641 if hasattr(self.kv_b_proj, "weight")
642 else self.kv_b_proj.params_dtype
643 )
644 if (
645 use_fp8_prefill or weight_dtype != current_platform.fp8_dtype()
646 ) and weight_dtype != torch.uint8:
647 kv_c = kv_c.to(weight_dtype)
648 k_pe_chunk = workspace[:toks, self.kv_lora_rank :].unsqueeze(1)
649 kv_nope = self.kv_b_proj(kv_c)[0].view(
650 -1, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
651 )
652 if use_fp8_prefill:
653 kv_nope = kv_nope.to(prefill.q_data_type)
654 k_pe_chunk = k_pe_chunk.to(prefill.q_data_type)
655 k_nope, v = kv_nope.split([self.qk_nope_head_dim, self.v_head_dim], dim=-1)
656 k = self._concat_k_nope_k_pe(k_nope, k_pe_chunk)
657 chunk_output = prefill.prefill_backend.run_sliding_window(
658 q=q[chunk.query_start : chunk.query_end],
659 k=k,
660 v=v,
661 cu_seq_lens_q=chunk.cu_seq_lens_q,
662 cu_seq_lens_k=chunk.cu_seq_lens_k,
663 max_seq_len_q=chunk.max_seq_len_q,
664 max_seq_len_k=chunk.max_seq_len_k,
665 sliding_window=self.sliding_window,
666 )
667 output[chunk.query_start : chunk.query_end].copy_(
668 chunk_output[..., : self.v_head_dim]
669 )
670
671 def forward_mqa(
672 self,
673 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
674 kv_c_and_k_pe_cache: torch.Tensor,
675 attn_metadata: MLACommonMetadata,
676 layer: AttentionLayer,
677 ) -> tuple[torch.Tensor, torch.Tensor | None]:
678 assert kv_c_and_k_pe_cache.numel() > 0
679 assert attn_metadata.decode is not None
680 if isinstance(q, tuple):
681 q = torch.cat(q, dim=-1)
682
683 decode = attn_metadata.decode
684 assert isinstance(decode, Dots3NoteDecodeMetadata)
685 return self._forward_swa_mqa(
686 q,
687 kv_c_and_k_pe_cache,
688 decode,
689 layer._k_scale,
690 )
691
692
693 class Dots3NotePaddedSparseBackend(FlashAttnMLASparseBackend):
694 """NOTE DSA backend for cache rows padded to the SWA latent width."""
695
696 @classmethod
697 def supported_kv_cache_layouts(cls) -> tuple[KVCacheLayout, ...]:
698 # NOTE packs the smaller DSA index pages beside the padded MLA/SWA pages.
699 # Keep the layer dimension inside the block dimension so mixed page sizes
700 # remain representable by the shared KV-cache allocation.
701 return (KVCacheLayout.BLHNC,)
702
703 @staticmethod
704 def get_name() -> str:
705 return "DOTS3_NOTE_PADDED_MLA_SPARSE"
706
707 @staticmethod
708 def get_impl_cls() -> type["Dots3NotePaddedSparseImpl"]:
709 return Dots3NotePaddedSparseImpl
710
711
712 class Dots3NotePaddedSparseImpl(FlashAttnMLASparseImpl):
713 """Read top-k KV directly from uniformly padded cache rows."""
714
715 def _logical_cache(self, kv_cache: torch.Tensor) -> torch.Tensor:
716 assert kv_cache.shape[-1] >= self.head_size
717 return kv_cache[..., : self.head_size]
718
719 def do_kv_cache_update(
720 self,
721 kv_c_normed: torch.Tensor,
722 k_pe: torch.Tensor,
723 kv_cache: torch.Tensor,
724 slot_mapping: torch.Tensor,
725 kv_cache_dtype: str,
726 k_scale: torch.Tensor,
727 ) -> None:
728 super().do_kv_cache_update(
729 kv_c_normed,
730 k_pe,
731 self._logical_cache(kv_cache),
732 slot_mapping,
733 kv_cache_dtype,
734 k_scale,
735 )
736
737 def forward_mha( # type: ignore[override]
738 self,
739 q: torch.Tensor,
740 kv_c_normed: torch.Tensor,
741 k_pe: torch.Tensor,
742 kv_c_and_k_pe_cache: torch.Tensor,
743 attn_metadata: FlashAttnMLASparseMetadata,
744 k_scale: torch.Tensor,
745 output: torch.Tensor,
746 output_scale: torch.Tensor | None = None,
747 ) -> None:
748 super().forward_mha(
749 q,
750 kv_c_normed,
751 k_pe,
752 self._logical_cache(kv_c_and_k_pe_cache),
753 attn_metadata,
754 k_scale,
755 output,
756 output_scale,
757 )
758
759 def forward_mqa(
760 self,
761 q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
762 kv_c_and_k_pe_cache: torch.Tensor,
763 attn_metadata: FlashAttnMLASparseMetadata,
764 layer: AttentionLayer,
765 ) -> tuple[torch.Tensor, torch.Tensor | None]:
766 if not isinstance(q, tuple):
767 raise NotImplementedError(
768 "Dots3NotePaddedSparseImpl expects split (q_nope, q_rope) input."
769 )
770 q_nope, q_rope = q
771 num_actual_toks = q_rope.shape[0]
772
773 assert self.topk_indices_buffer is not None
774 topk_indices = self.topk_indices_buffer[:num_actual_toks]
775 topk_indices, valid_counts = triton_convert_req_index_to_global_index(
776 attn_metadata.req_id_per_token[:num_actual_toks],
777 attn_metadata.block_table,
778 topk_indices,
779 BLOCK_SIZE=attn_metadata.block_size,
780 NUM_TOPK_TOKENS=topk_indices.shape[1],
781 return_valid_counts=True,
782 )
783 physical_head_size = kv_c_and_k_pe_cache.shape[-1]
784 block_stride, token_stride, head_stride = kv_c_and_k_pe_cache.stride()
785 if (
786 token_stride != physical_head_size
787 or head_stride != 1
788 or block_stride % physical_head_size != 0
789 ):
790 raise RuntimeError(
791 "Dots3 NOTE padded DSA cache must contain contiguous token rows; "
792 f"got shape={tuple(kv_c_and_k_pe_cache.shape)} and "
793 f"stride={kv_c_and_k_pe_cache.stride()}"
794 )
795 block_row_stride = block_stride // physical_head_size
796 block_indices = torch.div(
797 topk_indices,
798 attn_metadata.block_size,
799 rounding_mode="floor",
800 )
801 block_indices.clamp_min_(0).mul_(block_row_stride - attn_metadata.block_size)
802 topk_indices.add_(block_indices)
803
804 num_cache_rows = (
805 kv_c_and_k_pe_cache.shape[0] - 1
806 ) * block_row_stride + attn_metadata.block_size
807 kv_cache = kv_c_and_k_pe_cache.as_strided(
808 (num_cache_rows, physical_head_size),
809 (physical_head_size, 1),
810 )
811 cu_seqlens_q = torch.arange(
812 num_actual_toks + 1,
813 dtype=torch.int32,
814 device=q_rope.device,
815 )
816 output = flash_attn_varlen_func(
817 q=q_rope,
818 k=kv_cache[:, self.kv_lora_rank : self.head_size].unsqueeze(1).unsqueeze(1),
819 v=kv_cache[:, : self.kv_lora_rank].unsqueeze(1).unsqueeze(1),
820 q_v=q_nope,
821 max_seqlen_q=1,
822 cu_seqlens_q=cu_seqlens_q,
823 max_seqlen_k=topk_indices.shape[1],
824 seqused_k=valid_counts,
825 block_table=topk_indices,
826 softmax_scale=self.scale,
827 causal=True,
828 fa_version=3,
829 )
830 return output, None