1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""High-performance shared compressed DeepSeek Sparse Attention.
16
17This module implements the DeepSeek-V4.1 CSA2 training contract: Full layers
18publish compressed K=V and index K, Reindex layers produce fresh Top-K indices,
19and Reuse layers consume the latest selection. The decoder's first Full layer
20can additionally publish the blockwise candidate pool used by later Reindex
21layers. The attention replacement selects eager PyTorch references or fused
22Indexer, sparse attention and external-teacher KL operators while preserving
23the same sharing and parallel contexts. Explicit fusion rejects unsupported
24calls instead of falling back to another implementation.
25"""
26
27from __future__ import annotations
28
29from collections.abc import Callable, Mapping
30from dataclasses import dataclass, field
31from typing import Any, Protocol
32
33import torch # pylint: disable=forbidden-backend-import
34from torch import nn # pylint: disable=forbidden-backend-import
35
36from hyper_parallel.components.functional.aux_loss import aux_loss_auto_scale
37from hyper_parallel.components.functional.lightning_indexer import (
38 lightning_indexer, sort_key_indices, validate_indexer_tensors,
39)
40from hyper_parallel.components.functional.sparse_flash_mla import (
41 supports_fused_attention_inputs, sparse_flash_mla,
42)
43from hyper_parallel.components.functional.sparse_lightning_indexer_kl_loss_grad import (
44 gather_selected_keys, sparse_lightning_indexer_kl_loss, validate_fused_kl_inputs,
45)
46from hyper_parallel.core.activation_memory.wrapper import checkpoint_exclude_wrapper
47from hyper_parallel.core.activation_memory.recompute_state import is_recomputing
48from hyper_parallel.models.replacement import module_replacement
49
50
51class SequenceGatherHandle(Protocol):
52 """Handle returned by an asynchronous sequence all-gather."""
53
54 def wait(self) -> torch.Tensor:
55 """Wait for communication and return the differentiable result."""
56
57
58@dataclass
59class _DeferredSequenceGather:
60 """Synchronous fallback with the same interface as an async gather."""
61
62 tensor: torch.Tensor
63 sequence_dim: int
64 gather_sequence: Callable[[torch.Tensor, int], torch.Tensor]
65
66 def wait(self) -> torch.Tensor:
67 """Run the fallback gather at the first consumer boundary."""
68 return self.gather_sequence(self.tensor, self.sequence_dim)
69
70
71@dataclass
72class SharedCompressedAttentionState:
73 """Autograd-bearing CSA2 state scoped to one training forward.
74
75 State is addressed by the layer that produced it instead of using one
76 mutable "latest value" slot. This matters for the released V4.1 topology,
77 which has several Full/Reindex groups, and makes a consumer's dependency
78 explicit when activation recomputation revisits modules out of forward
79 order.
80
81 Nothing drops an entry early. The state travels into every layer as one of
82 the checkpoint region's inputs, so the graph keeps it alive from the forward
83 pass to the end of backward, and a layer whose forward is recomputed reads
84 its entries a second time there. A published entry therefore has to stay
85 readable until backward is done with every consumer, which is also why the
86 publishers skip their write while recomputing.
87 """
88
89 compressed_kv_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
90 index_key_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
91 topk_indices_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
92 candidate_blocks_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
93
94 @staticmethod
95 def _require(
96 values: dict[int, torch.Tensor],
97 source_layer: int | None,
98 value_name: str,
99 consumer_layer: int,
100 ) -> torch.Tensor:
101 """Return one published tensor or report the broken layer dependency."""
102 if source_layer is None or source_layer not in values:
103 raise RuntimeError(
104 f"layer {consumer_layer} requires {value_name} from source "
105 f"layer {source_layer}, but that source has not run in this forward"
106 )
107 return values[source_layer]
108
109 def publish_compressed_kv(self, source_layer: int, value: torch.Tensor) -> None:
110 """Publish compressed K=V while preserving its autograd graph."""
111 self.compressed_kv_by_source[source_layer] = value
112
113 def require_compressed_kv(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
114 """Read compressed K=V from the consumer's configured Full layer."""
115 return self._require(
116 self.compressed_kv_by_source,
117 source_layer,
118 "compressed K=V",
119 consumer_layer,
120 )
121
122 def publish_index_key(self, source_layer: int, value: torch.Tensor) -> None:
123 """Publish the Full Indexer's shared key tensor."""
124 self.index_key_by_source[source_layer] = value
125
126 def require_index_key(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
127 """Read the Indexer key associated with a compressed-KV source."""
128 return self._require(
129 self.index_key_by_source,
130 source_layer,
131 "Indexer key",
132 consumer_layer,
133 )
134
135 def publish_topk_indices(self, source_layer: int, value: torch.Tensor) -> None:
136 """Publish one Full/Reindex layer's token selection."""
137 self.topk_indices_by_source[source_layer] = value
138
139 def require_topk_indices(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
140 """Read the latest configured Full/Reindex selection."""
141 return self._require(
142 self.topk_indices_by_source,
143 source_layer,
144 "Top-K indices",
145 consumer_layer,
146 )
147
148 def publish_candidate_blocks(self, source_layer: int, value: torch.Tensor) -> None:
149 """Publish hierarchical candidate blocks from the configured source."""
150 self.candidate_blocks_by_source[source_layer] = value
151
152 def require_candidate_blocks(
153 self,
154 source_layer: int | None,
155 consumer_layer: int,
156 ) -> torch.Tensor:
157 """Read hierarchical candidates for a later Reindex layer."""
158 return self._require(
159 self.candidate_blocks_by_source,
160 source_layer,
161 "candidate blocks",
162 consumer_layer,
163 )
164
165
166@dataclass
167class SharedCompressedIndexerOutput:
168 """Tensors produced by one Full or Reindex Indexer invocation."""
169
170 topk_indices: torch.Tensor
171 index_key: torch.Tensor
172 candidate_blocks: torch.Tensor | None
173 index_query: torch.Tensor
174 merge_weight: torch.Tensor
175
176
177@dataclass(frozen=True)
178class SharedCompressedAttentionCPContext:
179 """KV-all-gather context for shared compressed DSA.
180
181 Query-side tensors remain local sequence shards. Raw KV, compressed KV,
182 and index keys are gathered in CP-rank order. ``launch_sequence`` is
183 optional so numerical tests and non-overlapped backends can supply only a
184 differentiable synchronous gather.
185 """
186
187 size: int
188 rank: int
189 gather_sequence: Callable[[torch.Tensor, int], torch.Tensor]
190 launch_sequence: Callable[[torch.Tensor, int], SequenceGatherHandle] | None = None
191
192 def launch(self, tensor: torch.Tensor, sequence_dim: int) -> SequenceGatherHandle:
193 """Launch an async gather or create a deferred synchronous gather."""
194 if self.launch_sequence is not None:
195 return self.launch_sequence(tensor, sequence_dim)
196 return _DeferredSequenceGather(tensor, sequence_dim, self.gather_sequence)
197
198
199@dataclass(frozen=True)
200class SharedCompressedAttentionTPContext:
201 """Tensor-parallel score reduction context for a head-sharded Indexer."""
202
203 size: int
204 rank: int
205 reduce_sum: Callable[[torch.Tensor], torch.Tensor]
206
207
208@dataclass(frozen=True)
209class SharedCompressedPackedSequence:
210 """Compact sample boundaries for one contiguous CP query shard."""
211
212 cu_seq_lens: torch.Tensor
213 local_query_start: int
214 local_query_length: int
215 global_sequence_length: int
216 _prepared_starts: dict[torch.device, torch.Tensor] = field(
217 default_factory=dict, init=False, repr=False, compare=False
218 )
219 _prepared_minima: dict[tuple[torch.device, int], torch.Tensor] = field(
220 default_factory=dict, init=False, repr=False, compare=False
221 )
222 _validated_ratios: set[int] = field(default_factory=set, init=False, repr=False, compare=False)
223 _snapshot: dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False)
224 _prepared_positions: dict[torch.device, torch.Tensor] = field(
225 default_factory=dict, init=False, repr=False, compare=False
226 )
227 _prepared_masks: dict[torch.device, torch.Tensor] = field(
228 default_factory=dict, init=False, repr=False, compare=False
229 )
230 _indexer_segments: dict[int, tuple[tuple[int, int, int, int, int], ...]] = field(
231 default_factory=dict, init=False, repr=False, compare=False
232 )
233
234 def prepare(self, device: torch.device, compress_ratios: tuple[int, ...]) -> SharedCompressedPackedSequence:
235 """Prepare an owned boundary snapshot, reused until the input is mutated.
236
237 Normal in-place tensor updates invalidate the cached snapshot. Existing
238 consumers retain their owned geometry. Prepared tensors are read-only;
239 callers must not mutate boundaries through ``.data`` or external storage.
240 Inference tensors without a version counter are always snapshotted anew.
241
242 Args:
243 device: Device on which tensors and prepared geometry are consumed.
244 compress_ratios: Compression ratios used by the layers in this forward.
245 """
246 device = torch.device(device)
247 # Torch exposes tensor mutation tracking only through its version counter.
248 version = None if self.cu_seq_lens.is_inference() else self.cu_seq_lens._version # pylint: disable=protected-access
249 if version is not None and self._snapshot.get("version") == version:
250 prepared: SharedCompressedPackedSequence = self if self._snapshot.get("owned") else self._snapshot["value"]
251 else:
252 prepared = SharedCompressedPackedSequence(
253 self.cu_seq_lens.detach().clone(), self.local_query_start,
254 self.local_query_length, self.global_sequence_length,
255 )
256 SharedCompressedPackedSequence._mark_owned(prepared)
257 self._snapshot.clear()
258 self._snapshot.update(version=version, value=prepared)
259 SharedCompressedPackedSequence._prepare_owned_metadata(prepared, device, compress_ratios)
260 return prepared
261
262 def _mark_owned(self) -> None:
263 """Record the owned tensor's mutation counter for subsequent layer reuse."""
264 if not self.cu_seq_lens.is_inference():
265 # Torch has no public read API for its tensor mutation counter.
266 self._snapshot.update(owned=True, version=self.cu_seq_lens._version) # pylint: disable=protected-access
267
268 def _prepare_owned_metadata(self, device: torch.device, compress_ratios: tuple[int, ...]) -> None:
269 """Populate caches on the owned snapshot, never on caller-owned boundaries."""
270 starts = self.local_segment_starts(device)
271 self._prepared_starts[device] = starts
272 for ratio in set(compress_ratios):
273 self.validate_compression_alignment(ratio)
274 self._validated_ratios.add(ratio)
275 if ratio > 0 and (device, ratio) not in self._prepared_minima:
276 self._prepared_minima[(device, ratio)] = starts // ratio
277
278 def local_positions(self, device: torch.device) -> torch.Tensor:
279 """Return the immutable local interval's global token positions."""
280 device = torch.device(device)
281 if device not in self._prepared_positions:
282 self._prepared_positions[device] = torch.arange(
283 self.local_query_start, self.local_query_start + self.local_query_length, device=device,
284 ).unsqueeze(0)
285 return self._prepared_positions[device]
286
287 def segment_start_mask(self, device: torch.device) -> torch.Tensor:
288 """Reuse the per-batch sample-start mask across model forwards."""
289 device = torch.device(device)
290 if device not in self._prepared_masks:
291 self._prepared_masks[device] = self.local_positions(device) == self.local_segment_starts(device)
292 return self._prepared_masks[device]
293
294 def indexer_segments(self, ratio: int) -> tuple[tuple[int, int, int, int, int], ...]:
295 """Describe packed Q intervals and completed K prefixes without per-layer synchronization."""
296 if ratio not in self._indexer_segments:
297 self.validate_compression_alignment(ratio)
298 boundaries = self.cu_seq_lens.detach().cpu().tolist()
299 local_start = self.local_query_start
300 local_end = local_start + self.local_query_length
301 segments = []
302 for sample_start, sample_end in zip(boundaries, boundaries[1:]):
303 start, end = max(local_start, sample_start), min(local_end, sample_end)
304 if start < end:
305 segments.append((start-local_start, end-local_start, sample_start//ratio, end//ratio,
306 (end-sample_start) % ratio))
307 self._indexer_segments[ratio] = tuple(segments)
308 return self._indexer_segments[ratio]
309
310 def minimum_key_indices(
311 self, device: torch.device, compress_ratio: int, segment_starts: torch.Tensor | None = None,
312 ) -> torch.Tensor:
313 """Return the first compressed key belonging to each query's sample.
314
315 Args:
316 device: Device on which tensors and prepared geometry are consumed.
317 compress_ratio: Compression ratio of the consuming layer.
318 segment_starts: Optional precomputed global sample starts for local queries.
319 """
320 if compress_ratio <= 0:
321 raise ValueError("minimum_key_indices requires a positive compression ratio")
322 cached = self._prepared_minima.get((torch.device(device), compress_ratio))
323 if cached is not None:
324 return cached
325 self.validate_compression_alignment(compress_ratio)
326 if segment_starts is None:
327 segment_starts = self.local_segment_starts(device)
328 return segment_starts // compress_ratio
329
330 def local_segment_starts(self, device: torch.device) -> torch.Tensor:
331 """Return each local query's global packed-sample start position.
332
333 Args:
334 device: Device on which tensors and prepared geometry are consumed.
335 """
336 cached = self._prepared_starts.get(torch.device(device))
337 if cached is not None:
338 return cached
339 if self.local_query_start < 0 or self.local_query_length < 0 or (
340 self.local_query_start + self.local_query_length > self.global_sequence_length
341 ):
342 raise ValueError("packed local query interval must lie within the global sequence")
343 boundaries = self.cu_seq_lens.to(dtype=torch.long)
344 if boundaries.ndim != 1 or boundaries.numel() < 2 or int(boundaries[0]) != 0:
345 raise ValueError("packed cu_seq_lens must be one-dimensional and start with zero")
346 if int(boundaries[-1]) != self.global_sequence_length:
347 raise ValueError(
348 "packed cu_seq_lens must cover global_sequence_length: "
349 f"{int(boundaries[-1])} versus {self.global_sequence_length}"
350 )
351 if torch.any(boundaries[1:] <= boundaries[:-1]):
352 raise ValueError("packed cu_seq_lens must be strictly increasing")
353 boundaries = boundaries.to(device=device)
354 query_positions = self.local_positions(device).squeeze(0)
355 segment_ids = torch.bucketize(query_positions, boundaries[1:], right=True)
356 return boundaries[:-1].index_select(0, segment_ids).unsqueeze(0)
357
358 def validate_compression_alignment(self, compress_ratio: int) -> None:
359 """Reject packed samples whose compressor groups would cross boundaries.
360
361 Args:
362 compress_ratio: Compression ratio of the consuming layer.
363 """
364 if compress_ratio <= 1 or compress_ratio in self._validated_ratios:
365 return
366 boundaries = self.cu_seq_lens.to(dtype=torch.long)
367 misaligned = boundaries[boundaries.remainder(compress_ratio) != 0]
368 if misaligned.numel():
369 raise ValueError(
370 "packed sample boundaries must align with the CSA2 compression ratio "
371 f"{compress_ratio}; misaligned boundaries={misaligned.tolist()}"
372 )
373
374
375def _rotate_half(tensor: torch.Tensor) -> torch.Tensor:
376 """Rotate adjacent real/imaginary pairs for V4 interleaved RoPE."""
377 first = tensor[..., 0::2]
378 second = tensor[..., 1::2]
379 return torch.stack((-second, first), dim=-1).flatten(-2)
380
381
382def _apply_v41_rope(
383 tensor: torch.Tensor,
384 cos: torch.Tensor,
385 sin: torch.Tensor,
386 unsqueeze_dim: int = 1,
387) -> torch.Tensor:
388 """Apply V4 interleaved RoPE to the trailing rotary channels."""
389 cos = cos.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
390 sin = sin.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
391 rope_dim = cos.shape[-1]
392 pass_through, rotary = tensor[..., :-rope_dim], tensor[..., -rope_dim:]
393 rotary = ((rotary.float() * cos) + (_rotate_half(rotary).float() * sin)).to(tensor.dtype)
394 return torch.cat((pass_through, rotary), dim=-1)
395
396
397def build_sliding_window_indices(
398 batch_size: int,
399 sequence_length: int,
400 window_size: int,
401 device: torch.device,
402 *,
403 query_offset: int = 0,
404 key_length: int | None = None,
405) -> torch.Tensor:
406 """Build fixed-width causal sliding-window indices."""
407 if key_length is None:
408 key_length = sequence_length
409 width = min(key_length, window_size)
410 query = torch.arange(
411 query_offset,
412 query_offset + sequence_length,
413 device=device,
414 ).unsqueeze(-1)
415 offsets = torch.arange(width - 1, -1, -1, device=device)
416 indices = query - offsets
417 indices = indices.masked_fill(indices < 0, -1)
418 return indices.unsqueeze(0).expand(batch_size, -1, -1)
419
420
421def _run_lightning_indexer(
422 query: torch.Tensor, key: torch.Tensor, merge_weight: torch.Tensor,
423 top_k: int, compress_ratio: int, query_offset: int,
424 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
425 minimum_key_indices: torch.Tensor | None, query_segments: tuple | None,
426) -> torch.Tensor:
427 """Run LI V2 with explicit adapter errors and native capability validation."""
428 if reduce_sum is not None:
429 raise NotImplementedError("Fused LightningIndexer does not support TP score reduction before TopK")
430 if minimum_key_indices is not None and query_segments is None:
431 raise ValueError("Fused LightningIndexer requires query_segments when minimum_key_indices is set")
432 validate_indexer_tensors(query, key, merge_weight)
433 if query.device.type != "npu":
434 raise RuntimeError(f"Fused LightningIndexer requires NPU inputs; got device={query.device}")
435 if query_segments is None:
436 length = query.shape[1]
437 raw_end = query_offset + length
438 query_segments = ((0, length, 0, raw_end // compress_ratio, raw_end % compress_ratio),)
439 return lightning_indexer(query, key, merge_weight, compress_ratio, top_k, query_segments)
440
441
442@torch.no_grad()
443def compressed_causal_topk(
444 query: torch.Tensor,
445 key: torch.Tensor,
446 merge_weight: torch.Tensor,
447 *,
448 compress_ratio: int,
449 sparse_count: int,
450 query_offset: int = 0,
451 query_chunk_size: int = 256,
452 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
453 minimum_key_indices: torch.Tensor | None = None,
454 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
455 use_fused: bool = False,
456) -> torch.Tensor:
457 """Select compressed positions with V4.1's ratio-aware causal rule.
458
459 CSA2 keys represent closed groups of ``compress_ratio`` source tokens.
460 Both LI V2 and the batched reference preserve the source rule
461 ``key < floor((query + 1) / ratio)``. The optional native path requires
462 document-local K prefixes; the reference retains TP score reductions.
463
464 Args:
465 query_segments: Local Q intersections and global compressed K prefixes.
466 use_fused: Select LI V2 and raise if a nonempty call is unsupported.
467 False explicitly selects the numerical reference.
468 """
469 if compress_ratio <= 0:
470 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
471 if query_chunk_size <= 0:
472 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
473 if query.ndim != 4:
474 raise ValueError(f"Indexer query must have shape [B, Q, H, D], got {tuple(query.shape)}")
475 batch_size, sequence_length, _, head_dim = query.shape
476 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
477 raise ValueError(
478 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
479 )
480 if merge_weight.shape != query.shape[:3]:
481 raise ValueError(
482 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
483 )
484 compressed_length = key.shape[1]
485 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
486 raise ValueError("minimum_key_indices must have shape [batch, query]")
487 top_k = min(sparse_count, compressed_length)
488 if top_k <= 0 or sequence_length == 0:
489 return torch.empty(
490 batch_size,
491 sequence_length,
492 max(top_k, 0),
493 dtype=torch.int32,
494 device=query.device,
495 )
496
497 if use_fused:
498 return _run_lightning_indexer(
499 query, key, merge_weight, top_k, compress_ratio, query_offset,
500 reduce_sum, minimum_key_indices, query_segments,
501 )
502
503 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
504 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
505 selected_chunks = []
506 for start in range(0, sequence_length, query_chunk_size):
507 end = min(start + query_chunk_size, sequence_length)
508 # SAC may retain the matmul result; do not mutate its cached tensor.
509 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
510 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
511 if reduce_sum is not None:
512 scores = reduce_sum(scores)
513 visible = (
514 torch.arange(query_offset + start, query_offset + end, device=query.device) + 1
515 ) // compress_ratio
516 scores.masked_fill_(key_positions >= visible.view(1, -1, 1), float("-inf"))
517 if minimum_key_indices is not None:
518 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
519 scores.masked_fill_(key_positions < minimum, float("-inf"))
520 top = scores.topk(top_k, dim=-1, sorted=False)
521 indices = top.indices.masked_fill(~torch.isfinite(top.values), compressed_length)
522 indices = sort_key_indices(indices, compressed_length)
523 indices = indices.masked_fill(indices == compressed_length, -1)
524 selected_chunks.append(indices.to(torch.int32))
525 return torch.cat(selected_chunks, dim=1)
526
527
528def select_candidate_blocks(
529 logits: torch.Tensor,
530 compress_lens: torch.Tensor | int,
531 topk_blocks: int,
532 block_size: int,
533) -> torch.Tensor:
534 """Select the official block-max hierarchical candidate pool.
535
536 The newest reachable partial block is pinned into the candidate set, then
537 the remaining blocks compete by their maximum position score. ``logits``
538 must already contain ``-inf`` at causally unreachable positions.
539 """
540 if topk_blocks <= 0:
541 raise ValueError(f"topk_blocks must be positive, got {topk_blocks}")
542 if block_size <= 0:
543 raise ValueError(f"block_size must be positive, got {block_size}")
544 width = logits.shape[-1]
545 if width == 0:
546 return torch.zeros_like(logits, dtype=torch.bool)
547 padding = -width % block_size
548 scores = torch.nn.functional.pad(logits, (0, padding), value=float("-inf"))
549 scores = scores.unflatten(-1, (-1, block_size)).amax(dim=-1)
550 num_blocks = scores.shape[-1]
551 if isinstance(compress_lens, torch.Tensor):
552 last = (compress_lens.to(device=logits.device) - 1) // block_size
553 else:
554 last = (compress_lens - 1) // block_size
555 block_positions = torch.arange(num_blocks, device=logits.device)
556 scores = scores.masked_fill(block_positions == last, float("inf"))
557 top = scores.topk(min(topk_blocks, num_blocks), dim=-1)
558 keep = torch.zeros_like(scores, dtype=torch.bool)
559 keep.scatter_(-1, top.indices, top.values > float("-inf"))
560 return keep.repeat_interleave(block_size, dim=-1)[..., :width]
561
562
563def select_candidate_block_indices(
564 logits: torch.Tensor,
565 compress_lens: torch.Tensor | int,
566 topk_blocks: int,
567 block_size: int,
568) -> torch.Tensor:
569 """Return compact block ids for the official hierarchical candidates.
570
571 Storing block ids uses ``block_size`` times less cross-layer state than
572 expanded position ids and avoids the full ``[batch, query, key]`` boolean
573 mask used by the released inference reference. Invalid slots are ``-1``.
574 """
575 if topk_blocks <= 0:
576 raise ValueError(f"topk_blocks must be positive, got {topk_blocks}")
577 if block_size <= 0:
578 raise ValueError(f"block_size must be positive, got {block_size}")
579 width = logits.shape[-1]
580 if width == 0:
581 return torch.empty(*logits.shape[:-1], 0, dtype=torch.int32, device=logits.device)
582 padding = -width % block_size
583 scores = torch.nn.functional.pad(logits, (0, padding), value=float("-inf"))
584 scores = scores.unflatten(-1, (-1, block_size)).amax(dim=-1)
585 num_blocks = scores.shape[-1]
586 if isinstance(compress_lens, torch.Tensor):
587 last = (compress_lens.to(device=logits.device) - 1) // block_size
588 else:
589 last = (compress_lens - 1) // block_size
590 block_positions = torch.arange(num_blocks, device=logits.device)
591 scores = scores.masked_fill(block_positions == last, float("inf"))
592 top = scores.topk(min(topk_blocks, num_blocks), dim=-1, sorted=False)
593 return top.indices.masked_fill(top.values == float("-inf"), -1).to(torch.int32)
594
595
596def compressed_causal_candidates(
597 query: torch.Tensor,
598 key: torch.Tensor,
599 merge_weight: torch.Tensor,
600 *,
601 compress_ratio: int,
602 topk_blocks: int,
603 block_size: int,
604 query_offset: int = 0,
605 query_chunk_size: int = 256,
606 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
607 minimum_key_indices: torch.Tensor | None = None,
608) -> torch.Tensor:
609 """Build compact blockwise candidate ids in bounded query chunks."""
610 _, sequence_length, _, _ = query.shape
611 compressed_length = key.shape[1]
612 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
613 raise ValueError("minimum_key_indices must have shape [batch, query]")
614 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
615 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
616 candidate_chunks = []
617 for start in range(0, sequence_length, query_chunk_size):
618 end = min(start + query_chunk_size, sequence_length)
619 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
620 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
621 if reduce_sum is not None:
622 scores = reduce_sum(scores)
623 visible = (
624 torch.arange(query_offset + start, query_offset + end, device=query.device) + 1
625 ) // compress_ratio
626 scores.masked_fill_(key_positions >= visible.view(1, -1, 1), float("-inf"))
627 if minimum_key_indices is not None:
628 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
629 scores.masked_fill_(key_positions < minimum, float("-inf"))
630 candidate_chunks.append(
631 select_candidate_block_indices(
632 scores,
633 visible.view(1, -1, 1),
634 topk_blocks,
635 block_size,
636 )
637 )
638 return torch.cat(candidate_chunks, dim=1)
639
640
641def compressed_causal_topk_and_candidates(
642 query: torch.Tensor,
643 key: torch.Tensor,
644 merge_weight: torch.Tensor,
645 *,
646 compress_ratio: int,
647 sparse_count: int,
648 topk_blocks: int,
649 block_size: int,
650 query_offset: int = 0,
651 query_chunk_size: int = 256,
652 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
653 minimum_key_indices: torch.Tensor | None = None,
654) -> tuple[torch.Tensor, torch.Tensor]:
655 """Select Full-layer Top-K and candidate blocks from one score pass."""
656 if compress_ratio <= 0:
657 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
658 if query_chunk_size <= 0:
659 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
660 batch_size, sequence_length, _, head_dim = query.shape
661 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
662 raise ValueError(
663 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
664 )
665 if merge_weight.shape != query.shape[:3]:
666 raise ValueError(
667 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
668 )
669 compressed_length = key.shape[1]
670 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
671 raise ValueError("minimum_key_indices must have shape [batch, query]")
672 top_k = min(sparse_count, compressed_length)
673 if top_k <= 0:
674 empty_topk = torch.empty(
675 batch_size,
676 sequence_length,
677 0,
678 dtype=torch.int32,
679 device=query.device,
680 )
681 return empty_topk, empty_topk
682
683 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
684 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
685 selected_chunks = []
686 candidate_chunks = []
687 for start in range(0, sequence_length, query_chunk_size):
688 end = min(start + query_chunk_size, sequence_length)
689 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
690 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
691 if reduce_sum is not None:
692 scores = reduce_sum(scores)
693 visible = (
694 torch.arange(query_offset + start, query_offset + end, device=query.device) + 1
695 ) // compress_ratio
696 scores.masked_fill_(key_positions >= visible.view(1, -1, 1), float("-inf"))
697 if minimum_key_indices is not None:
698 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
699 scores.masked_fill_(key_positions < minimum, float("-inf"))
700 top = scores.topk(top_k, dim=-1, sorted=False)
701 indices = top.indices.masked_fill(~torch.isfinite(top.values), compressed_length)
702 indices = indices.sort(dim=-1).values
703 selected_chunks.append(indices.masked_fill(indices == compressed_length, -1).to(torch.int32))
704 candidate_chunks.append(
705 select_candidate_block_indices(
706 scores,
707 visible.view(1, -1, 1),
708 topk_blocks,
709 block_size,
710 )
711 )
712 return torch.cat(selected_chunks, dim=1), torch.cat(candidate_chunks, dim=1)
713
714
715def compressed_candidate_topk(
716 query: torch.Tensor,
717 key: torch.Tensor,
718 merge_weight: torch.Tensor,
719 candidate_blocks: torch.Tensor,
720 *,
721 compress_ratio: int,
722 sparse_count: int,
723 block_size: int,
724 query_offset: int = 0,
725 query_chunk_size: int = 256,
726 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
727 minimum_key_indices: torch.Tensor | None = None,
728) -> torch.Tensor:
729 """Score only hierarchical candidate blocks and return global key ids."""
730 if compress_ratio <= 0:
731 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
732 if block_size <= 0:
733 raise ValueError(f"block_size must be positive, got {block_size}")
734 if query_chunk_size <= 0:
735 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
736 batch_size, sequence_length, _, head_dim = query.shape
737 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
738 raise ValueError(
739 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
740 )
741 if merge_weight.shape != query.shape[:3]:
742 raise ValueError(
743 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
744 )
745 if candidate_blocks.ndim != 3 or candidate_blocks.shape[:2] != query.shape[:2]:
746 raise ValueError(
747 "candidate_blocks must have shape [batch, query, candidate_blocks], "
748 f"got {tuple(candidate_blocks.shape)}"
749 )
750 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
751 raise ValueError("minimum_key_indices must have shape [batch, query]")
752
753 compressed_length = key.shape[1]
754 candidate_width = candidate_blocks.shape[-1] * block_size
755 top_k = min(sparse_count, candidate_width)
756 if top_k <= 0:
757 return torch.empty(
758 batch_size,
759 sequence_length,
760 0,
761 dtype=torch.int32,
762 device=query.device,
763 )
764
765 block_offsets = torch.arange(block_size, device=query.device)
766 batch_indices = torch.arange(batch_size, device=query.device).view(-1, 1, 1)
767 selected_chunks = []
768 for start in range(0, sequence_length, query_chunk_size):
769 end = min(start + query_chunk_size, sequence_length)
770 blocks = candidate_blocks[:, start:end].long()
771 block_positions = blocks.unsqueeze(-1) * block_size + block_offsets
772 positions = block_positions.flatten(-2)
773 visible = (
774 torch.arange(query_offset + start, query_offset + end, device=query.device) + 1
775 ) // compress_ratio
776 valid = (blocks.unsqueeze(-1) >= 0).expand_as(block_positions).flatten(-2)
777 valid = valid & (positions < compressed_length) & (positions < visible.view(1, -1, 1))
778 if minimum_key_indices is not None:
779 valid = valid & (positions >= minimum_key_indices[:, start:end].unsqueeze(-1))
780 safe_positions = positions.clamp(min=0, max=max(compressed_length - 1, 0))
781 selected_key = key[batch_indices, safe_positions].float()
782 dots = torch.einsum(
783 "bchd,bckd->bchk",
784 query[:, start:end].float(),
785 selected_key,
786 ).relu()
787 scores = (dots * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
788 if reduce_sum is not None:
789 scores = reduce_sum(scores)
790 scores.masked_fill_(~valid, float("-inf"))
791 top = scores.topk(top_k, dim=-1, sorted=False)
792 selected = positions.gather(-1, top.indices)
793 selected.masked_fill_(~torch.isfinite(top.values), compressed_length)
794 selected = selected.sort(dim=-1).values
795 selected.masked_fill_(selected == compressed_length, -1)
796 selected_chunks.append(selected.to(torch.int32))
797 return torch.cat(selected_chunks, dim=1)
798
799
800class _SharedCompressedIndexerKLLoss(torch.autograd.Function):
801 """PanGu-style precomputed-gradient KL loss for compressed index keys."""
802
803 @staticmethod
804 def forward(
805 ctx: Any,
806 index_query: torch.Tensor,
807 index_key: torch.Tensor,
808 merge_weight: torch.Tensor,
809 attention_query: torch.Tensor,
810 compressed_key: torch.Tensor,
811 topk_indices: torch.Tensor,
812 sinks: torch.Tensor,
813 attention_scale: float,
814 loss_coeff: float,
815 query_chunk_size: int,
816 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
817 ) -> torch.Tensor:
818 """Compute sparse KL and save only the three Indexer gradients."""
819 batch_size, sequence_length, _, _ = index_query.shape
820 denominator = batch_size * sequence_length
821 total_loss = index_query.new_zeros((), dtype=torch.float32)
822 grad_index_query = torch.zeros_like(index_query, dtype=torch.float32)
823 grad_index_key = torch.zeros_like(index_key, dtype=torch.float32)
824 grad_merge_weight = torch.zeros_like(merge_weight, dtype=torch.float32)
825
826 with torch.no_grad(), torch.autocast(device_type=index_query.device.type, enabled=False):
827 for start in range(0, sequence_length, query_chunk_size):
828 end = min(start + query_chunk_size, sequence_length)
829 selected = topk_indices[:, start:end]
830 valid = selected >= 0
831 valid_rows = valid.any(dim=-1)
832 if not torch.any(valid_rows):
833 continue
834 safe_indices = selected.clamp_min(0).long()
835
836 query_chunk = index_query[:, start:end].float()
837 weight_chunk = merge_weight[:, start:end].float()
838 selected_index_key = gather_selected_keys(index_key, safe_indices).float()
839 index_dots = torch.einsum(
840 "bcid,bckd->bcik", query_chunk, selected_index_key
841 )
842 index_relu = index_dots.relu()
843 index_scores = (index_relu * weight_chunk.unsqueeze(-1)).sum(dim=2)
844 if reduce_sum is not None:
845 index_scores = reduce_sum(index_scores)
846 index_scores.masked_fill_(~valid, -1.0e9)
847
848 selected_attention_key = gather_selected_keys(compressed_key, safe_indices).float()
849 attention_scores = torch.einsum(
850 "bhcd,bckd->bhck",
851 attention_query[:, :, start:end].float(),
852 selected_attention_key,
853 ) * attention_scale
854 attention_scores.masked_fill_(~valid.unsqueeze(1), -1.0e9)
855 sink_logits = sinks.float().view(1, -1, 1, 1).expand(
856 batch_size, -1, end - start, -1
857 )
858 target = torch.cat((attention_scores, sink_logits), dim=-1).softmax(dim=-1)
859 target = target[..., :-1].masked_fill(~valid.unsqueeze(1), 0.0).sum(dim=1)
860 if reduce_sum is not None:
861 target = reduce_sum(target)
862 target = target / target.sum(dim=-1, keepdim=True).clamp_min(
863 torch.finfo(torch.float32).tiny
864 )
865
866 log_prediction = index_scores.log_softmax(dim=-1)
867 target_log = target.clamp_min(torch.finfo(torch.float32).tiny).log()
868 row_loss = (target * (target_log - log_prediction)).sum(dim=-1)
869 total_loss.add_(row_loss[valid_rows].sum() * (loss_coeff / denominator))
870
871 grad_scores = log_prediction.exp() * target.sum(-1, keepdim=True) - target
872 grad_scores.mul_(loss_coeff / denominator)
873 grad_scores.masked_fill_(~valid, 0.0)
874 grad_dots = (
875 grad_scores.unsqueeze(2)
876 * weight_chunk.unsqueeze(-1)
877 * (index_dots > 0).to(index_dots.dtype)
878 )
879 grad_index_query[:, start:end] = torch.einsum(
880 "bcik,bckd->bcid", grad_dots, selected_index_key
881 )
882 grad_merge_weight[:, start:end] = (
883 grad_scores.unsqueeze(2) * index_relu
884 ).sum(dim=-1)
885 selected_key_grad = torch.einsum(
886 "bcik,bcid->bckd", grad_dots, query_chunk
887 )
888 # Preserve the einsum result if SAC cached its storage.
889 selected_key_grad = selected_key_grad.masked_fill(~valid.unsqueeze(-1), 0.0)
890 scatter_indices = safe_indices.unsqueeze(-1).expand_as(selected_key_grad)
891 grad_index_key.scatter_add_(
892 1,
893 scatter_indices.reshape(batch_size, -1, index_key.shape[-1]),
894 selected_key_grad.reshape(batch_size, -1, index_key.shape[-1]),
895 )
896
897 ctx.save_for_backward(grad_index_query, grad_index_key, grad_merge_weight)
898 return total_loss
899
900 @staticmethod
901 def backward(ctx: Any, grad_loss: torch.Tensor) -> tuple:
902 """Return the precomputed Indexer gradients and detach the teacher."""
903 grad_index_query, grad_index_key, grad_merge_weight = ctx.saved_tensors
904 return (
905 grad_index_query * grad_loss,
906 grad_index_key * grad_loss,
907 grad_merge_weight * grad_loss,
908 *((None,) * 8),
909 )
910
911
912def shared_compressed_indexer_kl_loss(
913 index_query: torch.Tensor,
914 index_key: torch.Tensor,
915 merge_weight: torch.Tensor,
916 attention_query: torch.Tensor,
917 compressed_key: torch.Tensor,
918 topk_indices: torch.Tensor,
919 sinks: torch.Tensor,
920 *,
921 attention_scale: float,
922 loss_coeff: float,
923 query_chunk_size: int = 256,
924 tp_context: SharedCompressedAttentionTPContext | None = None,
925 compress_ratio: int = 1,
926 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
927 use_fused: bool = False,
928) -> torch.Tensor:
929 """Compute the sparse-stage DSA KL objective on V4.1 compressed keys.
930
931 The target first softmaxes selected main-attention scores per attention
932 head, then averages and L1-normalizes across heads. Indexer inputs are
933 detached by the caller, so this objective updates only Indexer parameters.
934
935 ``use_fused`` is valid only for ordinary causal TopK selections, without
936 candidate-pool holes. ``query_segments`` supplies each document-local K
937 prefix; its ratio and residual encode the CP query offset for the kernel.
938 ``use_fused=True`` raises for unsupported calls when ``loss_coeff != 0``;
939 False explicitly selects the numerical reference. Native errors propagate.
940 """
941 if query_chunk_size <= 0:
942 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
943 if not loss_coeff:
944 return index_query.sum(dtype=torch.float32) * 0.0
945 if topk_indices.shape[:2] != index_query.shape[:2]:
946 raise ValueError("topk_indices and index_query must share batch/query dimensions")
947 if use_fused:
948 if tp_context is not None:
949 raise NotImplementedError("Fused KL does not support TP score/teacher reduction")
950 if query_segments is None:
951 raise ValueError("Fused KL requires document-local causal query_segments")
952 validate_fused_kl_inputs(index_query, index_key, merge_weight, topk_indices)
953 return sparse_lightning_indexer_kl_loss(
954 index_query, index_key, merge_weight, attention_query.detach(), compressed_key.detach(),
955 topk_indices, sinks.detach(), attention_scale, loss_coeff, query_chunk_size,
956 compress_ratio, query_segments,
957 )
958 return _SharedCompressedIndexerKLLoss.apply(
959 index_query,
960 index_key,
961 merge_weight,
962 attention_query.detach(),
963 compressed_key.detach(),
964 topk_indices,
965 sinks.detach(),
966 attention_scale,
967 loss_coeff,
968 query_chunk_size,
969 None if tp_context is None else tp_context.reduce_sum,
970 )
971
972
973class SharedCompressedDSAIndexer(nn.Module):
974 """Reusable V4.1 compressed Lightning Indexer semantics."""
975
976 # Installed by :meth:`enable_selection_exclude` for a layer that recomputes.
977 # It is written through ``object.__setattr__`` so the exclusion region stays
978 # out of ``modules()`` and the state dict.
979 _selection_region: Callable[..., Any] | None = None
980
981 def __init__(self, module: nn.Module) -> None:
982 """Transfer the source projections without changing checkpoint names."""
983 super().__init__()
984 required = ("q_b_proj", "weights_proj")
985 missing = [name for name in required if not hasattr(module, name)]
986 if missing:
987 raise TypeError(f"shared compressed DSA indexer is missing: {missing}")
988 for name, child in module._modules.items(): # pylint: disable=protected-access
989 self.add_module(name, child)
990 for name, parameter in module._parameters.items(): # pylint: disable=protected-access
991 self.register_parameter(name, parameter)
992 self.use_fused = False
993 self.compress_ratio = module.compress_ratio
994 self.global_num_heads = module.num_heads
995 self.head_dim = module.head_dim
996 self.index_topk = module.index_topk
997 self.owns_key = module.owns_key
998 self.is_candidate_source = module.is_candidate_source
999 self.uses_candidates = module.uses_candidates
1000 self.candidate_topk_blocks = module.candidate_topk_blocks
1001 self.candidate_block_size = module.candidate_block_size
1002 self.loss_coeff = module.loss_coeff
1003 self.query_chunk_size = int(getattr(module, "query_chunk_size", 256))
1004 self.train(module.training)
1005
1006 def _project_index_key(
1007 self,
1008 latent: torch.Tensor | None,
1009 cos: torch.Tensor,
1010 sin: torch.Tensor,
1011 index_key: torch.Tensor | None,
1012 cp_context: SharedCompressedAttentionCPContext | None,
1013 ) -> tuple[torch.Tensor, "SequenceGatherHandle | None"]:
1014 """Project or adopt the index key and launch its CP gather."""
1015 if self.owns_key:
1016 if latent is None:
1017 raise RuntimeError("a Full Indexer requires the compressor latent")
1018 key = self.k_norm(self.wk(latent.detach()))
1019 compressed_length = key.shape[1]
1020 key_cos = cos[:, :compressed_length * self.compress_ratio:self.compress_ratio]
1021 key_sin = sin[:, :compressed_length * self.compress_ratio:self.compress_ratio]
1022 key = _apply_v41_rope(key.unsqueeze(1), key_cos, key_sin).squeeze(1)
1023 key_handle = cp_context.launch(key, 1) if cp_context is not None else None
1024 return key, key_handle
1025 if index_key is None:
1026 raise RuntimeError("a Reindex layer requires index K from the preceding Full layer")
1027 return index_key, None
1028
1029 def forward(
1030 self,
1031 hidden_states: torch.Tensor,
1032 query_residual: torch.Tensor,
1033 latent: torch.Tensor | None,
1034 compress_position_embeddings: tuple[torch.Tensor, torch.Tensor],
1035 *,
1036 index_key: torch.Tensor | None = None,
1037 candidate_blocks: torch.Tensor | None = None,
1038 cp_context: SharedCompressedAttentionCPContext | None = None,
1039 tp_context: SharedCompressedAttentionTPContext | None = None,
1040 query_offset: int = 0,
1041 minimum_key_indices: torch.Tensor | None = None,
1042 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
1043 ) -> SharedCompressedIndexerOutput:
1044 """Project local queries and score them against global compressed keys.
1045
1046 When the enclosing layer is recomputed, :meth:`enable_selection_exclude`
1047 makes the discrete selection a checkpoint exclusion region: it runs in
1048 the original forward and its cached result is reused during the
1049 recomputation, so the chosen key set can never drift and the checkpoint
1050 keeps saving the same tensors in both passes.
1051 """
1052 batch_size, sequence_length, _ = hidden_states.shape
1053 cos, sin = compress_position_embeddings
1054
1055 key, key_handle = self._project_index_key(latent, cos, sin, index_key, cp_context)
1056
1057 query = self.q_b_proj(query_residual.detach())
1058 if query.shape[-1] % self.head_dim:
1059 raise ValueError(
1060 f"Indexer query width {query.shape[-1]} is not divisible by head_dim {self.head_dim}"
1061 )
1062 local_num_heads = query.shape[-1] // self.head_dim
1063 query = query.view(batch_size, sequence_length, local_num_heads, self.head_dim)
1064 query = _apply_v41_rope(query.transpose(1, 2), cos, sin).transpose(1, 2)
1065 merge_weight = self.weights_proj(hidden_states.detach())
1066 if merge_weight.shape[-1] != local_num_heads:
1067 raise ValueError(
1068 "Indexer q_b_proj and weights_proj must use the same TP head shard: "
1069 f"{local_num_heads} query heads versus {merge_weight.shape[-1]} weights"
1070 )
1071 merge_weight = merge_weight * (self.head_dim**-0.5 * self.global_num_heads**-0.5)
1072 reduce_sum = None if tp_context is None else tp_context.reduce_sum
1073 if key_handle is not None:
1074 key = key_handle.wait()
1075 topk_indices, candidate_blocks = self._run_selection(
1076 query, key, merge_weight, candidate_blocks, query_offset,
1077 reduce_sum, minimum_key_indices, query_segments,
1078 )
1079 return SharedCompressedIndexerOutput(
1080 topk_indices=topk_indices,
1081 index_key=key,
1082 candidate_blocks=candidate_blocks,
1083 index_query=query,
1084 merge_weight=merge_weight,
1085 )
1086
1087 def enable_selection_exclude(self) -> None:
1088 """Keep discrete selection fixed when the enclosing layer recomputes."""
1089 object.__setattr__(
1090 self, "_selection_region", checkpoint_exclude_wrapper(self._select_indices)
1091 )
1092
1093 def _run_selection(self, *args: Any, **kwargs: Any) -> tuple[torch.Tensor, torch.Tensor | None]:
1094 """Select directly, or reuse the result retained by the checkpoint region."""
1095 selection = self._select_indices if self._selection_region is None else self._selection_region
1096 return selection(*args, **kwargs)
1097
1098 def _select_indices(
1099 self, query: torch.Tensor, key: torch.Tensor, merge_weight: torch.Tensor,
1100 candidate_blocks: torch.Tensor | None, query_offset: int,
1101 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
1102 minimum_key_indices: torch.Tensor | None, query_segments: tuple | None,
1103 ) -> tuple[torch.Tensor, torch.Tensor | None]:
1104 """Select the requested implementation without changing candidate policies."""
1105 if self.use_fused and (self.is_candidate_source or self.uses_candidates):
1106 raise NotImplementedError("V4.1 fused Indexer does not support candidate-pool selection")
1107 if self.is_candidate_source:
1108 topk_indices, candidate_blocks = compressed_causal_topk_and_candidates(
1109 query,
1110 key,
1111 merge_weight,
1112 compress_ratio=self.compress_ratio,
1113 sparse_count=self.index_topk,
1114 topk_blocks=self.candidate_topk_blocks,
1115 block_size=self.candidate_block_size,
1116 query_offset=query_offset,
1117 query_chunk_size=self.query_chunk_size,
1118 reduce_sum=reduce_sum,
1119 minimum_key_indices=minimum_key_indices,
1120 )
1121 elif self.uses_candidates:
1122 if candidate_blocks is None:
1123 raise RuntimeError("a hierarchical Reindex layer requires candidate block ids")
1124 topk_indices = compressed_candidate_topk(
1125 query,
1126 key,
1127 merge_weight,
1128 candidate_blocks,
1129 compress_ratio=self.compress_ratio,
1130 sparse_count=self.index_topk,
1131 block_size=self.candidate_block_size,
1132 query_offset=query_offset,
1133 query_chunk_size=self.query_chunk_size,
1134 reduce_sum=reduce_sum,
1135 minimum_key_indices=minimum_key_indices,
1136 )
1137 else:
1138 candidate_blocks = None
1139 topk_indices = compressed_causal_topk(
1140 query,
1141 key,
1142 merge_weight,
1143 compress_ratio=self.compress_ratio,
1144 sparse_count=self.index_topk,
1145 query_offset=query_offset,
1146 query_chunk_size=self.query_chunk_size,
1147 reduce_sum=reduce_sum,
1148 minimum_key_indices=minimum_key_indices,
1149 query_segments=query_segments,
1150 use_fused=self.use_fused,
1151 )
1152 return topk_indices, candidate_blocks
1153
1154
1155def reference_sparse_attention(
1156 query: torch.Tensor,
1157 key_value: torch.Tensor,
1158 sparse_indices: torch.Tensor,
1159 sinks: torch.Tensor,
1160 scale: float,
1161) -> torch.Tensor:
1162 """Compute sink attention with eager Torch operations on any device.
1163
1164 Args:
1165 query: Main queries [batch, heads, local queries, head dimension].
1166 key_value: Shared K=V bank [batch, 1, keys, head dimension].
1167 sparse_indices: Visible bank IDs [batch, local queries, selections];
1168 negative entries are padding and repeated IDs count only once.
1169 sinks: Learnable scalar sink logits [heads], with zero-valued output.
1170 scale: Main attention logit scale.
1171
1172 Returns:
1173 Attention values [batch, local queries, heads, head dimension].
1174 Softmax uses FP32; matrix multiplications retain the input precision.
1175
1176 Note:
1177 This reference materializes a dense query/key mask and scores. It is
1178 intended for explicit unfused comparisons, not long-sequence efficiency.
1179 """
1180 batch_size, num_heads, sequence_length, _ = query.shape
1181 key_length = key_value.shape[2]
1182 valid = sparse_indices >= 0
1183 safe_indices = sparse_indices.clamp_min(0).long()
1184 allowed_counts = torch.zeros(
1185 batch_size,
1186 sequence_length,
1187 key_length,
1188 dtype=torch.int32,
1189 device=query.device,
1190 )
1191 allowed_counts.scatter_add_(2, safe_indices, valid.to(torch.int32))
1192 allowed = allowed_counts > 0
1193 logits = torch.matmul(query, key_value.transpose(-1, -2)) * scale
1194 logits = logits.masked_fill(~allowed.unsqueeze(1), float("-inf"))
1195 sink_logits = sinks.view(1, num_heads, 1, 1).expand(batch_size, -1, sequence_length, -1)
1196 probabilities = torch.cat((logits, sink_logits), dim=-1).softmax(dim=-1, dtype=torch.float32)
1197 return torch.matmul(probabilities[..., :-1].to(key_value.dtype), key_value).transpose(1, 2)
1198
1199
1200class SharedCompressedDSAAttentionBase(nn.Module):
1201 """Shared V4.1 attention semantics with selectable reference execution."""
1202
1203 def __init__(
1204 self,
1205 module: nn.Module,
1206 *,
1207 replace_indexer: bool,
1208 use_fused_ops: bool,
1209 ) -> None:
1210 """Transfer source state and configure the execution implementation."""
1211 super().__init__()
1212 for name, child in module._modules.items(): # pylint: disable=protected-access
1213 self.add_module(name, child)
1214 for name, parameter in module._parameters.items(): # pylint: disable=protected-access
1215 self.register_parameter(name, parameter)
1216 indexer = getattr(self, "indexer", None)
1217 if (
1218 replace_indexer
1219 and indexer is not None
1220 and not isinstance(indexer, SharedCompressedDSAIndexer)
1221 ):
1222 self.indexer = SharedCompressedDSAIndexer(indexer)
1223 self.config = module.config
1224 self.layer_idx = module.layer_idx
1225 self.num_heads = module.num_heads
1226 self.num_key_value_groups = module.num_key_value_groups
1227 self.num_groups = module.config.o_groups
1228 self.compress_ratio = module.compress_ratio
1229 self.is_kv_source = module.is_kv_source
1230 self.is_index_source = module.is_index_source
1231 self.kv_source_layer_idx = module.kv_source_layer_idx
1232 self.index_source_layer_idx = module.index_source_layer_idx
1233 self.candidate_source_layer_idx = module.candidate_source_layer_idx
1234 self.head_dim = module.head_dim
1235 self.rope_head_dim = module.config.qk_rope_head_dim
1236 self.sliding_window = module.sliding_window
1237 self.scaling = module.scaling
1238 self.use_fused_ops = use_fused_ops
1239 if self.is_index_source:
1240 self.indexer.use_fused = use_fused_ops
1241 self.train(module.training)
1242
1243 @staticmethod
1244 def _validate_cp_inputs(
1245 cp_context: SharedCompressedAttentionCPContext,
1246 position_ids: torch.Tensor,
1247 sequence_length: int,
1248 compress_ratio: int,
1249 is_kv_source: bool,
1250 ) -> tuple[int, int]:
1251 """Validate contiguous CP shards and return global query geometry."""
1252 if cp_context.size <= 1 or not 0 <= cp_context.rank < cp_context.size:
1253 raise ValueError(
1254 "shared compressed attention CP requires size > 1 and "
1255 f"0 <= rank < size, got rank={cp_context.rank}, size={cp_context.size}"
1256 )
1257 if is_kv_source and sequence_length % compress_ratio:
1258 raise ValueError(
1259 "compressed-KV CP requires every local sequence shard to divide "
1260 f"compress_ratio={compress_ratio}, got {sequence_length}"
1261 )
1262 query_offset = cp_context.rank * sequence_length
1263 if not torch.all(position_ids[..., 0] == query_offset):
1264 raise ValueError(
1265 "shared compressed attention CP requires contiguous global position_ids; "
1266 f"rank {cp_context.rank} expected first position {query_offset}"
1267 )
1268 return query_offset, sequence_length * cp_context.size
1269
1270 def _project_raw_kv(
1271 self,
1272 hidden_states: torch.Tensor,
1273 cos: torch.Tensor,
1274 sin: torch.Tensor,
1275 cp_context: SharedCompressedAttentionCPContext | None,
1276 ) -> tuple[torch.Tensor | None, SequenceGatherHandle | None]:
1277 """Project raw K=V and launch CP communication before query GEMMs."""
1278 key_value = self.kv_norm(self.kv_proj(hidden_states)).unsqueeze(1)
1279 key_value = _apply_v41_rope(key_value, cos, sin)
1280 if cp_context is None:
1281 return key_value, None
1282 return None, cp_context.launch(key_value, 2)
1283
1284 def _project_query(
1285 self,
1286 hidden_states: torch.Tensor,
1287 cos: torch.Tensor,
1288 sin: torch.Tensor,
1289 ) -> tuple[torch.Tensor, torch.Tensor]:
1290 """Project and normalize the local query heads."""
1291 batch_size, sequence_length, _ = hidden_states.shape
1292 query_residual = self.q_a_norm(self.q_a_proj(hidden_states))
1293 query = self.q_b_proj(query_residual).view(batch_size, sequence_length, -1, self.head_dim)
1294 query = query.transpose(1, 2)
1295 return query_residual, _apply_v41_rope(query, cos, sin)
1296
1297 @staticmethod
1298 def _validate_forward_inputs(
1299 hidden_states: torch.Tensor,
1300 attention_mask: torch.Tensor | None,
1301 past_key_values: Any | None,
1302 kwargs: dict[str, Any],
1303 ) -> tuple[
1304 "SharedCompressedAttentionState",
1305 "SharedCompressedPackedSequence | None",
1306 "SharedCompressedAttentionCPContext | None",
1307 "SharedCompressedAttentionTPContext | None",
1308 int,
1309 int,
1310 ]:
1311 """Pop and validate the shared-attention keyword arguments."""
1312 if past_key_values is not None:
1313 raise NotImplementedError("shared compressed DSA training does not support KV cache")
1314 shared_state = kwargs.pop("shared_attention_state", None)
1315 if not isinstance(shared_state, SharedCompressedAttentionState):
1316 raise ValueError("shared_attention_state is required for shared compressed DSA")
1317 if attention_mask is not None:
1318 raise NotImplementedError(
1319 "shared compressed DSA consumes compact packed_seq_params instead of a dense mask"
1320 )
1321 batch_size, sequence_length, _ = hidden_states.shape
1322 packed_sequence = kwargs.pop("packed_seq_params", None)
1323 if packed_sequence is not None and not isinstance(
1324 packed_sequence, SharedCompressedPackedSequence
1325 ):
1326 raise TypeError(
1327 "packed_seq_params must be SharedCompressedPackedSequence, "
1328 f"got {type(packed_sequence).__name__}"
1329 )
1330 cp_context = kwargs.pop("shared_attention_cp_context", None)
1331 if cp_context is not None and not isinstance(cp_context, SharedCompressedAttentionCPContext):
1332 raise TypeError(
1333 "shared_attention_cp_context must be SharedCompressedAttentionCPContext, "
1334 f"got {type(cp_context).__name__}"
1335 )
1336 tp_context = kwargs.pop("shared_attention_tp_context", None)
1337 if tp_context is not None and not isinstance(tp_context, SharedCompressedAttentionTPContext):
1338 raise TypeError(
1339 "shared_attention_tp_context must be SharedCompressedAttentionTPContext, "
1340 f"got {type(tp_context).__name__}"
1341 )
1342 return shared_state, packed_sequence, cp_context, tp_context, batch_size, sequence_length
1343
1344 def _resolve_packed_segments(
1345 self,
1346 packed_sequence: "SharedCompressedPackedSequence | None",
1347 batch_size: int,
1348 sequence_length: int,
1349 query_offset: int,
1350 global_sequence_length: int,
1351 device: torch.device,
1352 ) -> tuple[torch.Tensor | None, torch.Tensor | None, tuple]:
1353 """Validate packed document boundaries and derive local Q/K segments."""
1354 segment_starts = None
1355 minimum_key_indices = None
1356 ratio = max(1, self.compress_ratio)
1357 raw_end = query_offset + sequence_length
1358 query_segments = ((0, sequence_length, 0, raw_end // ratio, raw_end % ratio),)
1359 if packed_sequence is not None:
1360 if batch_size != 1:
1361 raise ValueError("V4.1 compact packed attention currently requires micro_batch_size=1")
1362 if (
1363 packed_sequence.local_query_start != query_offset
1364 or packed_sequence.local_query_length != sequence_length
1365 or packed_sequence.global_sequence_length != global_sequence_length
1366 ):
1367 packed_shard = (
1368 packed_sequence.local_query_start,
1369 packed_sequence.local_query_length,
1370 packed_sequence.global_sequence_length,
1371 )
1372 raise ValueError(
1373 "packed sequence geometry does not match the local CP shard: "
1374 f"packed={packed_shard}, "
1375 f"attention={(query_offset, sequence_length, global_sequence_length)}"
1376 )
1377 packed_sequence = packed_sequence.prepare(device, (self.compress_ratio,))
1378 segment_starts = packed_sequence.local_segment_starts(device)
1379 if self.compress_ratio:
1380 minimum_key_indices = packed_sequence.minimum_key_indices(device, self.compress_ratio)
1381 query_segments = packed_sequence.indexer_segments(ratio)
1382 return segment_starts, minimum_key_indices, query_segments
1383
1384 def _compute_compressed_kv(
1385 self,
1386 hidden_states: torch.Tensor,
1387 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
1388 shared_state: "SharedCompressedAttentionState",
1389 cp_context: "SharedCompressedAttentionCPContext | None",
1390 ) -> tuple[torch.Tensor | None, "SequenceGatherHandle | None"]:
1391 """Compress local hidden states and publish or launch the compressed KV.
1392
1393 The compressed K=V reaches its consumers - the producing Full layer
1394 included - through the shared state, so only the publish (or the CP
1395 gather handle that feeds it) leaves this call.
1396 """
1397 latent = None
1398 compressed_handle = None
1399 if self.is_kv_source:
1400 latent, compressed = self.compressor(hidden_states, position_embeddings["compress"])
1401 if cp_context is None:
1402 if not is_recomputing():
1403 shared_state.publish_compressed_kv(self.layer_idx, compressed)
1404 else:
1405 compressed_handle = cp_context.launch(compressed, 1)
1406 return latent, compressed_handle
1407
1408 def _run_indexer(
1409 self,
1410 hidden_states: torch.Tensor,
1411 query_residual: torch.Tensor,
1412 latent: torch.Tensor | None,
1413 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
1414 shared_state: "SharedCompressedAttentionState",
1415 cp_context: "SharedCompressedAttentionCPContext | None",
1416 tp_context: "SharedCompressedAttentionTPContext | None",
1417 query_offset: int,
1418 minimum_key_indices: torch.Tensor | None,
1419 query_segments: tuple,
1420 ) -> Any | None:
1421 """Run the indexer on index-source layers and publish its forward outputs."""
1422 if not self.is_index_source:
1423 return None
1424 index_key = (
1425 None
1426 if self.indexer.owns_key
1427 else shared_state.require_index_key(self.kv_source_layer_idx, self.layer_idx)
1428 )
1429 candidate_blocks = (
1430 shared_state.require_candidate_blocks(
1431 self.candidate_source_layer_idx,
1432 self.layer_idx,
1433 )
1434 if self.indexer.uses_candidates else None
1435 )
1436 indexer_output = self.indexer(
1437 hidden_states,
1438 query_residual,
1439 latent,
1440 position_embeddings["compress"],
1441 index_key=index_key,
1442 candidate_blocks=candidate_blocks,
1443 cp_context=cp_context,
1444 tp_context=tp_context,
1445 query_offset=query_offset,
1446 minimum_key_indices=minimum_key_indices,
1447 query_segments=query_segments,
1448 )
1449 if is_recomputing():
1450 # A recompute must not publish again: its consumers read the entries
1451 # the forward pass wrote, and a second write would break the
1452 # single-publisher contract.
1453 return indexer_output
1454 if self.indexer.owns_key:
1455 shared_state.publish_index_key(self.layer_idx, indexer_output.index_key)
1456 shared_state.publish_topk_indices(self.layer_idx, indexer_output.topk_indices)
1457 if indexer_output.candidate_blocks is not None:
1458 shared_state.publish_candidate_blocks(
1459 self.layer_idx,
1460 indexer_output.candidate_blocks,
1461 )
1462 return indexer_output
1463
1464 def _build_sparse_indices(
1465 self,
1466 key_value: torch.Tensor,
1467 compressed_kv: torch.Tensor | None,
1468 topk_indices: torch.Tensor | None,
1469 sequence_length: int,
1470 query_offset: int,
1471 global_sequence_length: int,
1472 segment_starts: torch.Tensor | None,
1473 ) -> tuple[torch.Tensor, torch.Tensor]:
1474 """Assemble dense-reference banks only after reference dispatch is selected."""
1475 window = build_sliding_window_indices(
1476 key_value.shape[0], sequence_length, self.sliding_window, key_value.device,
1477 query_offset=query_offset, key_length=global_sequence_length,
1478 )
1479 if segment_starts is not None:
1480 window.masked_fill_(window < segment_starts.unsqueeze(-1), -1)
1481 if compressed_kv is None:
1482 return key_value, window
1483 combined_key_value = torch.cat((key_value, compressed_kv.unsqueeze(1)), dim=2)
1484 compressed_indices = torch.where(topk_indices >= 0, topk_indices.long() + global_sequence_length, -1)
1485 return combined_key_value, torch.cat((window, compressed_indices), dim=-1)
1486
1487 def _apply_indexer_loss(
1488 self,
1489 indexer_output: Any | None,
1490 query: torch.Tensor,
1491 compressed_kv: torch.Tensor | None,
1492 tp_context: "SharedCompressedAttentionTPContext | None",
1493 query_segments: tuple,
1494 ) -> torch.Tensor:
1495 """Auto-scale the query by the indexer KL loss when configured."""
1496 if (
1497 indexer_output is not None
1498 and self.training
1499 and self.indexer.loss_coeff
1500 and compressed_kv is not None
1501 ):
1502 indexer_loss = shared_compressed_indexer_kl_loss(
1503 indexer_output.index_query,
1504 indexer_output.index_key,
1505 indexer_output.merge_weight,
1506 query,
1507 compressed_kv,
1508 indexer_output.topk_indices,
1509 self.sinks,
1510 attention_scale=self.scaling,
1511 loss_coeff=self.indexer.loss_coeff,
1512 query_chunk_size=self.indexer.query_chunk_size,
1513 tp_context=tp_context,
1514 compress_ratio=self.compress_ratio,
1515 query_segments=query_segments,
1516 use_fused=self.use_fused_ops,
1517 )
1518 query = aux_loss_auto_scale(query, indexer_loss)
1519 return query
1520
1521 def _run_sparse_attention(
1522 self, query: torch.Tensor, key_value: torch.Tensor, compressed_kv: torch.Tensor | None,
1523 topk_indices: torch.Tensor | None, query_offset: int, global_sequence_length: int,
1524 segment_starts: torch.Tensor | None, query_segments: tuple,
1525 ) -> torch.Tensor:
1526 """Run the chosen implementation; unsupported explicit fusion always raises."""
1527 if self.use_fused_ops:
1528 if not (self.candidate_source_layer_idx is None
1529 and (topk_indices is None or topk_indices.shape[-1] == min(512, compressed_kv.shape[1]))
1530 and supports_fused_attention_inputs(query, self.compress_ratio, self.sliding_window,
1531 0 if topk_indices is None else topk_indices.shape[-1])):
1532 raise RuntimeError(
1533 "V4.1 fused attention is required but this call is unsupported: "
1534 f"layer={self.layer_idx}, device={query.device}, dtype={query.dtype}, Q={tuple(query.shape)}, "
1535 f"ratio={self.compress_ratio}, window={self.sliding_window}. "
1536 "Check the SMLA training extension and native input contract; no reference fallback was run."
1537 )
1538 return sparse_flash_mla(
1539 query, key_value, compressed_kv, topk_indices, self.sinks, self.scaling,
1540 self.compress_ratio, self.sliding_window, query_offset, query_segments,
1541 )
1542 combined_key_value, sparse_indices = self._build_sparse_indices(
1543 key_value, compressed_kv, topk_indices, query.shape[2], query_offset,
1544 global_sequence_length, segment_starts,
1545 )
1546 return reference_sparse_attention(query, combined_key_value, sparse_indices, self.sinks, self.scaling)
1547
1548 def _project_output(
1549 self,
1550 attention_output: torch.Tensor,
1551 cos: torch.Tensor,
1552 sin: torch.Tensor,
1553 batch_size: int,
1554 sequence_length: int,
1555 ) -> torch.Tensor:
1556 """Undo RoPE and apply the grouped low-rank output projection."""
1557 attention_output = _apply_v41_rope(attention_output.transpose(1, 2), cos, -sin).transpose(1, 2)
1558 grouped = attention_output.reshape(batch_size, sequence_length, self.num_groups, -1)
1559 hidden_per_group = grouped.shape[-1]
1560 output_per_group = self.o_a_proj.weight.shape[0] // self.num_groups
1561 projection = self.o_a_proj.weight.view(
1562 self.num_groups, output_per_group, hidden_per_group
1563 ).transpose(1, 2)
1564 flattened = grouped.reshape(-1, self.num_groups, hidden_per_group).transpose(0, 1)
1565 projected = torch.bmm(flattened, projection).transpose(0, 1)
1566 projected = projected.reshape(batch_size, sequence_length, -1)
1567 return self.o_b_proj(projected)
1568
1569 def forward(
1570 self,
1571 hidden_states: torch.Tensor,
1572 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
1573 position_ids: torch.Tensor,
1574 attention_mask: torch.Tensor | None,
1575 past_key_values: Any | None = None,
1576 **kwargs: Any,
1577 ) -> tuple[torch.Tensor, torch.Tensor | None]:
1578 """Publish or consume compressed KV and execute indexed K=V attention."""
1579 (
1580 shared_state,
1581 packed_sequence,
1582 cp_context,
1583 tp_context,
1584 batch_size,
1585 sequence_length,
1586 ) = self._validate_forward_inputs(hidden_states, attention_mask, past_key_values, kwargs)
1587 query_offset = 0
1588 global_sequence_length = sequence_length
1589 if cp_context is not None:
1590 query_offset, global_sequence_length = self._validate_cp_inputs(
1591 cp_context,
1592 position_ids,
1593 sequence_length,
1594 self.compress_ratio,
1595 self.is_kv_source,
1596 )
1597 segment_starts, minimum_key_indices, query_segments = self._resolve_packed_segments(
1598 packed_sequence,
1599 batch_size,
1600 sequence_length,
1601 query_offset,
1602 global_sequence_length,
1603 hidden_states.device,
1604 )
1605
1606 rope_type = "compress" if self.compress_ratio else "main"
1607 cos, sin = position_embeddings[rope_type]
1608 key_value, raw_kv_handle = self._project_raw_kv(hidden_states, cos, sin, cp_context)
1609 query_residual, query = self._project_query(hidden_states, cos, sin)
1610 latent, compressed_handle = self._compute_compressed_kv(
1611 hidden_states, position_embeddings, shared_state, cp_context
1612 )
1613 indexer_output = self._run_indexer(
1614 hidden_states,
1615 query_residual,
1616 latent,
1617 position_embeddings,
1618 shared_state,
1619 cp_context,
1620 tp_context,
1621 query_offset,
1622 minimum_key_indices,
1623 query_segments,
1624 )
1625
1626 if raw_kv_handle is not None:
1627 key_value = raw_kv_handle.wait()
1628 if compressed_handle is not None:
1629 compressed = compressed_handle.wait()
1630 if not is_recomputing():
1631 shared_state.publish_compressed_kv(self.layer_idx, compressed)
1632 if key_value is None:
1633 raise RuntimeError("raw KV all-gather did not produce a tensor")
1634
1635 compressed_kv = None
1636 topk_indices = None
1637 if self.compress_ratio:
1638 compressed_kv = shared_state.require_compressed_kv(self.kv_source_layer_idx, self.layer_idx)
1639 topk_indices = shared_state.require_topk_indices(self.index_source_layer_idx, self.layer_idx)
1640 query = self._apply_indexer_loss(indexer_output, query, compressed_kv, tp_context, query_segments)
1641 attention_output = self._run_sparse_attention(
1642 query, key_value, compressed_kv, topk_indices, query_offset, global_sequence_length,
1643 segment_starts, query_segments,
1644 )
1645 output = self._project_output(attention_output, cos, sin, batch_size, sequence_length)
1646 return output, None
1647
1648
1649@module_replacement
1650class SharedCompressedDSAAttention(SharedCompressedDSAAttentionBase):
1651 """High-performance V4.1 shared compressed attention replacement."""
1652
1653 def __init__(
1654 self,
1655 module: nn.Module,
1656 module_fqn: str = "",
1657 context: Mapping[str, Any] | None = None,
1658 *,
1659 use_fused_ops: bool = False,
1660 ) -> None:
1661 """Transfer source state and select the implementation used for comparisons.
1662
1663 Args:
1664 module: Source attention module exposing the shared-compressed DSA
1665 structural contract.
1666 module_fqn: Fully qualified source name supplied by replacement.
1667 context: Read-only replacement context supplied by Trainer.
1668 use_fused_ops: Use LI V2, SMLA and fused KL when True,
1669 raising on unsupported calls without automatic fallback.
1670 False selects eager PyTorch attention, Indexer and KL on every
1671 device, without loading optional operator extensions.
1672
1673 Note:
1674 All implementations retain the same parameter and CP/TP contracts.
1675 Selecting native preserves the existing external teacher and scalar
1676 KL. Reuse and disabled KL do not introduce extra operator calls.
1677 Fusion supports ordinary causal r1/r2 training on NPU with BF16
1678 main attention, D=512, SWA=128, Indexer D=128, TopK=512 and TP=1.
1679 Active fused KL requires 8, 16, 32 or 64 Indexer heads. Candidate
1680 pools are unsupported. Packed inputs retain the existing alignment
1681 constraints and are dispatched as separate BSND document segments.
1682 The ops-transformer training extension is loaded on the first
1683 supported fused call; a missing extension raises ImportError.
1684 """
1685 if not isinstance(use_fused_ops, bool):
1686 raise ValueError(f"use_fused_ops must be a bool, got {use_fused_ops!r}")
1687 del module_fqn, context
1688 super().__init__(
1689 module,
1690 replace_indexer=True,
1691 use_fused_ops=use_fused_ops,
1692 )
1693
1694
1695__all__ = [
1696 "SharedCompressedAttentionCPContext",
1697 "SharedCompressedAttentionState",
1698 "SharedCompressedPackedSequence",
1699 "SharedCompressedAttentionTPContext",
1700 "SharedCompressedDSAAttention",
1701 "SharedCompressedDSAAttentionBase",
1702 "SharedCompressedDSAIndexer",
1703 "build_sliding_window_indices",
1704 "compressed_candidate_topk",
1705 "compressed_causal_topk_and_candidates",
1706 "compressed_causal_topk",
1707 "select_candidate_block_indices",
1708 "select_candidate_blocks",
1709 "shared_compressed_indexer_kl_loss",
1710 "reference_sparse_attention",
1711]