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 functools import partial
32from typing import Any, Protocol
33
34import torch # pylint: disable=forbidden-backend-import
35from torch import nn # pylint: disable=forbidden-backend-import
36from torch.utils._pytree import register_pytree_node # pylint: disable=forbidden-backend-import
37
38from hyper_parallel.components.functional.aux_loss import aux_loss_auto_scale
39from hyper_parallel.components.functional.lightning_indexer import (
40 lightning_indexer, sort_key_indices, validate_indexer_tensors,
41)
42from hyper_parallel.components.functional.sparse_flash_mla import (
43 supports_fused_attention_inputs, sparse_flash_mla,
44)
45from hyper_parallel.components.functional.sparse_lightning_indexer_kl_loss_grad import (
46 gather_selected_keys, sparse_lightning_indexer_kl_loss, validate_fused_kl_inputs,
47)
48from hyper_parallel.core.activation_memory.recompute_state import is_recomputing
49from hyper_parallel.core.activation_memory.wrapper import checkpoint_exclude_wrapper
50from hyper_parallel.distributed.context_parallel.compressed_document import (
51 CompressionBoundaryRoute, document_compression_plan,
52)
53from hyper_parallel.distributed.context_parallel.indexer_balance import (
54 resolve_indexer_balance_block_size, validate_indexer_hybrid_threshold,
55)
56from hyper_parallel.distributed.context_parallel.sequence_dispatch import (
57 SequenceDispatchRoute, document_prefix_route, sliding_window_route,
58)
59from hyper_parallel.models.replacement import module_replacement
60
61
62class SequenceGatherHandle(Protocol):
63 """Handle returned by asynchronous differentiable bank communication."""
64
65 def wait(self) -> torch.Tensor:
66 """Wait for communication and return the differentiable result."""
67
68
69@dataclass
70class _DeferredSequenceGather:
71 """Synchronous fallback with the same interface as an async gather."""
72
73 tensor: torch.Tensor
74 sequence_dim: int
75 gather_sequence: Callable[[torch.Tensor, int], torch.Tensor]
76
77 def wait(self) -> torch.Tensor:
78 """Run the fallback gather at the first consumer boundary."""
79 return self.gather_sequence(self.tensor, self.sequence_dim)
80
81
82@dataclass
83class _DeferredBankLaunch:
84 """Keep main K behind Index K in the shared communication queue."""
85
86 launcher: Callable[[], SequenceGatherHandle] | None
87 handle: SequenceGatherHandle | None = None
88
89 def start(self) -> SequenceGatherHandle:
90 """Launch once in the differentiable forward, then release captured inputs."""
91 if self.handle is None:
92 self.handle = self.launcher()
93 self.launcher = None
94 return self.handle
95
96 def wait(self) -> torch.Tensor:
97 """Resolve the same differentiable handle at its first consumer."""
98 return self.start().wait()
99
100
101@dataclass
102class SharedCompressedAttentionState:
103 """Autograd-bearing CSA2 state scoped to one training forward.
104
105 State is addressed by the layer that produced it instead of using one
106 mutable "latest value" slot. This matters for the released V4.1 topology,
107 which has several Full/Reindex groups, and makes a consumer's dependency
108 explicit when activation recomputation revisits modules out of forward
109 order.
110
111 Nothing drops an entry early. The state travels into every layer as one of
112 the checkpoint region's inputs, so the graph keeps it alive from the forward
113 pass to the end of backward, and a layer whose forward is recomputed reads
114 its entries a second time there. A published entry therefore has to stay
115 readable until backward is done with every consumer, which is also why the
116 publishers skip their write while recomputing.
117 """
118
119 compressed_kv_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
120 index_key_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
121 topk_indices_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
122 candidate_blocks_by_source: dict[int, torch.Tensor] = field(default_factory=dict)
123 compressed_kv_offsets: dict[int, int] = field(default_factory=dict)
124
125 @staticmethod
126 def _require(
127 values: dict[int, torch.Tensor],
128 source_layer: int | None,
129 value_name: str,
130 consumer_layer: int,
131 ) -> torch.Tensor:
132 """Return one published tensor or report the broken layer dependency."""
133 if source_layer is None or source_layer not in values:
134 raise RuntimeError(
135 f"layer {consumer_layer} requires {value_name} from source "
136 f"layer {source_layer}, but that source has not run in this forward"
137 )
138 return values[source_layer]
139
140 def publish_compressed_kv(self, source_layer: int, value: torch.Tensor, key_offset: int = 0) -> None:
141 """Publish compressed K=V and its global origin, preserving the autograd graph."""
142 self.compressed_kv_by_source[source_layer] = value
143 self.compressed_kv_offsets[source_layer] = key_offset
144
145 def require_compressed_kv(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
146 """Read compressed K=V from the consumer's configured Full layer."""
147 return self._require(
148 self.compressed_kv_by_source,
149 source_layer,
150 "compressed K=V",
151 consumer_layer,
152 )
153
154 def publish_index_key(self, source_layer: int, value: torch.Tensor) -> None:
155 """Publish the Full Indexer's shared key tensor."""
156 self.index_key_by_source[source_layer] = value
157
158 def require_index_key(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
159 """Read the Indexer key associated with a compressed-KV source."""
160 return self._require(
161 self.index_key_by_source,
162 source_layer,
163 "Indexer key",
164 consumer_layer,
165 )
166
167 def publish_topk_indices(self, source_layer: int, value: torch.Tensor) -> None:
168 """Publish one Full/Reindex layer's token selection."""
169 self.topk_indices_by_source[source_layer] = value
170
171 def require_topk_indices(self, source_layer: int | None, consumer_layer: int) -> torch.Tensor:
172 """Read the latest configured Full/Reindex selection."""
173 return self._require(
174 self.topk_indices_by_source,
175 source_layer,
176 "Top-K indices",
177 consumer_layer,
178 )
179
180 def publish_candidate_blocks(self, source_layer: int, value: torch.Tensor) -> None:
181 """Publish hierarchical candidate blocks from the configured source."""
182 self.candidate_blocks_by_source[source_layer] = value
183
184 def require_candidate_blocks(
185 self,
186 source_layer: int | None,
187 consumer_layer: int,
188 ) -> torch.Tensor:
189 """Read hierarchical candidates for a later Reindex layer."""
190 return self._require(
191 self.candidate_blocks_by_source,
192 source_layer,
193 "candidate blocks",
194 consumer_layer,
195 )
196
197
198@dataclass
199class SharedCompressedIndexerOutput:
200 """Tensors produced by one Full or Reindex Indexer invocation."""
201
202 topk_indices: torch.Tensor
203 index_key: torch.Tensor
204 candidate_blocks: torch.Tensor | None
205 index_query: torch.Tensor
206 merge_weight: torch.Tensor
207
208
209@dataclass(frozen=True)
210class SharedCompressedAttentionCPContext:
211 """Communication for local queries and shared compressed banks.
212
213 ``launch_raw`` exchanges the same-document SWA halo when supplied.
214 Optional ``launch_main`` exchanges main KV document prefixes.
215 Index K retains differentiable all-gather.
216 Contexts without ``launch_raw`` provide the full raw-bank reference.
217 """
218
219 size: int
220 rank: int
221 gather_sequence: Callable[[torch.Tensor, int], torch.Tensor]
222 launch_sequence: Callable[[torch.Tensor, int], SequenceGatherHandle] | None = None
223 exchange_boundary: Callable[[torch.Tensor, CompressionBoundaryRoute], torch.Tensor] | None = None
224 launch_raw: Callable[[torch.Tensor, int, SequenceDispatchRoute], SequenceGatherHandle] | None = None
225 balance_indexer: Callable[..., torch.Tensor] | None = None
226 launch_main: Callable[[torch.Tensor, int, SequenceDispatchRoute], SequenceGatherHandle] | None = None
227 launch_balance: Callable[..., Any] | None = None
228 indexer_priority: bool = False
229
230 def launch(self, tensor: torch.Tensor, sequence_dim: int) -> SequenceGatherHandle:
231 """Launch an async gather or create a deferred synchronous gather."""
232 if self.launch_sequence is not None:
233 return self.launch_sequence(tensor, sequence_dim)
234 return _DeferredSequenceGather(tensor, sequence_dim, self.gather_sequence)
235
236
237@dataclass(frozen=True)
238class SharedCompressedAttentionTPContext:
239 """Tensor-parallel score reduction context for a head-sharded Indexer."""
240
241 size: int
242 rank: int
243 reduce_sum: Callable[[torch.Tensor], torch.Tensor]
244
245
246@dataclass(frozen=True)
247class SharedCompressedPackedSequence:
248 """Compact sample boundaries for one contiguous CP query shard."""
249
250 cu_seq_lens: torch.Tensor
251 local_query_start: int
252 local_query_length: int
253 global_sequence_length: int
254 _prepared_starts: dict[torch.device, torch.Tensor] = field(
255 default_factory=dict, init=False, repr=False, compare=False
256 )
257 _prepared_minima: dict[tuple[torch.device, int], torch.Tensor] = field(
258 default_factory=dict, init=False, repr=False, compare=False
259 )
260 _prepared_bounds: dict[tuple[torch.device, int], torch.Tensor] = field(
261 default_factory=dict, init=False, repr=False, compare=False
262 )
263 _packing_alignment: int = field(default=0, init=False, repr=False, compare=False)
264 _compression_rope: dict[int, tuple] = field(default_factory=dict, init=False, repr=False, compare=False)
265 _snapshot: dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False)
266 _prepared_positions: dict[torch.device, torch.Tensor] = field(
267 default_factory=dict, init=False, repr=False, compare=False
268 )
269 _prepared_masks: dict[torch.device, torch.Tensor] = field(
270 default_factory=dict, init=False, repr=False, compare=False
271 )
272 _indexer_segments: dict[int, tuple[tuple[int, int, int, int, int], ...]] = field(
273 default_factory=dict, init=False, repr=False, compare=False
274 )
275
276 _prepared_sequences: dict[torch.device, torch.Tensor] = field(
277 default_factory=dict, init=False, repr=False, compare=False
278 )
279 _use_prepared_packing_inputs: bool = field(default=False, init=False, repr=False, compare=False)
280 _restored_from_packing_tensors: bool = field(default=False, init=False, repr=False, compare=False)
281
282 def prepare_packing_geometry(self, device: torch.device) -> None:
283 """Prepare CP1 graph metadata independently of compression or indexing.
284
285 The runtime calls this before capture. Eager retains its real boundaries;
286 graph inputs carry token-shaped metadata, not a variable document list.
287 """
288 if self.local_query_start != 0 or self.local_query_length != self.global_sequence_length:
289 return
290 self.segment_start_mask(device)
291 object.__setattr__(self, "_use_prepared_packing_inputs", True)
292
293 def router_metadata(self, device: torch.device) -> tuple[torch.Tensor, int]:
294 """Return document IDs and storage capacity; empty rows do not affect the mean."""
295 count = self.local_query_length if self._restored_from_packing_tensors else self.cu_seq_lens.numel() - 1
296 return self._prepared_sequences[torch.device(device)], count
297
298 def prepare(self, device: torch.device, compress_ratios: tuple[int, ...]) -> SharedCompressedPackedSequence:
299 """Prepare an owned boundary snapshot, reused until the input is mutated.
300
301 Normal in-place tensor updates invalidate the cached snapshot. Existing
302 consumers retain their owned geometry. Prepared tensors are read-only;
303 callers must not mutate boundaries through ``.data`` or external storage.
304 Inference tensors without a version counter are always snapshotted anew.
305
306 Args:
307 device: Device on which tensors and prepared geometry are consumed.
308 compress_ratios: Compression ratios used by the layers in this forward.
309 """
310 device = torch.device(device)
311 # Torch exposes tensor mutation tracking only through its version counter.
312 version = None if self.cu_seq_lens.is_inference() else self.cu_seq_lens._version # pylint: disable=protected-access
313 if version is not None and self._snapshot.get("version") == version:
314 prepared: SharedCompressedPackedSequence = self if self._snapshot.get("owned") else self._snapshot["value"]
315 else:
316 prepared = type(self)(
317 self.cu_seq_lens.detach().clone(), self.local_query_start,
318 self.local_query_length, self.global_sequence_length,
319 )
320 SharedCompressedPackedSequence._mark_owned(prepared)
321 self._snapshot.clear()
322 self._snapshot.update(version=version, value=prepared)
323 SharedCompressedPackedSequence._prepare_owned_metadata(prepared, device, compress_ratios)
324 return prepared
325
326 def _mark_owned(self) -> None:
327 """Record the owned tensor's mutation counter for subsequent layer reuse."""
328 if not self.cu_seq_lens.is_inference():
329 # Torch has no public read API for its tensor mutation counter.
330 self._snapshot.update(owned=True, version=self.cu_seq_lens._version) # pylint: disable=protected-access
331
332 def _prepare_owned_metadata(self, device: torch.device, compress_ratios: tuple[int, ...]) -> None:
333 """Populate caches on the owned snapshot, never on caller-owned boundaries."""
334 starts = self.local_segment_starts(device)
335 self._prepared_starts[device] = starts
336 if device not in self._prepared_sequences:
337 self._prepared_sequences[device] = torch.bucketize(
338 self.local_positions(device).squeeze(0), self.cu_seq_lens.to(device=device)[1:], right=True,
339 ).unsqueeze(0)
340 for ratio in set(compress_ratios):
341 self.validate_compression_ratio(ratio)
342 if ratio > 0 and (device, ratio) not in self._prepared_minima:
343 if self._restored_from_packing_tensors:
344 raise ValueError(
345 "graph packing inputs carry no compressed-key metadata for ratio "
346 f"{ratio}; configure every active model compress ratio on the "
347 "runtime input adapter (compress_ratios) so the graph boundary "
348 "can prepare it"
349 )
350 self._prepared_minima[(device, ratio)] = self.minimum_key_indices(device, ratio)
351
352 def local_positions(self, device: torch.device) -> torch.Tensor:
353 """Return the immutable local interval's global token positions."""
354 device = torch.device(device)
355 if device not in self._prepared_positions:
356 self._prepared_positions[device] = torch.arange(
357 self.local_query_start, self.local_query_start + self.local_query_length, device=device,
358 ).unsqueeze(0)
359 return self._prepared_positions[device]
360
361 def segment_start_mask(self, device: torch.device) -> torch.Tensor:
362 """Reuse the per-batch sample-start mask across model forwards."""
363 device = torch.device(device)
364 if device not in self._prepared_masks:
365 self._prepared_masks[device] = self.local_positions(device) == self.local_segment_starts(device)
366 return self._prepared_masks[device]
367
368 def document_boundaries(self) -> tuple[int, ...]:
369 """Reuse the owned batch's host boundaries for routing across layers."""
370 if "boundaries" not in self._snapshot:
371 self._snapshot["boundaries"] = tuple(self.cu_seq_lens.detach().cpu().tolist())
372 return self._snapshot["boundaries"]
373
374 def indexer_segments(self, ratio: int) -> tuple[tuple[int, int, int, int, int], ...]:
375 """Describe packed Q intervals and completed K prefixes without per-layer synchronization.
376
377 Args:
378 ratio: Compression ratio used to express each document's key prefix.
379 """
380 if self._restored_from_packing_tensors:
381 raise NotImplementedError(
382 "graph packing inputs do not carry Python document segments; the "
383 "reference selection consumes per-token tensor bounds instead"
384 )
385 if ratio not in self._indexer_segments:
386 self.validate_compression_ratio(ratio)
387 boundaries = self.document_boundaries()
388 local_start = self.local_query_start
389 local_end = local_start + self.local_query_length
390 segments = []
391 key_base = 0
392 for sample_start, sample_end in zip(boundaries, boundaries[1:]):
393 start, end = max(local_start, sample_start), min(local_end, sample_end)
394 if start < end:
395 segments.append((start-local_start, end-local_start, key_base,
396 key_base+(end-sample_start)//ratio, (end-sample_start) % ratio))
397 key_base += (sample_end-sample_start)//ratio
398 self._indexer_segments[ratio] = tuple(segments)
399 return self._indexer_segments[ratio]
400
401 def minimum_key_indices(
402 self, device: torch.device, compress_ratio: int, segment_starts: torch.Tensor | None = None,
403 ) -> torch.Tensor:
404 """Return the first compressed key belonging to each query's sample.
405
406 Args:
407 device: Device on which tensors and prepared geometry are consumed.
408 compress_ratio: Compression ratio of the consuming layer.
409 segment_starts: Optional precomputed global sample starts for local queries.
410 """
411 if compress_ratio <= 0:
412 raise ValueError("minimum_key_indices requires a positive compression ratio")
413 cached = self._prepared_minima.get((torch.device(device), compress_ratio))
414 if cached is not None:
415 return cached
416 self.validate_compression_ratio(compress_ratio)
417 del segment_starts
418 device = torch.device(device)
419 # Pure tensor arithmetic from cu_seq_lens, identical for eager and the
420 # graph boundary: count each document's complete blocks, then take the
421 # exclusive cumsum as the document's global compressed-key base.
422 boundaries = self.cu_seq_lens.to(device=device, dtype=torch.long)
423 documents = self._prepared_sequences.get(device)
424 if documents is None:
425 documents = torch.bucketize(
426 self.local_positions(device).squeeze(0), boundaries[1:], right=True,
427 ).unsqueeze(0)
428 counts = (boundaries[1:] - boundaries[:-1]) // compress_ratio
429 offsets = torch.cat((counts.new_zeros(1), counts.cumsum(dim=0)[:-1]))
430 return offsets.index_select(0, documents.squeeze(0)).unsqueeze(0)
431
432 def causal_key_upper_bounds(
433 self, device: torch.device, compress_ratio: int,
434 ) -> torch.Tensor:
435 """Return the exclusive upper bound of compressed keys visible to each query.
436
437 Both execution modes share one derivation: a query at document-local
438 position ``p`` sees compressed keys ``[document base, base + (p+1)//ratio)``.
439 The result is a ``[1, local queries]`` tensor, so graph capture replays it
440 across batches whose document count and lengths differ.
441
442 Args:
443 device: Device on which tensors and prepared geometry are consumed.
444 compress_ratio: Compression ratio of the consuming layer.
445 """
446 if compress_ratio <= 0:
447 raise ValueError("causal_key_upper_bounds requires a positive compression ratio")
448 key = (torch.device(device), compress_ratio)
449 cached = self._prepared_bounds.get(key)
450 if cached is None:
451 device = torch.device(device)
452 minima = self.minimum_key_indices(device, compress_ratio)
453 cached = minima + (
454 (self.local_positions(device) - self.local_segment_starts(device) + 1) // compress_ratio
455 )
456 self._prepared_bounds[key] = cached
457 return cached
458
459 def local_segment_starts(self, device: torch.device) -> torch.Tensor:
460 """Return each local query's global packed-sample start position.
461
462 Args:
463 device: Device on which tensors and prepared geometry are consumed.
464 """
465 cached = self._prepared_starts.get(torch.device(device))
466 if cached is not None:
467 return cached
468 if self.local_query_start < 0 or self.local_query_length < 0 or (
469 self.local_query_start + self.local_query_length > self.global_sequence_length
470 ):
471 raise ValueError("packed local query interval must lie within the global sequence")
472 boundaries = self.cu_seq_lens.to(dtype=torch.long)
473 if boundaries.ndim != 1 or boundaries.numel() < 2 or int(boundaries[0]) != 0:
474 raise ValueError("packed cu_seq_lens must be one-dimensional and start with zero")
475 if int(boundaries[-1]) != self.global_sequence_length:
476 raise ValueError(
477 "packed cu_seq_lens must cover global_sequence_length: "
478 f"{int(boundaries[-1])} versus {self.global_sequence_length}"
479 )
480 if torch.any(boundaries[1:] <= boundaries[:-1]):
481 raise ValueError("packed cu_seq_lens must be strictly increasing")
482 boundaries = boundaries.to(device=device)
483 query_positions = self.local_positions(device).squeeze(0)
484 segment_ids = torch.bucketize(query_positions, boundaries[1:], right=True)
485 return boundaries[:-1].index_select(0, segment_ids).unsqueeze(0)
486
487 @staticmethod
488 def validate_compression_ratio(compress_ratio: int) -> None:
489 """Validate the ratio; complete block planning handles natural document lengths."""
490 if compress_ratio < 0:
491 raise ValueError("compression ratio must be nonnegative")
492
493 def is_compression_aligned(self, compress_ratio: int) -> bool:
494 """Report whether every document boundary lies on a complete-block edge.
495
496 Eager reads the real boundaries; graph packing inputs read the alignment
497 the graph boundary validated before capture. Both answer the same
498 question for the same batch.
499
500 Args:
501 compress_ratio: Compression ratio of the consuming layer.
502 """
503 if compress_ratio <= 1:
504 return True
505 if self._restored_from_packing_tensors:
506 return 0 < self._packing_alignment and self._packing_alignment % compress_ratio == 0
507 return not any(boundary % compress_ratio for boundary in self.document_boundaries())
508
509 def compressed_key_length(self, compress_ratio: int) -> int:
510 """Return the total number of complete compressed keys in the batch.
511
512 For alignment-validated graph packing inputs the contiguous count equals
513 the per-document sum; eager keeps exact arithmetic for natural lengths.
514
515 Args:
516 compress_ratio: Compression ratio of the consuming layer.
517 """
518 ratio = max(1, compress_ratio)
519 if self._restored_from_packing_tensors:
520 return self.local_query_length // ratio
521 boundaries = self.document_boundaries()
522 return sum((end - start) // ratio for start, end in zip(boundaries, boundaries[1:]))
523
524 def block_plan(self, ratio: int, size: int, rank: int) -> Any:
525 """Use the original contiguous compressor fast path when every group is aligned.
526
527 Args:
528 ratio: Compression ratio of the consuming layer.
529 size: Number of contiguous CP shards.
530 rank: Local rank within the CP group.
531
532 Returns:
533 Document-local block plan, or None when contiguous compression suffices.
534 """
535 if self._restored_from_packing_tensors:
536 # The graph boundary rejects misaligned documents before capture, so
537 # contiguous compression is exact and needs no Python-side plan.
538 return None
539 if ratio <= 1 or (self.local_query_length % ratio == 0
540 and all(boundary % ratio == 0 for boundary in self.document_boundaries())):
541 return None
542 return document_compression_plan(self.document_boundaries(), ratio, self.local_query_length, size, rank)
543
544 def compressed_positions(self, plan: Any, embeddings: tuple, context: Any) -> tuple:
545 """Reuse boundary RoPE once per batch/source embedding, retaining its original precision.
546
547 Args:
548 plan: Document-local compression plan for the current CP rank.
549 embeddings: Cosine and sine coefficients for the local raw-token shard.
550 context: CP boundary-dispatch context, or None when no exchange is needed.
551
552 Returns:
553 Cosine and sine coefficients at the first token of each owned block.
554 """
555 cos, sin = embeddings
556 # Preserve cache correctness when caller-owned RoPE tensors are updated in place.
557 versions = tuple(None if item.is_inference() else item._version for item in embeddings) # pylint: disable=protected-access
558 cached = self._compression_rope.get(plan.ratio)
559 if (cached is not None and cached[0] is cos and cached[1] is sin and cached[2] == versions
560 and None not in versions and not cos.requires_grad and not sin.requires_grad):
561 return cached[3]
562 result = plan.positions(cos, sin, context)
563 if not cos.requires_grad and not sin.requires_grad:
564 self._compression_rope[plan.ratio] = (cos, sin, versions, result)
565 return result
566
567 def raw_document_starts(self) -> tuple:
568 """Keep raw token coordinates separate from compact compressed-key IDs."""
569 if self._restored_from_packing_tensors:
570 # Only the fused attention kernel consumes these Python
571 # coordinates; fused execution is gated out of graph mode.
572 return ()
573 first, last = self.local_query_start, self.local_query_start + self.local_query_length
574 return tuple((max(first, start)-first, start)
575 for start, end in zip(self.document_boundaries(), self.document_boundaries()[1:])
576 if max(first, start) < min(last, end))
577
578
579 def pytree_flatten(self) -> tuple[list[Any], tuple]:
580 """Expose packing tensors with a schema independent of document count and S."""
581 if self._use_prepared_packing_inputs:
582 return [self._prepared_starts, self._prepared_positions,
583 self._prepared_masks, self._prepared_sequences,
584 self._prepared_minima], ("packing", self._packing_alignment)
585 return [self.cu_seq_lens], (self.local_query_start, self.local_query_length, self.global_sequence_length)
586
587 @classmethod
588 def pytree_unflatten(cls, children: Any, context: tuple) -> SharedCompressedPackedSequence:
589 """Restore prepared metadata without reading boundary values during capture."""
590 if context[0] != "packing":
591 return cls(children[0], *context)
592 starts, positions, masks, sequences, minima = children
593 reference = next(iter(starts.values()))
594 length = reference.shape[-1]
595 packed = cls(reference.new_empty(0), 0, length, length)
596 packed._prepared_starts.update(starts)
597 packed._prepared_positions.update(positions)
598 packed._prepared_masks.update(masks)
599 packed._prepared_sequences.update(sequences)
600 packed._prepared_minima.update(minima)
601 object.__setattr__(packed, "_packing_alignment", context[1])
602 object.__setattr__(packed, "_use_prepared_packing_inputs", True)
603 object.__setattr__(packed, "_restored_from_packing_tensors", True)
604 packed._mark_owned()
605 return packed
606
607
608class GraphPackedSequence(SharedCompressedPackedSequence):
609 """Tensor-only packing inputs created exclusively at the graph boundary.
610
611 The subclass is a registration marker: only graph-boundary inputs are
612 flattened as pytree nodes, so eager FSDP keeps treating packed inputs as
613 opaque leaves that retain real boundaries and caches. Every semantic lives
614 in the shared base, including the compressed-key metadata that lets the
615 graph carry indexer and compressor execution with the same code path.
616 """
617
618
619register_pytree_node(GraphPackedSequence, GraphPackedSequence.pytree_flatten,
620 GraphPackedSequence.pytree_unflatten)
621
622
623def _rotate_half(tensor: torch.Tensor) -> torch.Tensor:
624 """Rotate adjacent real/imaginary pairs for V4 interleaved RoPE."""
625 first, second = tensor.unflatten(-1, (-1, 2)).unbind(-1)
626 return torch.stack((-second, first), dim=-1).flatten(-2)
627
628
629def _apply_v41_rope(
630 tensor: torch.Tensor,
631 cos: torch.Tensor,
632 sin: torch.Tensor,
633 unsqueeze_dim: int = 1,
634) -> torch.Tensor:
635 """Apply V4 interleaved RoPE to the trailing rotary channels."""
636 cos = cos.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
637 sin = sin.repeat_interleave(2, dim=-1).unsqueeze(unsqueeze_dim)
638 rope_dim = cos.shape[-1]
639 pass_through, rotary = tensor.split((tensor.shape[-1] - rope_dim, rope_dim), dim=-1)
640 rotary = ((rotary.float() * cos) + (_rotate_half(rotary).float() * sin)).to(tensor.dtype)
641 return torch.cat((pass_through, rotary), dim=-1) if pass_through.shape[-1] else rotary
642
643
644def build_sliding_window_indices(
645 batch_size: int,
646 sequence_length: int,
647 window_size: int,
648 device: torch.device,
649 *,
650 query_offset: int = 0,
651 key_length: int | None = None,
652) -> torch.Tensor:
653 """Build fixed-width causal sliding-window indices."""
654 if key_length is None:
655 key_length = sequence_length
656 width = min(key_length, window_size)
657 query = torch.arange(
658 query_offset,
659 query_offset + sequence_length,
660 device=device,
661 ).unsqueeze(-1)
662 offsets = torch.arange(width - 1, -1, -1, device=device)
663 indices = query - offsets
664 indices = indices.masked_fill(indices < 0, -1)
665 return indices.unsqueeze(0).expand(batch_size, -1, -1)
666
667
668def _run_lightning_indexer(
669 query: torch.Tensor, key: torch.Tensor, merge_weight: torch.Tensor,
670 top_k: int, compress_ratio: int, query_offset: int,
671 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
672 minimum_key_indices: torch.Tensor | None, query_segments: tuple | None,
673) -> torch.Tensor:
674 """Run LI V2 with explicit adapter errors and native capability validation."""
675 if reduce_sum is not None:
676 raise NotImplementedError("Fused LightningIndexer does not support TP score reduction before TopK")
677 if minimum_key_indices is not None and query_segments is None:
678 raise ValueError("Fused LightningIndexer requires query_segments when minimum_key_indices is set")
679 validate_indexer_tensors(query, key, merge_weight)
680 if query.device.type != "npu":
681 raise RuntimeError(f"Fused LightningIndexer requires NPU inputs; got device={query.device}")
682 if query_segments is None:
683 length = query.shape[1]
684 raw_end = query_offset + length
685 query_segments = ((0, length, 0, raw_end // compress_ratio, raw_end % compress_ratio),)
686 return lightning_indexer(query, key, merge_weight, compress_ratio, top_k, query_segments)
687
688
689def _causal_key_bounds(
690 query: torch.Tensor, ratio: int, query_offset: int,
691 minimum_key_indices: torch.Tensor | None, segments: tuple | None,
692 causal_upper: torch.Tensor | None = None,
693) -> tuple[torch.Tensor, torch.Tensor | None]:
694 """Derive visibility from original query positions within each independently compressed document.
695
696 The returned upper bound always has shape ``[1, queries]`` (shared across
697 the batch), so chunked consumers slice it along the sequence dimension.
698 ``causal_upper`` is the packed per-query exclusive bound shared by eager and
699 graph execution; Python ``segments`` remain available for eager callers and
700 the fused native kernels.
701 """
702 batch_size, length = query.shape[:2]
703 if causal_upper is not None:
704 return causal_upper, minimum_key_indices
705 if segments is None:
706 positions = torch.arange(query_offset, query_offset + length, device=query.device)
707 return ((positions + 1) // ratio).unsqueeze(0), minimum_key_indices
708 upper = torch.empty(length, device=query.device, dtype=torch.long)
709 lower = torch.empty_like(upper)
710 for begin, end, key_start, key_end, residual in segments:
711 first = ratio * key_end + residual - (end - begin)
712 positions = torch.arange(first, first + end - begin, device=query.device)
713 upper[begin:end] = (positions + 1) // ratio
714 lower[begin:end] = key_start
715 return upper.unsqueeze(0), lower.unsqueeze(0).expand(batch_size, -1)
716
717
718@torch.no_grad()
719def compressed_causal_topk(
720 query: torch.Tensor,
721 key: torch.Tensor,
722 merge_weight: torch.Tensor,
723 *,
724 compress_ratio: int,
725 sparse_count: int,
726 query_offset: int = 0,
727 query_chunk_size: int = 256,
728 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
729 minimum_key_indices: torch.Tensor | None = None,
730 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
731 causal_upper: torch.Tensor | None = None,
732 use_fused: bool = False,
733) -> torch.Tensor:
734 """Select compressed positions with V4.1's ratio-aware causal rule.
735
736 CSA2 keys represent closed groups of ``compress_ratio`` source tokens.
737 Both LI V2 and the batched reference preserve the source rule
738 ``key < floor((query + 1) / ratio)``. The optional native path requires
739 document-local K prefixes; the reference retains TP score reductions.
740
741 Args:
742 query_segments: Local Q intersections and global compressed K prefixes.
743 causal_upper: Packed per-query exclusive causal bound shared by eager
744 and graph execution; supersedes ``query_segments`` for reference
745 selection when provided.
746 use_fused: Select LI V2 and raise if a nonempty call is unsupported.
747 False explicitly selects the numerical reference.
748 """
749 if compress_ratio <= 0:
750 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
751 if query_chunk_size <= 0:
752 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
753 if query.ndim != 4:
754 raise ValueError(f"Indexer query must have shape [B, Q, H, D], got {tuple(query.shape)}")
755 batch_size, sequence_length, _, head_dim = query.shape
756 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
757 raise ValueError(
758 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
759 )
760 if merge_weight.shape != query.shape[:3]:
761 raise ValueError(
762 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
763 )
764 compressed_length = key.shape[1]
765 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
766 raise ValueError("minimum_key_indices must have shape [batch, query]")
767 top_k = min(sparse_count, compressed_length)
768 if top_k <= 0 or sequence_length == 0:
769 return torch.empty(
770 batch_size,
771 sequence_length,
772 max(top_k, 0),
773 dtype=torch.int32,
774 device=query.device,
775 )
776
777 if use_fused:
778 return _run_lightning_indexer(
779 query, key, merge_weight, top_k, compress_ratio, query_offset,
780 reduce_sum, minimum_key_indices, query_segments,
781 )
782
783 visible_keys, minimum_key_indices = _causal_key_bounds(
784 query, compress_ratio, query_offset, minimum_key_indices, query_segments, causal_upper,
785 )
786 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
787 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
788 selected_chunks = []
789 for start in range(0, sequence_length, query_chunk_size):
790 end = min(start + query_chunk_size, sequence_length)
791 # SAC may retain the matmul result; do not mutate its cached tensor.
792 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
793 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
794 if reduce_sum is not None:
795 scores = reduce_sum(scores)
796 # Bounds are [1, S]; slice the query axis so the mask covers this chunk.
797 visible = visible_keys[:, start:end].unsqueeze(-1)
798 scores.masked_fill_(key_positions >= visible, float("-inf"))
799 if minimum_key_indices is not None:
800 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
801 scores.masked_fill_(key_positions < minimum, float("-inf"))
802 top = scores.topk(top_k, dim=-1, sorted=False)
803 indices = top.indices.masked_fill(~torch.isfinite(top.values), compressed_length)
804 indices = sort_key_indices(indices, compressed_length)
805 indices = indices.masked_fill(indices == compressed_length, -1)
806 selected_chunks.append(indices.to(torch.int32))
807 return torch.cat(selected_chunks, dim=1)
808
809
810def select_candidate_blocks(
811 logits: torch.Tensor,
812 compress_lens: torch.Tensor | int,
813 topk_blocks: int,
814 block_size: int,
815) -> torch.Tensor:
816 """Select the official block-max hierarchical candidate pool.
817
818 The newest reachable partial block is pinned into the candidate set, then
819 the remaining blocks compete by their maximum position score. ``logits``
820 must already contain ``-inf`` at causally unreachable positions.
821 """
822 if topk_blocks <= 0:
823 raise ValueError(f"topk_blocks must be positive, got {topk_blocks}")
824 if block_size <= 0:
825 raise ValueError(f"block_size must be positive, got {block_size}")
826 width = logits.shape[-1]
827 if width == 0:
828 return torch.zeros_like(logits, dtype=torch.bool)
829 padding = -width % block_size
830 scores = torch.nn.functional.pad(logits, (0, padding), value=float("-inf"))
831 scores = scores.unflatten(-1, (-1, block_size)).amax(dim=-1)
832 num_blocks = scores.shape[-1]
833 if isinstance(compress_lens, torch.Tensor):
834 last = (compress_lens.to(device=logits.device) - 1) // block_size
835 else:
836 last = (compress_lens - 1) // block_size
837 block_positions = torch.arange(num_blocks, device=logits.device)
838 scores = scores.masked_fill(block_positions == last, float("inf"))
839 top = scores.topk(min(topk_blocks, num_blocks), dim=-1)
840 keep = torch.zeros_like(scores, dtype=torch.bool)
841 keep.scatter_(-1, top.indices, top.values > float("-inf"))
842 return keep.repeat_interleave(block_size, dim=-1)[..., :width]
843
844
845def select_candidate_block_indices(
846 logits: torch.Tensor,
847 compress_lens: torch.Tensor | int,
848 topk_blocks: int,
849 block_size: int,
850) -> torch.Tensor:
851 """Return compact block ids for the official hierarchical candidates.
852
853 Storing block ids uses ``block_size`` times less cross-layer state than
854 expanded position ids and avoids the full ``[batch, query, key]`` boolean
855 mask used by the released inference reference. Invalid slots are ``-1``.
856 """
857 if topk_blocks <= 0:
858 raise ValueError(f"topk_blocks must be positive, got {topk_blocks}")
859 if block_size <= 0:
860 raise ValueError(f"block_size must be positive, got {block_size}")
861 width = logits.shape[-1]
862 if width == 0:
863 return torch.empty(*logits.shape[:-1], 0, dtype=torch.int32, device=logits.device)
864 padding = -width % block_size
865 scores = torch.nn.functional.pad(logits, (0, padding), value=float("-inf"))
866 scores = scores.unflatten(-1, (-1, block_size)).amax(dim=-1)
867 num_blocks = scores.shape[-1]
868 if isinstance(compress_lens, torch.Tensor):
869 last = (compress_lens.to(device=logits.device) - 1) // block_size
870 else:
871 last = (compress_lens - 1) // block_size
872 block_positions = torch.arange(num_blocks, device=logits.device)
873 scores = scores.masked_fill(block_positions == last, float("inf"))
874 top = scores.topk(min(topk_blocks, num_blocks), dim=-1, sorted=False)
875 return top.indices.masked_fill(top.values == float("-inf"), -1).to(torch.int32)
876
877
878def compressed_causal_candidates(
879 query: torch.Tensor,
880 key: torch.Tensor,
881 merge_weight: torch.Tensor,
882 *,
883 compress_ratio: int,
884 topk_blocks: int,
885 block_size: int,
886 query_offset: int = 0,
887 query_chunk_size: int = 256,
888 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
889 minimum_key_indices: torch.Tensor | None = None,
890) -> torch.Tensor:
891 """Build compact blockwise candidate ids in bounded query chunks."""
892 _, sequence_length, _, _ = query.shape
893 compressed_length = key.shape[1]
894 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
895 raise ValueError("minimum_key_indices must have shape [batch, query]")
896 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
897 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
898 candidate_chunks = []
899 for start in range(0, sequence_length, query_chunk_size):
900 end = min(start + query_chunk_size, sequence_length)
901 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
902 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
903 if reduce_sum is not None:
904 scores = reduce_sum(scores)
905 visible = (
906 torch.arange(query_offset + start, query_offset + end, device=query.device) + 1
907 ) // compress_ratio
908 scores.masked_fill_(key_positions >= visible.view(1, -1, 1), float("-inf"))
909 if minimum_key_indices is not None:
910 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
911 scores.masked_fill_(key_positions < minimum, float("-inf"))
912 candidate_chunks.append(
913 select_candidate_block_indices(
914 scores,
915 visible.view(1, -1, 1),
916 topk_blocks,
917 block_size,
918 )
919 )
920 return torch.cat(candidate_chunks, dim=1)
921
922
923def compressed_causal_topk_and_candidates(
924 query: torch.Tensor,
925 key: torch.Tensor,
926 merge_weight: torch.Tensor,
927 *,
928 compress_ratio: int,
929 sparse_count: int,
930 topk_blocks: int,
931 block_size: int,
932 query_offset: int = 0,
933 query_chunk_size: int = 256,
934 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
935 minimum_key_indices: torch.Tensor | None = None,
936 causal_upper: torch.Tensor | None = None,
937) -> tuple[torch.Tensor, torch.Tensor]:
938 """Select Full-layer Top-K and candidate blocks from one score pass.
939
940 ``causal_upper`` supplies the packed per-query causal bound for eager and
941 graph execution; it must be compression-aligned packed input when given.
942 """
943 if compress_ratio <= 0:
944 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
945 if query_chunk_size <= 0:
946 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
947 batch_size, sequence_length, _, head_dim = query.shape
948 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
949 raise ValueError(
950 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
951 )
952 if merge_weight.shape != query.shape[:3]:
953 raise ValueError(
954 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
955 )
956 compressed_length = key.shape[1]
957 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
958 raise ValueError("minimum_key_indices must have shape [batch, query]")
959 top_k = min(sparse_count, compressed_length)
960 if top_k <= 0:
961 empty_topk = torch.empty(
962 batch_size,
963 sequence_length,
964 0,
965 dtype=torch.int32,
966 device=query.device,
967 )
968 return empty_topk, empty_topk
969
970 key_fp32 = key.float().transpose(1, 2).unsqueeze(1)
971 key_positions = torch.arange(compressed_length, device=query.device).view(1, 1, -1)
972 selected_chunks = []
973 candidate_chunks = []
974 for start in range(0, sequence_length, query_chunk_size):
975 end = min(start + query_chunk_size, sequence_length)
976 scores = torch.matmul(query[:, start:end].float(), key_fp32).relu()
977 scores = (scores * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
978 if reduce_sum is not None:
979 scores = reduce_sum(scores)
980 visible = (
981 causal_upper[:, start:end] if causal_upper is not None else
982 (torch.arange(query_offset + start, query_offset + end, device=query.device) + 1) // compress_ratio
983 )
984 scores.masked_fill_(key_positions >= visible.view(1, -1, 1), float("-inf"))
985 if minimum_key_indices is not None:
986 minimum = minimum_key_indices[:, start:end].unsqueeze(-1)
987 scores.masked_fill_(key_positions < minimum, float("-inf"))
988 top = scores.topk(top_k, dim=-1, sorted=False)
989 indices = top.indices.masked_fill(~torch.isfinite(top.values), compressed_length)
990 indices = indices.sort(dim=-1).values
991 selected_chunks.append(indices.masked_fill(indices == compressed_length, -1).to(torch.int32))
992 candidate_chunks.append(
993 select_candidate_block_indices(
994 scores,
995 visible.view(1, -1, 1),
996 topk_blocks,
997 block_size,
998 )
999 )
1000 return torch.cat(selected_chunks, dim=1), torch.cat(candidate_chunks, dim=1)
1001
1002
1003def compressed_candidate_topk(
1004 query: torch.Tensor,
1005 key: torch.Tensor,
1006 merge_weight: torch.Tensor,
1007 candidate_blocks: torch.Tensor,
1008 *,
1009 compress_ratio: int,
1010 sparse_count: int,
1011 block_size: int,
1012 query_offset: int = 0,
1013 query_chunk_size: int = 256,
1014 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None = None,
1015 minimum_key_indices: torch.Tensor | None = None,
1016 causal_upper: torch.Tensor | None = None,
1017) -> torch.Tensor:
1018 """Score only hierarchical candidate blocks and return global key ids.
1019
1020 ``causal_upper`` supplies the packed per-query causal bound for eager and
1021 graph execution; it must be compression-aligned packed input when given.
1022 """
1023 if compress_ratio <= 0:
1024 raise ValueError(f"compress_ratio must be positive, got {compress_ratio}")
1025 if block_size <= 0:
1026 raise ValueError(f"block_size must be positive, got {block_size}")
1027 if query_chunk_size <= 0:
1028 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
1029 batch_size, sequence_length, _, head_dim = query.shape
1030 if key.ndim != 3 or key.shape[::2] != (batch_size, head_dim):
1031 raise ValueError(
1032 "compressed index key must have shape [batch, compressed_sequence, head_dim]"
1033 )
1034 if merge_weight.shape != query.shape[:3]:
1035 raise ValueError(
1036 f"merge_weight must have shape {tuple(query.shape[:3])}, got {tuple(merge_weight.shape)}"
1037 )
1038 if candidate_blocks.ndim != 3 or candidate_blocks.shape[:2] != query.shape[:2]:
1039 raise ValueError(
1040 "candidate_blocks must have shape [batch, query, candidate_blocks], "
1041 f"got {tuple(candidate_blocks.shape)}"
1042 )
1043 if minimum_key_indices is not None and minimum_key_indices.shape != query.shape[:2]:
1044 raise ValueError("minimum_key_indices must have shape [batch, query]")
1045
1046 compressed_length = key.shape[1]
1047 candidate_width = candidate_blocks.shape[-1] * block_size
1048 top_k = min(sparse_count, candidate_width)
1049 if top_k <= 0:
1050 return torch.empty(
1051 batch_size,
1052 sequence_length,
1053 0,
1054 dtype=torch.int32,
1055 device=query.device,
1056 )
1057
1058 block_offsets = torch.arange(block_size, device=query.device)
1059 batch_indices = torch.arange(batch_size, device=query.device).view(-1, 1, 1)
1060 selected_chunks = []
1061 for start in range(0, sequence_length, query_chunk_size):
1062 end = min(start + query_chunk_size, sequence_length)
1063 blocks = candidate_blocks[:, start:end].long()
1064 block_positions = blocks.unsqueeze(-1) * block_size + block_offsets
1065 positions = block_positions.flatten(-2)
1066 visible = (
1067 causal_upper[:, start:end] if causal_upper is not None else
1068 (torch.arange(query_offset + start, query_offset + end, device=query.device) + 1) // compress_ratio
1069 )
1070 valid = (blocks.unsqueeze(-1) >= 0).expand_as(block_positions).flatten(-2)
1071 valid = valid & (positions < compressed_length) & (positions < visible.view(1, -1, 1))
1072 if minimum_key_indices is not None:
1073 valid = valid & (positions >= minimum_key_indices[:, start:end].unsqueeze(-1))
1074 safe_positions = positions.clamp(min=0, max=max(compressed_length - 1, 0))
1075 selected_key = key[batch_indices, safe_positions].float()
1076 dots = torch.einsum(
1077 "bchd,bckd->bchk",
1078 query[:, start:end].float(),
1079 selected_key,
1080 ).relu()
1081 scores = (dots * merge_weight[:, start:end].float().unsqueeze(-1)).sum(dim=2)
1082 if reduce_sum is not None:
1083 scores = reduce_sum(scores)
1084 scores.masked_fill_(~valid, float("-inf"))
1085 top = scores.topk(top_k, dim=-1, sorted=False)
1086 selected = positions.gather(-1, top.indices)
1087 selected.masked_fill_(~torch.isfinite(top.values), compressed_length)
1088 selected = selected.sort(dim=-1).values
1089 selected.masked_fill_(selected == compressed_length, -1)
1090 selected_chunks.append(selected.to(torch.int32))
1091 return torch.cat(selected_chunks, dim=1)
1092
1093
1094class _SharedCompressedIndexerKLLoss(torch.autograd.Function):
1095 """PanGu-style precomputed-gradient KL loss for compressed index keys."""
1096
1097 @staticmethod
1098 def forward(
1099 ctx: Any,
1100 index_query: torch.Tensor,
1101 index_key: torch.Tensor,
1102 merge_weight: torch.Tensor,
1103 attention_query: torch.Tensor,
1104 compressed_key: torch.Tensor,
1105 topk_indices: torch.Tensor,
1106 sinks: torch.Tensor,
1107 attention_scale: float,
1108 loss_coeff: float,
1109 query_chunk_size: int,
1110 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
1111 ) -> torch.Tensor:
1112 """Compute sparse KL and save only the three Indexer gradients."""
1113 batch_size, sequence_length, _, _ = index_query.shape
1114 denominator = batch_size * sequence_length
1115 total_loss = index_query.new_zeros((), dtype=torch.float32)
1116 grad_index_query = torch.zeros_like(index_query, dtype=torch.float32)
1117 grad_index_key = torch.zeros_like(index_key, dtype=torch.float32)
1118 grad_merge_weight = torch.zeros_like(merge_weight, dtype=torch.float32)
1119
1120 with torch.no_grad(), torch.autocast(device_type=index_query.device.type, enabled=False):
1121 for start in range(0, sequence_length, query_chunk_size):
1122 end = min(start + query_chunk_size, sequence_length)
1123 selected = topk_indices[:, start:end]
1124 valid = selected >= 0
1125 # Graph capture forbids data-dependent control flow, so empty
1126 # rows contribute through masks instead of a `torch.any` skip:
1127 # their targets are all-zero, their gradients are all-zero, and
1128 # eager evaluates exactly the same masked expressions.
1129 safe_indices = selected.clamp_min(0).long()
1130
1131 query_chunk = index_query[:, start:end].float()
1132 weight_chunk = merge_weight[:, start:end].float()
1133 selected_index_key = gather_selected_keys(index_key, safe_indices).float()
1134 index_dots = torch.einsum(
1135 "bcid,bckd->bcik", query_chunk, selected_index_key
1136 )
1137 index_relu = index_dots.relu()
1138 index_scores = (index_relu * weight_chunk.unsqueeze(-1)).sum(dim=2)
1139 if reduce_sum is not None:
1140 index_scores = reduce_sum(index_scores)
1141 index_scores.masked_fill_(~valid, -1.0e9)
1142
1143 selected_attention_key = gather_selected_keys(compressed_key, safe_indices).float()
1144 attention_scores = torch.einsum(
1145 "bhcd,bckd->bhck",
1146 attention_query[:, :, start:end].float(),
1147 selected_attention_key,
1148 ) * attention_scale
1149 attention_scores.masked_fill_(~valid.unsqueeze(1), -1.0e9)
1150 sink_logits = sinks.float().view(1, -1, 1, 1).expand(
1151 batch_size, -1, end - start, -1
1152 )
1153 target = torch.cat((attention_scores, sink_logits), dim=-1).softmax(dim=-1)
1154 target = target[..., :-1].masked_fill(~valid.unsqueeze(1), 0.0).sum(dim=1)
1155 if reduce_sum is not None:
1156 target = reduce_sum(target)
1157 target = target / target.sum(dim=-1, keepdim=True).clamp_min(
1158 torch.finfo(torch.float32).tiny
1159 )
1160
1161 log_prediction = index_scores.log_softmax(dim=-1)
1162 target_log = target.clamp_min(torch.finfo(torch.float32).tiny).log()
1163 row_loss = (target * (target_log - log_prediction)).sum(dim=-1)
1164 # Masked reduction instead of boolean indexing: identical value
1165 # in eager and traceable across runtime-dependent row validity.
1166 row_mask = valid.any(dim=-1).to(row_loss.dtype)
1167 total_loss.add_((row_loss * row_mask).sum() * (loss_coeff / denominator))
1168
1169 grad_scores = log_prediction.exp() * target.sum(-1, keepdim=True) - target
1170 grad_scores.mul_(loss_coeff / denominator)
1171 grad_scores.masked_fill_(~valid, 0.0)
1172 grad_dots = (
1173 grad_scores.unsqueeze(2)
1174 * weight_chunk.unsqueeze(-1)
1175 * (index_dots > 0).to(index_dots.dtype)
1176 )
1177 grad_index_query[:, start:end] = torch.einsum(
1178 "bcik,bckd->bcid", grad_dots, selected_index_key
1179 )
1180 grad_merge_weight[:, start:end] = (
1181 grad_scores.unsqueeze(2) * index_relu
1182 ).sum(dim=-1)
1183 selected_key_grad = torch.einsum(
1184 "bcik,bcid->bckd", grad_dots, query_chunk
1185 )
1186 # Preserve the einsum result if SAC cached its storage.
1187 selected_key_grad = selected_key_grad.masked_fill(~valid.unsqueeze(-1), 0.0)
1188 scatter_indices = safe_indices.unsqueeze(-1).expand_as(selected_key_grad)
1189 grad_index_key.scatter_add_(
1190 1,
1191 scatter_indices.reshape(batch_size, -1, index_key.shape[-1]),
1192 selected_key_grad.reshape(batch_size, -1, index_key.shape[-1]),
1193 )
1194
1195 ctx.save_for_backward(grad_index_query, grad_index_key, grad_merge_weight)
1196 return total_loss
1197
1198 @staticmethod
1199 def backward(ctx: Any, grad_loss: torch.Tensor) -> tuple:
1200 """Return the precomputed Indexer gradients and detach the teacher."""
1201 grad_index_query, grad_index_key, grad_merge_weight = ctx.saved_tensors
1202 return (
1203 grad_index_query * grad_loss,
1204 grad_index_key * grad_loss,
1205 grad_merge_weight * grad_loss,
1206 *((None,) * 8),
1207 )
1208
1209
1210def shared_compressed_indexer_kl_loss(
1211 index_query: torch.Tensor,
1212 index_key: torch.Tensor,
1213 merge_weight: torch.Tensor,
1214 attention_query: torch.Tensor,
1215 compressed_key: torch.Tensor,
1216 topk_indices: torch.Tensor,
1217 sinks: torch.Tensor,
1218 *,
1219 attention_scale: float,
1220 loss_coeff: float,
1221 query_chunk_size: int = 256,
1222 tp_context: SharedCompressedAttentionTPContext | None = None,
1223 compress_ratio: int = 1,
1224 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
1225 use_fused: bool = False,
1226) -> torch.Tensor:
1227 """Compute the sparse-stage DSA KL objective on V4.1 compressed keys.
1228
1229 The target first softmaxes selected main-attention scores per attention
1230 head, then averages and L1-normalizes across heads. Indexer inputs are
1231 detached by the caller, so this objective updates only Indexer parameters.
1232
1233 ``use_fused`` is valid only for ordinary causal TopK selections, without
1234 candidate-pool holes. ``query_segments`` supplies each document-local K
1235 prefix; its ratio and residual encode the CP query offset for the kernel.
1236 ``use_fused=True`` raises for unsupported calls when ``loss_coeff != 0``;
1237 False explicitly selects the numerical reference. Native errors propagate.
1238 """
1239 if query_chunk_size <= 0:
1240 raise ValueError(f"query_chunk_size must be positive, got {query_chunk_size}")
1241 if not loss_coeff:
1242 return index_query.sum(dtype=torch.float32) * 0.0
1243 if index_key.shape[1] == 0:
1244 # Keep zero gradients connected even when no document contains a complete block.
1245 return (index_query[..., :0].sum(dtype=torch.float32) + index_key.sum(dtype=torch.float32)
1246 + merge_weight[..., :0].sum(dtype=torch.float32)) * 0.0
1247 if topk_indices.shape[:2] != index_query.shape[:2]:
1248 raise ValueError("topk_indices and index_query must share batch/query dimensions")
1249 if use_fused:
1250 if tp_context is not None:
1251 raise NotImplementedError("Fused KL does not support TP score/teacher reduction")
1252 if query_segments is None:
1253 raise ValueError("Fused KL requires document-local causal query_segments")
1254 validate_fused_kl_inputs(index_query, index_key, merge_weight, topk_indices)
1255 return sparse_lightning_indexer_kl_loss(
1256 index_query, index_key, merge_weight, attention_query.detach(), compressed_key.detach(),
1257 topk_indices, sinks.detach(), attention_scale, loss_coeff, query_chunk_size,
1258 compress_ratio, query_segments,
1259 )
1260 return _SharedCompressedIndexerKLLoss.apply(
1261 index_query,
1262 index_key,
1263 merge_weight,
1264 attention_query.detach(),
1265 compressed_key.detach(),
1266 topk_indices,
1267 sinks.detach(),
1268 attention_scale,
1269 loss_coeff,
1270 query_chunk_size,
1271 None if tp_context is None else tp_context.reduce_sum,
1272 )
1273
1274
1275class SharedCompressedDSAIndexer(nn.Module):
1276 """Reusable V4.1 compressed Lightning Indexer semantics."""
1277
1278 # Installed by :meth:`enable_selection_exclude` for a layer that recomputes.
1279 # It is written through ``object.__setattr__`` so the exclusion region stays
1280 # out of ``modules()`` and the state dict.
1281 _selection_region: Callable[..., Any] | None = None
1282 _input_region: Callable[..., Any] | None = None
1283 _prepare_region: Callable[..., Any] | None = None
1284 _result_region: Callable[..., Any] | None = None
1285 _wait_region: Callable[..., Any] | None = None
1286
1287 def __init__(self, module: nn.Module) -> None:
1288 """Transfer the source projections without changing checkpoint names."""
1289 super().__init__()
1290 required = ("q_b_proj", "weights_proj")
1291 missing = [name for name in required if not hasattr(module, name)]
1292 if missing:
1293 raise TypeError(f"shared compressed DSA indexer is missing: {missing}")
1294 for name, child in module._modules.items(): # pylint: disable=protected-access
1295 self.add_module(name, child)
1296 for name, parameter in module._parameters.items(): # pylint: disable=protected-access
1297 self.register_parameter(name, parameter)
1298 self.use_fused = False
1299 self.compress_ratio = module.compress_ratio
1300 self.global_num_heads = module.num_heads
1301 self.head_dim = module.head_dim
1302 self.index_topk = module.index_topk
1303 self.owns_key = module.owns_key
1304 self.is_candidate_source = module.is_candidate_source
1305 self.uses_candidates = module.uses_candidates
1306 self.candidate_topk_blocks = module.candidate_topk_blocks
1307 self.candidate_block_size = module.candidate_block_size
1308 self.loss_coeff = module.loss_coeff
1309 self.query_chunk_size = int(getattr(module, "query_chunk_size", 256))
1310 self.train(module.training)
1311
1312 def _project_index_query(
1313 self, query_residual: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor,
1314 *, sequence_major: bool = False,
1315 ) -> torch.Tensor:
1316 """Project queries with their original rotary positions, including moved tasks."""
1317 query = self.q_b_proj(query_residual.detach())
1318 if query.shape[-1] % self.head_dim:
1319 raise ValueError(
1320 f"Indexer query width {query.shape[-1]} is not divisible by head_dim {self.head_dim}"
1321 )
1322 query = query.view(*query_residual.shape[:2], -1, self.head_dim)
1323 if sequence_major:
1324 return _apply_v41_rope(query, cos, sin, unsqueeze_dim=2)
1325 return _apply_v41_rope(query.transpose(1, 2), cos, sin).transpose(1, 2)
1326
1327 @torch.no_grad()
1328 def _project_worker_index_query(
1329 self, query_residual: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor,
1330 owner_query_length: int,
1331 ) -> torch.Tensor:
1332 """Preserve the owner's projection row shape for discrete worker selection.
1333
1334 BF16 GEMM rounding can depend on its row count. Match the original
1335 owner's GEMM shape so task redistribution does not perturb TopK inputs.
1336 Padding is confined to the no-grad projection and never reaches LI.
1337
1338 Args:
1339 query_residual: Low-rank query rows assigned to this worker.
1340 cos: Original-position cosine rows for the worker queries.
1341 sin: Original-position sine rows for the worker queries.
1342 owner_query_length: Query count in the original owner projection.
1343
1344 Returns:
1345 Worker Index Q in sequence-major layout, excluding padded rows.
1346 """
1347 if owner_query_length <= 0:
1348 raise ValueError("Indexer owner query length must be positive")
1349 batch, length = query_residual.shape[:2]
1350 if length == owner_query_length:
1351 return self._project_index_query(query_residual, cos, sin, sequence_major=True)
1352 if not length:
1353 query = self.q_b_proj(query_residual.detach())
1354 return query.view(batch, 0, query.shape[-1] // self.head_dim, self.head_dim)
1355
1356 query = None
1357 for start in range(0, length, owner_query_length):
1358 end = min(start + owner_query_length, length)
1359 chunk = query_residual[:, start:end].detach()
1360 if end - start < owner_query_length:
1361 chunk = torch.nn.functional.pad(chunk, (0, 0, 0, owner_query_length - (end - start)))
1362 projected = self.q_b_proj(chunk)
1363 if query is None:
1364 query = projected.new_empty((batch, length, projected.shape[-1]))
1365 query[:, start:end].copy_(projected[:, :end - start])
1366 del projected, chunk
1367 query = query.view(batch, length, query.shape[-1] // self.head_dim, self.head_dim)
1368 return _apply_v41_rope(query, cos, sin, unsqueeze_dim=2)
1369
1370 def _project_index_key(
1371 self,
1372 latent: torch.Tensor | None,
1373 cos: torch.Tensor,
1374 sin: torch.Tensor,
1375 index_key: torch.Tensor | None,
1376 cp_context: SharedCompressedAttentionCPContext | None,
1377 block_plan: Any = None,
1378 key_position_embeddings: tuple | None = None,
1379 ) -> tuple[torch.Tensor, "SequenceGatherHandle | None"]:
1380 """Project or adopt the index key and launch its CP gather."""
1381 if self.owns_key:
1382 if latent is None:
1383 raise RuntimeError("a Full Indexer requires the compressor latent")
1384 key = self.k_norm(self.wk(latent.detach()))
1385 compressed_length = key.shape[1]
1386 if key_position_embeddings is None:
1387 key_cos = cos[:, :compressed_length * self.compress_ratio:self.compress_ratio]
1388 key_sin = sin[:, :compressed_length * self.compress_ratio:self.compress_ratio]
1389 else:
1390 key_cos, key_sin = key_position_embeddings
1391 key = _apply_v41_rope(key.unsqueeze(1), key_cos, key_sin).squeeze(1)
1392 key_handle = None
1393 if cp_context is not None:
1394 key_handle = (cp_context.launch(key, 1) if block_plan is None
1395 else block_plan.launch_bank(key, cp_context))
1396 return key, key_handle
1397 if index_key is None:
1398 raise RuntimeError("a Reindex layer requires index K from the preceding Full layer")
1399 return index_key, None
1400
1401 def forward(
1402 self,
1403 hidden_states: torch.Tensor,
1404 query_residual: torch.Tensor,
1405 latent: torch.Tensor | None,
1406 compress_position_embeddings: tuple[torch.Tensor, torch.Tensor],
1407 *,
1408 index_key: torch.Tensor | None = None,
1409 candidate_blocks: torch.Tensor | None = None,
1410 cp_context: SharedCompressedAttentionCPContext | None = None,
1411 tp_context: SharedCompressedAttentionTPContext | None = None,
1412 query_offset: int = 0,
1413 minimum_key_indices: torch.Tensor | None = None,
1414 query_segments: tuple[tuple[int, int, int, int, int], ...] | None = None,
1415 document_lengths: tuple[int, ...] = (),
1416 prepare_attention: Callable[[], None] | None = None,
1417 block_plan: Any = None,
1418 key_position_embeddings: tuple | None = None,
1419 causal_upper: torch.Tensor | None = None,
1420 ) -> SharedCompressedIndexerOutput:
1421 """Project local queries and score them against global compressed keys.
1422
1423 When the enclosing layer is recomputed, :meth:`enable_selection_exclude`
1424 makes the discrete selection a checkpoint exclusion region: it runs in
1425 the original forward and its cached result is reused during the
1426 recomputation, so the chosen key set can never drift and the checkpoint
1427 keeps saving the same tensors in both passes.
1428 """
1429 cos, sin = compress_position_embeddings
1430 if cp_context is not None and cp_context.launch_balance is not None:
1431 return self._forward_dispatched(
1432 hidden_states, query_residual, latent, (cos, sin), index_key, cp_context,
1433 tp_context, document_lengths, prepare_attention, block_plan, key_position_embeddings,
1434 )
1435 key, key_handle = self._project_index_key(
1436 latent, cos, sin, index_key, cp_context, block_plan, key_position_embeddings,
1437 )
1438 if prepare_attention is not None:
1439 prepare_attention()
1440
1441 query = self._project_index_query(query_residual, cos, sin)
1442 local_num_heads = query.shape[2]
1443 merge_weight = self.weights_proj(hidden_states.detach())
1444 if merge_weight.shape[-1] != local_num_heads:
1445 raise ValueError(
1446 "Indexer q_b_proj and weights_proj must use the same TP head shard: "
1447 f"{local_num_heads} query heads versus {merge_weight.shape[-1]} weights"
1448 )
1449 merge_weight = merge_weight * (self.head_dim**-0.5 * self.global_num_heads**-0.5)
1450 reduce_sum = None if tp_context is None else tp_context.reduce_sum
1451 if key_handle is not None:
1452 key = key_handle.wait()
1453 topk_indices, candidate_blocks = self._run_selection(
1454 query, key, merge_weight, query_residual, (cos, sin), cp_context, tp_context,
1455 candidate_blocks, query_offset, reduce_sum, minimum_key_indices, query_segments,
1456 document_lengths, causal_upper,
1457 )
1458 return SharedCompressedIndexerOutput(
1459 topk_indices=topk_indices,
1460 index_key=key,
1461 candidate_blocks=candidate_blocks,
1462 index_query=query,
1463 merge_weight=merge_weight,
1464 )
1465
1466 def _forward_dispatched(
1467 self, hidden_states: torch.Tensor, query_residual: torch.Tensor, latent: torch.Tensor | None,
1468 embeddings: tuple, index_key: torch.Tensor | None, context: SharedCompressedAttentionCPContext,
1469 tp_context: SharedCompressedAttentionTPContext | None, document_lengths: tuple,
1470 prepare_attention: Callable[[], None] | None,
1471 block_plan: Any = None, key_position_embeddings: tuple | None = None,
1472 ) -> SharedCompressedIndexerOutput:
1473 """Overlap worker inputs and key gathering while retaining owner-side KL edges."""
1474 cos, sin = embeddings
1475 if tp_context is not None or self.is_candidate_source or self.uses_candidates:
1476 raise NotImplementedError("Indexer overlap requires ordinary causal selection and TP=1")
1477 weights = self.weights_proj(hidden_states.detach())
1478 weights = weights * (self.head_dim**-0.5 * self.global_num_heads**-0.5)
1479 launch = self._launch_selection if self._input_region is None else self._input_region
1480 wave = launch(query_residual, weights, (cos, sin), document_lengths, context)
1481 key, handle = self._project_index_key(
1482 latent, cos, sin, index_key, context, block_plan, key_position_embeddings,
1483 )
1484 if prepare_attention is not None:
1485 prepare_attention()
1486 prepare = self._prepare_dispatched if self._prepare_region is None else self._prepare_region
1487 prepared = prepare(wave) if context.indexer_priority else None
1488 # KL uses this autograd edge; only selection transport runs under no_grad.
1489 if handle is not None:
1490 key = handle.wait()
1491 if not context.indexer_priority:
1492 prepared = prepare(wave)
1493 select = self._select_dispatched if self._result_region is None else self._result_region
1494 result = select(prepared, key)
1495 del prepared
1496 query = self._project_index_query(query_residual, cos, sin)
1497 if weights.shape[-1] != query.shape[2]:
1498 raise ValueError("Indexer query and merge weights must have matching head counts")
1499 finish = self._finish_selection if self._wait_region is None else self._wait_region
1500 return SharedCompressedIndexerOutput(finish(result), key, None, query, weights)
1501
1502 def enable_selection_exclude(self) -> None:
1503 """Keep discrete selection fixed when the enclosing layer recomputes."""
1504 object.__setattr__(
1505 self, "_selection_region", checkpoint_exclude_wrapper(self._select_tokens)
1506 )
1507 object.__setattr__(self, "_input_region", checkpoint_exclude_wrapper(self._launch_selection))
1508 # This output goes straight into an exclusion region; retain IDs, not expanded worker Q.
1509 object.__setattr__(self, "_prepare_region", checkpoint_exclude_wrapper( # pylint: disable=E1123
1510 self._prepare_dispatched, save_output=False,
1511 ))
1512 object.__setattr__(self, "_result_region", checkpoint_exclude_wrapper(self._select_dispatched))
1513 object.__setattr__(self, "_wait_region", checkpoint_exclude_wrapper(self._finish_selection))
1514
1515 @torch.no_grad()
1516 def _launch_selection(self, qr: torch.Tensor, weights: torch.Tensor, embeddings: tuple,
1517 lengths: tuple, context: SharedCompressedAttentionCPContext) -> Any:
1518 """Cache the input launch so checkpoint replay does not repeat communication."""
1519 return context.launch_balance(qr, weights, embeddings, lengths, self.compress_ratio)
1520
1521 @torch.no_grad()
1522 def _prepare_dispatched(self, wave: Any) -> tuple:
1523 """Expand received qr while Index K may still be in flight."""
1524 qr, weights, cos, sin = wave.wait()
1525 query = self._project_worker_index_query(qr, cos, sin, wave.owner_queries)
1526 return query, weights, wave
1527
1528 @torch.no_grad()
1529 def _select_dispatched(self, prepared: tuple, key: torch.Tensor) -> Any:
1530 """Consume worker Q once and launch the inverse ID transfer."""
1531 query, weights, wave = prepared
1532 selected = self._select_indices(query, key, weights, None, 0, None, None, wave.route.segments)[0]
1533 return wave.launch_result(selected[0])
1534
1535 @staticmethod
1536 @torch.no_grad()
1537 def _finish_selection(result: Any) -> torch.Tensor:
1538 """Cache completed IDs; replay never consumes an already finished handle."""
1539 return result.wait().unsqueeze(0)
1540
1541 def _run_selection(self, *args: Any, **kwargs: Any) -> tuple[torch.Tensor, torch.Tensor | None]:
1542 """Select directly, or reuse the result retained by the checkpoint region."""
1543 selection = self._select_tokens if self._selection_region is None else self._selection_region
1544 return selection(*args, **kwargs)
1545
1546 def _select_tokens(
1547 self, query: torch.Tensor, key: torch.Tensor, merge_weight: torch.Tensor,
1548 query_residual: torch.Tensor, embeddings: tuple,
1549 cp_context: SharedCompressedAttentionCPContext | None,
1550 tp_context: SharedCompressedAttentionTPContext | None,
1551 candidate_blocks: torch.Tensor | None, query_offset: int,
1552 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
1553 minimum_key_indices: torch.Tensor | None, query_segments: tuple | None,
1554 document_lengths: tuple, causal_upper: torch.Tensor | None = None,
1555 ) -> tuple[torch.Tensor, torch.Tensor | None]:
1556 """Cache dispatch, discrete selection and TopK return as one recompute region."""
1557 cos, sin = embeddings
1558 if cp_context is not None and cp_context.balance_indexer is not None:
1559 if tp_context is not None or self.is_candidate_source or self.uses_candidates:
1560 raise NotImplementedError("Indexer load balance currently requires ordinary causal selection and TP=1")
1561
1562 def select_tasks(task_qr: torch.Tensor, task_weights: torch.Tensor,
1563 task_embeddings: tuple, segments: tuple) -> torch.Tensor:
1564 """Project on the receiver while this layer's parameters are resident."""
1565 task_query = self._project_worker_index_query(
1566 task_qr, *task_embeddings, query_residual.shape[1],
1567 )
1568 return self._select_indices(
1569 task_query, key, task_weights, None, 0, None, None, segments,
1570 )[0]
1571
1572 topk_indices = cp_context.balance_indexer(
1573 query_residual, merge_weight, (cos, sin), select_tasks, document_lengths, self.compress_ratio,
1574 )
1575 candidate_blocks = None
1576 else:
1577 topk_indices, candidate_blocks = self._select_indices(
1578 query, key, merge_weight, candidate_blocks, query_offset,
1579 reduce_sum, minimum_key_indices, query_segments, causal_upper,
1580 )
1581 return topk_indices, candidate_blocks
1582
1583 def _select_indices(
1584 self, query: torch.Tensor, key: torch.Tensor, merge_weight: torch.Tensor,
1585 candidate_blocks: torch.Tensor | None, query_offset: int,
1586 reduce_sum: Callable[[torch.Tensor], torch.Tensor] | None,
1587 minimum_key_indices: torch.Tensor | None, query_segments: tuple | None,
1588 causal_upper: torch.Tensor | None = None,
1589 ) -> tuple[torch.Tensor, torch.Tensor | None]:
1590 """Select the requested implementation without changing candidate policies."""
1591 if self.use_fused and (self.is_candidate_source or self.uses_candidates):
1592 raise NotImplementedError("V4.1 fused Indexer does not support candidate-pool selection")
1593 if self.is_candidate_source:
1594 topk_indices, candidate_blocks = compressed_causal_topk_and_candidates(
1595 query,
1596 key,
1597 merge_weight,
1598 compress_ratio=self.compress_ratio,
1599 sparse_count=self.index_topk,
1600 topk_blocks=self.candidate_topk_blocks,
1601 block_size=self.candidate_block_size,
1602 query_offset=query_offset,
1603 query_chunk_size=self.query_chunk_size,
1604 reduce_sum=reduce_sum,
1605 minimum_key_indices=minimum_key_indices,
1606 causal_upper=causal_upper,
1607 )
1608 elif self.uses_candidates:
1609 if candidate_blocks is None:
1610 raise RuntimeError("a hierarchical Reindex layer requires candidate block ids")
1611 topk_indices = compressed_candidate_topk(
1612 query,
1613 key,
1614 merge_weight,
1615 candidate_blocks,
1616 compress_ratio=self.compress_ratio,
1617 sparse_count=self.index_topk,
1618 block_size=self.candidate_block_size,
1619 query_offset=query_offset,
1620 query_chunk_size=self.query_chunk_size,
1621 reduce_sum=reduce_sum,
1622 minimum_key_indices=minimum_key_indices,
1623 causal_upper=causal_upper,
1624 )
1625 else:
1626 candidate_blocks = None
1627 topk_indices = compressed_causal_topk(
1628 query,
1629 key,
1630 merge_weight,
1631 compress_ratio=self.compress_ratio,
1632 sparse_count=self.index_topk,
1633 query_offset=query_offset,
1634 query_chunk_size=self.query_chunk_size,
1635 reduce_sum=reduce_sum,
1636 minimum_key_indices=minimum_key_indices,
1637 query_segments=query_segments,
1638 causal_upper=causal_upper,
1639 use_fused=self.use_fused,
1640 )
1641 return topk_indices, candidate_blocks
1642
1643
1644def reference_sparse_attention(
1645 query: torch.Tensor,
1646 key_value: torch.Tensor,
1647 sparse_indices: torch.Tensor,
1648 sinks: torch.Tensor,
1649 scale: float,
1650) -> torch.Tensor:
1651 """Compute sink attention with eager Torch operations on any device.
1652
1653 Args:
1654 query: Main queries [batch, heads, local queries, head dimension].
1655 key_value: Shared K=V bank [batch, 1, keys, head dimension].
1656 sparse_indices: Visible bank IDs [batch, local queries, selections];
1657 negative entries are padding and repeated IDs count only once.
1658 sinks: Learnable scalar sink logits [heads], with zero-valued output.
1659 scale: Main attention logit scale.
1660
1661 Returns:
1662 Attention values [batch, local queries, heads, head dimension].
1663 Softmax uses FP32; matrix multiplications retain the input precision.
1664
1665 Note:
1666 This reference materializes a dense query/key mask and scores. It is
1667 intended for explicit unfused comparisons, not long-sequence efficiency.
1668 """
1669 batch_size, num_heads, sequence_length, _ = query.shape
1670 key_length = key_value.shape[2]
1671 valid = sparse_indices >= 0
1672 safe_indices = sparse_indices.clamp_min(0).long()
1673 allowed_counts = torch.zeros(
1674 batch_size,
1675 sequence_length,
1676 key_length,
1677 dtype=torch.int32,
1678 device=query.device,
1679 )
1680 allowed_counts.scatter_add_(2, safe_indices, valid.to(torch.int32))
1681 allowed = allowed_counts > 0
1682 logits = torch.matmul(query, key_value.transpose(-1, -2)) * scale
1683 logits = logits.masked_fill(~allowed.unsqueeze(1), float("-inf"))
1684 sink_logits = sinks.view(1, num_heads, 1, 1).expand(batch_size, -1, sequence_length, -1)
1685 probabilities = torch.cat((logits, sink_logits), dim=-1).softmax(dim=-1, dtype=torch.float32)
1686 return torch.matmul(probabilities[..., :-1].to(key_value.dtype), key_value).transpose(1, 2)
1687
1688
1689class SharedCompressedDSAAttentionBase(nn.Module):
1690 """Shared V4.1 attention semantics with selectable reference execution."""
1691
1692 def __init__(
1693 self,
1694 module: nn.Module,
1695 *,
1696 replace_indexer: bool,
1697 use_fused_ops: bool,
1698 ) -> None:
1699 """Transfer source state and configure the execution implementation."""
1700 super().__init__()
1701 for name, child in module._modules.items(): # pylint: disable=protected-access
1702 self.add_module(name, child)
1703 for name, parameter in module._parameters.items(): # pylint: disable=protected-access
1704 self.register_parameter(name, parameter)
1705 indexer = getattr(self, "indexer", None)
1706 if (
1707 replace_indexer
1708 and indexer is not None
1709 and not isinstance(indexer, SharedCompressedDSAIndexer)
1710 ):
1711 self.indexer = SharedCompressedDSAIndexer(indexer)
1712 self.config = module.config
1713 self.layer_idx = module.layer_idx
1714 self.num_heads = module.num_heads
1715 self.num_key_value_groups = module.num_key_value_groups
1716 self.num_groups = module.config.o_groups
1717 self.compress_ratio = module.compress_ratio
1718 self.is_kv_source = module.is_kv_source
1719 self.is_index_source = module.is_index_source
1720 self.kv_source_layer_idx = module.kv_source_layer_idx
1721 self.index_source_layer_idx = module.index_source_layer_idx
1722 self.candidate_source_layer_idx = module.candidate_source_layer_idx
1723 self.head_dim = module.head_dim
1724 self.rope_head_dim = module.config.qk_rope_head_dim
1725 self.sliding_window = module.sliding_window
1726 self.scaling = module.scaling
1727 self.use_fused_ops = use_fused_ops
1728 if self.is_index_source:
1729 self.indexer.use_fused = use_fused_ops
1730 self.train(module.training)
1731
1732 @staticmethod
1733 def _validate_cp_inputs(
1734 cp_context: SharedCompressedAttentionCPContext,
1735 position_ids: torch.Tensor,
1736 sequence_length: int,
1737 ) -> tuple[int, int]:
1738 """Validate contiguous CP shards and return global query geometry."""
1739 if cp_context.size <= 1 or not 0 <= cp_context.rank < cp_context.size:
1740 raise ValueError(
1741 "shared compressed attention CP requires size > 1 and "
1742 f"0 <= rank < size, got rank={cp_context.rank}, size={cp_context.size}"
1743 )
1744 query_offset = cp_context.rank * sequence_length
1745 if not torch.all(position_ids[..., 0] == query_offset):
1746 raise ValueError(
1747 "shared compressed attention CP requires contiguous global position_ids; "
1748 f"rank {cp_context.rank} expected first position {query_offset}"
1749 )
1750 return query_offset, sequence_length * cp_context.size
1751
1752 def _project_raw_kv(
1753 self,
1754 hidden_states: torch.Tensor,
1755 cos: torch.Tensor,
1756 sin: torch.Tensor,
1757 cp_context: SharedCompressedAttentionCPContext | None,
1758 raw_route: SequenceDispatchRoute | None = None,
1759 ) -> tuple[torch.Tensor | None, SequenceGatherHandle | None]:
1760 """Project raw K=V and launch CP communication before query GEMMs."""
1761 key_value = self.kv_norm(self.kv_proj(hidden_states)).unsqueeze(1)
1762 key_value = _apply_v41_rope(key_value, cos, sin)
1763 if cp_context is None:
1764 return key_value, None
1765 if raw_route is not None:
1766 return None, cp_context.launch_raw(key_value, 2, raw_route)
1767 return None, cp_context.launch(key_value, 2)
1768
1769 def _project_query(
1770 self,
1771 hidden_states: torch.Tensor,
1772 cos: torch.Tensor,
1773 sin: torch.Tensor,
1774 ) -> tuple[torch.Tensor, torch.Tensor]:
1775 """Project and normalize the local query heads."""
1776 query_residual = self.q_a_norm(self.q_a_proj(hidden_states))
1777 return query_residual, self._expand_query(query_residual, cos, sin)
1778
1779 def _expand_query(self, query_residual: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
1780 """Expand main heads after low-rank query inputs have been dispatched."""
1781 query = self.q_b_proj(query_residual).view(*query_residual.shape[:2], -1, self.head_dim)
1782 # Preserve sequence-major storage for sparse attention while keeping the head-major API.
1783 return _apply_v41_rope(query, cos, sin, unsqueeze_dim=2).transpose(1, 2)
1784
1785 @staticmethod
1786 def _validate_forward_inputs(
1787 hidden_states: torch.Tensor,
1788 attention_mask: torch.Tensor | None,
1789 past_key_values: Any | None,
1790 kwargs: dict[str, Any],
1791 ) -> tuple[
1792 "SharedCompressedAttentionState",
1793 "SharedCompressedPackedSequence | None",
1794 "SharedCompressedAttentionCPContext | None",
1795 "SharedCompressedAttentionTPContext | None",
1796 int,
1797 int,
1798 ]:
1799 """Pop and validate the shared-attention keyword arguments."""
1800 if past_key_values is not None:
1801 raise NotImplementedError("shared compressed DSA training does not support KV cache")
1802 shared_state = kwargs.pop("shared_attention_state", None)
1803 if not isinstance(shared_state, SharedCompressedAttentionState):
1804 raise ValueError("shared_attention_state is required for shared compressed DSA")
1805 if attention_mask is not None:
1806 raise NotImplementedError(
1807 "shared compressed DSA consumes compact packed_seq_params instead of a dense mask"
1808 )
1809 batch_size, sequence_length, _ = hidden_states.shape
1810 packed_sequence = kwargs.pop("packed_seq_params", None)
1811 if packed_sequence is not None and not isinstance(
1812 packed_sequence, SharedCompressedPackedSequence
1813 ):
1814 raise TypeError(
1815 "packed_seq_params must be SharedCompressedPackedSequence, "
1816 f"got {type(packed_sequence).__name__}"
1817 )
1818 cp_context = kwargs.pop("shared_attention_cp_context", None)
1819 if cp_context is not None and not isinstance(cp_context, SharedCompressedAttentionCPContext):
1820 raise TypeError(
1821 "shared_attention_cp_context must be SharedCompressedAttentionCPContext, "
1822 f"got {type(cp_context).__name__}"
1823 )
1824 tp_context = kwargs.pop("shared_attention_tp_context", None)
1825 if tp_context is not None and not isinstance(tp_context, SharedCompressedAttentionTPContext):
1826 raise TypeError(
1827 "shared_attention_tp_context must be SharedCompressedAttentionTPContext, "
1828 f"got {type(tp_context).__name__}"
1829 )
1830 return shared_state, packed_sequence, cp_context, tp_context, batch_size, sequence_length
1831
1832 def _resolve_packed_segments(
1833 self,
1834 packed_sequence: "SharedCompressedPackedSequence | None",
1835 batch_size: int,
1836 sequence_length: int,
1837 query_offset: int,
1838 global_sequence_length: int,
1839 device: torch.device,
1840 ) -> tuple[torch.Tensor | None, torch.Tensor | None, tuple, torch.Tensor | None]:
1841 """Validate packed document boundaries and derive local Q/K geometry.
1842
1843 Both execution modes consume the same per-token tensors: document
1844 starts, per-query compressed-key minima and the packed causal upper
1845 bound. Python ``query_segments`` stay available for the eager fused
1846 kernels only; graph packing inputs never carry them.
1847 """
1848 segment_starts = None
1849 minimum_key_indices = None
1850 causal_upper = None
1851 ratio = max(1, self.compress_ratio)
1852 raw_end = query_offset + sequence_length
1853 query_segments = ((0, sequence_length, 0, raw_end // ratio, raw_end % ratio),)
1854 if packed_sequence is not None:
1855 if batch_size != 1:
1856 raise ValueError("V4.1 compact packed attention currently requires micro_batch_size=1")
1857 if (
1858 packed_sequence.local_query_start != query_offset
1859 or packed_sequence.local_query_length != sequence_length
1860 or packed_sequence.global_sequence_length != global_sequence_length
1861 ):
1862 packed_shard = (
1863 packed_sequence.local_query_start,
1864 packed_sequence.local_query_length,
1865 packed_sequence.global_sequence_length,
1866 )
1867 raise ValueError(
1868 "packed sequence geometry does not match the local CP shard: "
1869 f"packed={packed_shard}, "
1870 f"attention={(query_offset, sequence_length, global_sequence_length)}"
1871 )
1872 prepared = packed_sequence.prepare(device, (self.compress_ratio,))
1873 segment_starts = prepared.local_segment_starts(device)
1874 if self.compress_ratio:
1875 minimum_key_indices = prepared.minimum_key_indices(device, self.compress_ratio)
1876 causal_upper = prepared.causal_key_upper_bounds(device, self.compress_ratio)
1877 if (self.candidate_source_layer_idx is not None
1878 and not prepared.is_compression_aligned(ratio)):
1879 raise ValueError("candidate-pool packed attention still requires compression-aligned documents")
1880 query_segments = (
1881 None if prepared._restored_from_packing_tensors # pylint: disable=protected-access
1882 else prepared.indexer_segments(ratio)
1883 )
1884 return segment_starts, minimum_key_indices, query_segments, causal_upper
1885
1886 def _prepare_compression(
1887 self, packed: SharedCompressedPackedSequence | None, context: SharedCompressedAttentionCPContext | None,
1888 embeddings: dict, global_length: int, device: torch.device,
1889 ) -> tuple:
1890 """Prepare block-start positions only for a source that needs document-local grouping."""
1891 if not self.is_kv_source or self.compress_ratio <= 1:
1892 return None, None
1893 size, rank = (1, 0) if context is None else (context.size, context.rank)
1894 if packed is None:
1895 local_length = global_length // size
1896 if local_length % self.compress_ratio == 0:
1897 return None, None
1898 packed = SharedCompressedPackedSequence(
1899 torch.tensor((0, global_length)), rank * local_length, local_length, global_length,
1900 )
1901 packed = packed.prepare(device, (self.compress_ratio,))
1902 plan = packed.block_plan(self.compress_ratio, size, rank)
1903 positions = None if plan is None else packed.compressed_positions(plan, embeddings["compress"], context)
1904 return plan, positions
1905
1906 def _plan_bank_dispatch(
1907 self, packed_sequence: SharedCompressedPackedSequence | None,
1908 cp_context: SharedCompressedAttentionCPContext | None,
1909 sequence_length: int, global_sequence_length: int, device: torch.device,
1910 ) -> tuple[SequenceDispatchRoute | None, SequenceDispatchRoute | None, tuple[int, ...]]:
1911 """Reuse document boundaries to route each bank in its own physical coordinates.
1912
1913 Boundary reads happen only for CP communication planning; without a CP
1914 context the packed metadata carries no Python document list and the
1915 per-document token accounting lives entirely in tensors.
1916 """
1917 raw_route, main_route = None, None
1918 if cp_context is None:
1919 return raw_route, main_route, ()
1920 boundaries = (0, global_sequence_length)
1921 if packed_sequence is not None:
1922 boundaries = packed_sequence.prepare(device, (self.compress_ratio,)).document_boundaries()
1923 if cp_context.launch_raw is not None:
1924 raw_route = sliding_window_route(
1925 boundaries, sequence_length, cp_context.size, cp_context.rank, self.sliding_window,
1926 )
1927 if self.compress_ratio and cp_context.launch_main is not None:
1928 main_route = document_prefix_route(
1929 boundaries, sequence_length, cp_context.size, cp_context.rank, self.compress_ratio,
1930 )
1931 return raw_route, main_route, tuple(end - start for start, end in zip(boundaries, boundaries[1:]))
1932
1933 def _compute_compressed_kv(
1934 self,
1935 hidden_states: torch.Tensor,
1936 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
1937 shared_state: "SharedCompressedAttentionState",
1938 cp_context: "SharedCompressedAttentionCPContext | None",
1939 main_route: SequenceDispatchRoute | None = None,
1940 block_plan: Any = None,
1941 key_position_embeddings: tuple | None = None,
1942 defer_communication: bool = False,
1943 ) -> tuple[torch.Tensor | None, "SequenceGatherHandle | None"]:
1944 """Compress local hidden states and publish or launch the compressed KV.
1945
1946 The compressed K=V reaches its consumers - the producing Full layer
1947 included - through the shared state, so only the publish (or the CP
1948 gather handle that feeds it) leaves this call.
1949 """
1950 latent = None
1951 compressed_handle = None
1952 if self.is_kv_source:
1953 if block_plan is None:
1954 latent, compressed = self.compressor(hidden_states, position_embeddings["compress"])
1955 else:
1956 latent, compressed = self.compressor(hidden_states, key_position_embeddings, block_plan, cp_context)
1957 if cp_context is None:
1958 if not is_recomputing():
1959 shared_state.publish_compressed_kv(self.layer_idx, compressed)
1960 else:
1961 launch = partial(self._launch_compressed_bank, compressed, cp_context, main_route, block_plan)
1962 compressed_handle = _DeferredBankLaunch(launch) if defer_communication else launch()
1963 return latent, compressed_handle
1964
1965 @staticmethod
1966 def _launch_compressed_bank(compressed: torch.Tensor, context: SharedCompressedAttentionCPContext,
1967 route: SequenceDispatchRoute | None, block_plan: Any) -> SequenceGatherHandle:
1968 """Use the configured physical bank layout at the scheduled launch point."""
1969 if route is not None:
1970 return context.launch_main(compressed, 1, route)
1971 if block_plan is not None:
1972 return block_plan.launch_bank(compressed, context)
1973 return context.launch(compressed, 1)
1974
1975 def _run_indexer(
1976 self,
1977 hidden_states: torch.Tensor,
1978 query_residual: torch.Tensor,
1979 latent: torch.Tensor | None,
1980 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
1981 shared_state: "SharedCompressedAttentionState",
1982 cp_context: "SharedCompressedAttentionCPContext | None",
1983 tp_context: "SharedCompressedAttentionTPContext | None",
1984 query_offset: int,
1985 minimum_key_indices: torch.Tensor | None,
1986 query_segments: tuple,
1987 document_lengths: tuple[int, ...],
1988 block_plan: Any = None,
1989 key_position_embeddings: tuple | None = None,
1990 prepare_attention: Callable[[], None] | None = None,
1991 causal_upper: torch.Tensor | None = None,
1992 ) -> Any | None:
1993 """Run the indexer on index-source layers and publish its forward outputs."""
1994 if not self.is_index_source:
1995 return None
1996 index_key = (
1997 None
1998 if self.indexer.owns_key
1999 else shared_state.require_index_key(self.kv_source_layer_idx, self.layer_idx)
2000 )
2001 candidate_blocks = (
2002 shared_state.require_candidate_blocks(
2003 self.candidate_source_layer_idx,
2004 self.layer_idx,
2005 )
2006 if self.indexer.uses_candidates else None
2007 )
2008 indexer_output = self.indexer(
2009 hidden_states,
2010 query_residual,
2011 latent,
2012 position_embeddings["compress"],
2013 prepare_attention=prepare_attention,
2014 index_key=index_key,
2015 candidate_blocks=candidate_blocks,
2016 cp_context=cp_context,
2017 tp_context=tp_context,
2018 query_offset=query_offset,
2019 minimum_key_indices=minimum_key_indices,
2020 query_segments=query_segments,
2021 block_plan=block_plan,
2022 key_position_embeddings=key_position_embeddings,
2023 document_lengths=document_lengths,
2024 causal_upper=causal_upper,
2025 )
2026 if is_recomputing():
2027 # A recompute must not publish again: its consumers read the entries
2028 # the forward pass wrote, and a second write would break the
2029 # single-publisher contract.
2030 return indexer_output
2031 if self.indexer.owns_key:
2032 shared_state.publish_index_key(self.layer_idx, indexer_output.index_key)
2033 shared_state.publish_topk_indices(self.layer_idx, indexer_output.topk_indices)
2034 if indexer_output.candidate_blocks is not None:
2035 shared_state.publish_candidate_blocks(
2036 self.layer_idx,
2037 indexer_output.candidate_blocks,
2038 )
2039 return indexer_output
2040
2041 def _finish_bank_communication(
2042 self, key_value: torch.Tensor | None, raw_kv_handle: SequenceGatherHandle | None,
2043 compressed_handle: SequenceGatherHandle | None, shared_state: SharedCompressedAttentionState,
2044 main_route: SequenceDispatchRoute | None,
2045 ) -> torch.Tensor:
2046 """Resolve differentiable banks only when attention and KL can consume them."""
2047 if raw_kv_handle is not None:
2048 key_value = raw_kv_handle.wait()
2049 if compressed_handle is not None:
2050 compressed = compressed_handle.wait()
2051 if not is_recomputing():
2052 shared_state.publish_compressed_kv(
2053 self.layer_idx, compressed, 0 if main_route is None else main_route.global_start,
2054 )
2055 if key_value is None:
2056 raise RuntimeError("raw KV communication did not produce a tensor")
2057
2058 return key_value
2059
2060 def _build_sparse_indices(
2061 self,
2062 key_value: torch.Tensor,
2063 compressed_kv: torch.Tensor | None,
2064 topk_indices: torch.Tensor | None,
2065 sequence_length: int,
2066 query_offset: int,
2067 global_sequence_length: int,
2068 segment_starts: torch.Tensor | None,
2069 raw_key_offset: int = 0,
2070 main_key_offset: int = 0,
2071 ) -> tuple[torch.Tensor, torch.Tensor]:
2072 """Assemble dense-reference banks only after reference dispatch is selected."""
2073 window = build_sliding_window_indices(
2074 key_value.shape[0], sequence_length, self.sliding_window, key_value.device,
2075 query_offset=query_offset, key_length=global_sequence_length,
2076 )
2077 if segment_starts is not None:
2078 window.masked_fill_(window < segment_starts.unsqueeze(-1), -1)
2079 window = torch.where(window >= 0, window - raw_key_offset, -1)
2080 if compressed_kv is None:
2081 return key_value, window
2082 combined_key_value = torch.cat((key_value, compressed_kv.unsqueeze(1)), dim=2)
2083 compressed_indices = torch.where(
2084 topk_indices >= 0, topk_indices.long() - main_key_offset + key_value.shape[2], -1,
2085 )
2086 return combined_key_value, torch.cat((window, compressed_indices), dim=-1)
2087
2088 def _apply_indexer_loss(
2089 self,
2090 indexer_output: Any | None,
2091 query: torch.Tensor,
2092 compressed_kv: torch.Tensor | None,
2093 tp_context: "SharedCompressedAttentionTPContext | None",
2094 query_segments: tuple,
2095 main_key_offset: int = 0,
2096 ) -> torch.Tensor:
2097 """Auto-scale the query by the indexer KL loss when configured."""
2098 if (
2099 indexer_output is not None
2100 and self.training
2101 and self.indexer.loss_coeff
2102 and compressed_kv is not None
2103 ):
2104 # KL uses the original owner's queries. A view aligns its student K
2105 # with the compact teacher bank while preserving global Index K gradients.
2106 index_key = indexer_output.index_key
2107 if main_key_offset or compressed_kv.shape[1] != index_key.shape[1]:
2108 index_key = index_key[:, main_key_offset:main_key_offset+compressed_kv.shape[1]]
2109 selected = indexer_output.topk_indices
2110 if main_key_offset:
2111 selected = torch.where(selected >= 0, selected - main_key_offset, -1)
2112 query_segments = tuple((s, e, ks-main_key_offset, ke-main_key_offset, residual)
2113 for s, e, ks, ke, residual in query_segments)
2114 indexer_loss = shared_compressed_indexer_kl_loss(
2115 indexer_output.index_query,
2116 index_key,
2117 indexer_output.merge_weight,
2118 query,
2119 compressed_kv,
2120 selected,
2121 self.sinks,
2122 attention_scale=self.scaling,
2123 loss_coeff=self.indexer.loss_coeff,
2124 query_chunk_size=self.indexer.query_chunk_size,
2125 tp_context=tp_context,
2126 compress_ratio=self.compress_ratio,
2127 query_segments=query_segments,
2128 use_fused=self.use_fused_ops,
2129 )
2130 query = aux_loss_auto_scale(query, indexer_loss)
2131 return query
2132
2133 def _run_sparse_attention(
2134 self, query: torch.Tensor, key_value: torch.Tensor, compressed_kv: torch.Tensor | None,
2135 topk_indices: torch.Tensor | None, query_offset: int, global_sequence_length: int,
2136 segment_starts: torch.Tensor | None, query_segments: tuple, raw_key_offset: int = 0,
2137 main_key_offset: int = 0, raw_document_starts: tuple = (), global_key_length: int | None = None,
2138 ) -> torch.Tensor:
2139 """Run the chosen implementation; unsupported explicit fusion always raises."""
2140 if self.use_fused_ops:
2141 expected_topk = min(512, global_key_length if global_key_length is not None
2142 else global_sequence_length // max(1, self.compress_ratio))
2143 if not (self.candidate_source_layer_idx is None
2144 and (topk_indices is None or topk_indices.shape[-1] == expected_topk)
2145 and supports_fused_attention_inputs(
2146 query, self.compress_ratio, self.sliding_window,
2147 0 if topk_indices is None else topk_indices.shape[-1])):
2148 raise RuntimeError(
2149 "V4.1 fused attention is required but this call is unsupported: "
2150 f"layer={self.layer_idx}, device={query.device}, dtype={query.dtype}, Q={tuple(query.shape)}, "
2151 f"ratio={self.compress_ratio}, window={self.sliding_window}. "
2152 "Check the SMLA training extension and native input contract; no reference fallback was run."
2153 )
2154 output = sparse_flash_mla(
2155 query, key_value, compressed_kv, topk_indices, self.sinks, self.scaling,
2156 self.compress_ratio, self.sliding_window, query_offset, query_segments, raw_key_offset, main_key_offset,
2157 raw_document_starts,
2158 )
2159 if compressed_kv is not None and all(end == start for _, _, start, end, _ in query_segments):
2160 # Empty-prefix ranks must still participate in the shared bank's collective backward.
2161 output = output + compressed_kv[:, :0].sum().to(output.dtype)
2162 return output
2163 combined_key_value, sparse_indices = self._build_sparse_indices(
2164 key_value, compressed_kv, topk_indices, query.shape[2], query_offset,
2165 global_sequence_length, segment_starts, raw_key_offset, main_key_offset,
2166 )
2167 return reference_sparse_attention(query, combined_key_value, sparse_indices, self.sinks, self.scaling)
2168
2169 def _project_output(
2170 self,
2171 attention_output: torch.Tensor,
2172 cos: torch.Tensor,
2173 sin: torch.Tensor,
2174 batch_size: int,
2175 sequence_length: int,
2176 ) -> torch.Tensor:
2177 """Undo RoPE and apply the grouped low-rank output projection."""
2178 attention_output = _apply_v41_rope(attention_output, cos, -sin, unsqueeze_dim=2)
2179 grouped = attention_output.reshape(batch_size, sequence_length, self.num_groups, -1)
2180 hidden_per_group = grouped.shape[-1]
2181 output_per_group = self.o_a_proj.weight.shape[0] // self.num_groups
2182 projection = self.o_a_proj.weight.view(
2183 self.num_groups, output_per_group, hidden_per_group
2184 ).transpose(1, 2)
2185 flattened = grouped.reshape(-1, self.num_groups, hidden_per_group).transpose(0, 1)
2186 projected = torch.bmm(flattened, projection).transpose(0, 1)
2187 projected = projected.reshape(batch_size, sequence_length, -1)
2188 return self.o_b_proj(projected)
2189
2190 def forward(
2191 self,
2192 hidden_states: torch.Tensor,
2193 position_embeddings: dict[str, tuple[torch.Tensor, torch.Tensor]],
2194 position_ids: torch.Tensor,
2195 attention_mask: torch.Tensor | None,
2196 past_key_values: Any | None = None,
2197 **kwargs: Any,
2198 ) -> tuple[torch.Tensor, torch.Tensor | None]:
2199 """Publish or consume compressed KV and execute indexed K=V attention."""
2200 (
2201 shared_state,
2202 packed_sequence,
2203 cp_context,
2204 tp_context,
2205 batch_size,
2206 sequence_length,
2207 ) = self._validate_forward_inputs(hidden_states, attention_mask, past_key_values, kwargs)
2208 query_offset = 0
2209 global_sequence_length = sequence_length
2210 if cp_context is not None:
2211 query_offset, global_sequence_length = self._validate_cp_inputs(
2212 cp_context,
2213 position_ids,
2214 sequence_length,
2215 )
2216 if packed_sequence is not None:
2217 packed_sequence = packed_sequence.prepare(hidden_states.device, (self.compress_ratio,))
2218 if (self.use_fused_ops and packed_sequence._restored_from_packing_tensors): # pylint: disable=protected-access
2219 raise NotImplementedError(
2220 "fused DSA operators require Python document segments and are not "
2221 "supported with graph packing inputs; use the reference implementation"
2222 )
2223 segment_starts, minimum_key_indices, query_segments, causal_upper = self._resolve_packed_segments(
2224 packed_sequence,
2225 batch_size,
2226 sequence_length,
2227 query_offset,
2228 global_sequence_length,
2229 hidden_states.device,
2230 )
2231
2232 block_plan, key_positions = self._prepare_compression(
2233 packed_sequence, cp_context, position_embeddings, global_sequence_length, hidden_states.device,
2234 )
2235 raw_document_starts = () if packed_sequence is None else packed_sequence.raw_document_starts()
2236 raw_route, main_route, document_lengths = self._plan_bank_dispatch(
2237 packed_sequence, cp_context, sequence_length, global_sequence_length, hidden_states.device,
2238 )
2239 if packed_sequence is not None:
2240 global_key_length = packed_sequence.compressed_key_length(self.compress_ratio)
2241 else:
2242 global_key_length = global_sequence_length // max(1, self.compress_ratio)
2243 raw_key_offset = 0 if raw_route is None else raw_route.global_start
2244 rope_type = "compress" if self.compress_ratio else "main"
2245 cos, sin = position_embeddings[rope_type]
2246 key_value, raw_kv_handle = self._project_raw_kv(hidden_states, cos, sin, cp_context, raw_route)
2247 overlap = (self.is_index_source and cp_context is not None
2248 and (cp_context.indexer_priority or cp_context.launch_balance is not None))
2249 prepare_attention = None
2250 if overlap:
2251 query_residual = self.q_a_norm(self.q_a_proj(hidden_states))
2252 query = None
2253
2254 def _prepare_attention() -> None:
2255 """Keep differentiable main Q work outside selection checkpoint regions."""
2256 nonlocal query
2257 if isinstance(compressed_handle, _DeferredBankLaunch):
2258 compressed_handle.start()
2259 query = self._expand_query(query_residual, cos, sin)
2260
2261 prepare_attention = _prepare_attention
2262 else:
2263 query_residual, query = self._project_query(hidden_states, cos, sin)
2264 latent, compressed_handle = self._compute_compressed_kv(
2265 hidden_states, position_embeddings, shared_state, cp_context, main_route, block_plan, key_positions,
2266 defer_communication=overlap and cp_context.indexer_priority,
2267 )
2268 indexer_output = self._run_indexer(
2269 hidden_states, query_residual, latent, position_embeddings, shared_state, cp_context, tp_context,
2270 query_offset, minimum_key_indices, query_segments, document_lengths, block_plan, key_positions,
2271 prepare_attention=prepare_attention, causal_upper=causal_upper,
2272 )
2273
2274 key_value = self._finish_bank_communication(
2275 key_value, raw_kv_handle, compressed_handle, shared_state, main_route,
2276 )
2277
2278 compressed_kv = None
2279 main_key_offset = 0
2280 topk_indices = None
2281 if self.compress_ratio:
2282 compressed_kv = shared_state.require_compressed_kv(self.kv_source_layer_idx, self.layer_idx)
2283 topk_indices = shared_state.require_topk_indices(self.index_source_layer_idx, self.layer_idx)
2284 main_key_offset = shared_state.compressed_kv_offsets.get(self.kv_source_layer_idx, 0)
2285 query = self._apply_indexer_loss(
2286 indexer_output, query, compressed_kv, tp_context, query_segments, main_key_offset,
2287 )
2288 attention_output = self._run_sparse_attention(
2289 query, key_value, compressed_kv, topk_indices, query_offset, global_sequence_length,
2290 segment_starts, query_segments, raw_key_offset, main_key_offset, raw_document_starts,
2291 global_key_length,
2292 )
2293 output = self._project_output(attention_output, cos, sin, batch_size, sequence_length)
2294 return output, None
2295
2296
2297@module_replacement
2298class SharedCompressedDSAAttention(SharedCompressedDSAAttentionBase):
2299 """High-performance V4.1 shared compressed attention replacement."""
2300
2301 def __init__(
2302 self,
2303 module: nn.Module,
2304 module_fqn: str = "",
2305 context: Mapping[str, Any] | None = None,
2306 *,
2307 use_fused_ops: bool = False,
2308 indexer_load_balance: str = "none",
2309 main_kv_comm: str = "all_gather",
2310 indexer_balance_block_size: int | None = None,
2311 indexer_zigzag_min_length: int | None = None,
2312 ) -> None:
2313 """Transfer source state and select the implementation used for comparisons.
2314
2315 Args:
2316 module: Source attention module exposing the shared-compressed DSA
2317 structural contract.
2318 module_fqn: Fully qualified source name supplied by replacement.
2319 context: Read-only replacement context supplied by Trainer.
2320 use_fused_ops: Use LI V2, SMLA and fused KL when True,
2321 raising on unsupported calls without automatic fallback.
2322 False selects eager PyTorch attention, Indexer and KL on every
2323 device, without loading optional operator extensions.
2324 indexer_load_balance: 'none', 'zigzag', 'greedy' or 'hybrid' query-task allocation.
2325 Selection exchanges low-rank qr, merge weights and rotary embeddings;
2326 attention and KL retain the original owner and autograd graph.
2327 indexer_balance_block_size: Maximum original query rows per greedy task,
2328 before adjacent tasks on one worker are merged. Greedy/hybrid default to 2048.
2329 Explicit values require greedy/hybrid and must be positive and no larger
2330 than local Q. Omit this option for none/zigzag.
2331 indexer_zigzag_min_length: Explicit hybrid-only minimum document length in raw tokens.
2332 Eligible head/tail fragments stay intact; leading remainders and other documents
2333 share the greedy pool and row budget. Documents shorter than 2*CP have no complete
2334 zigzag fragments and enter the greedy pool.
2335 main_kv_comm: 'all_to_all_v' dispatches document-causal main KV prefixes;
2336 'all_gather' retains the full-bank comparison.
2337
2338 Note:
2339 All implementations retain the same parameter and CP/TP contracts.
2340 Selecting fusion preserves the existing external teacher and scalar
2341 KL. Reuse and disabled KL do not introduce extra operator calls.
2342 Fusion supports ordinary causal r1/r2 training on NPU with BF16
2343 main attention, D=512, SWA=128, Indexer D=128, TopK=512 and TP=1.
2344 Active fused KL requires 8, 16, 32 or 64 Indexer heads. Candidate
2345 pools are unsupported. Packed inputs compress each document independently;
2346 multiple local document segments use native TND batching.
2347 The ops-transformer training extension is loaded on the first
2348 supported fused call; a missing extension raises ImportError.
2349 """
2350 if not isinstance(use_fused_ops, bool):
2351 raise ValueError(f"use_fused_ops must be a bool, got {use_fused_ops!r}")
2352 if main_kv_comm not in ("all_gather", "all_to_all_v"):
2353 raise ValueError("main_kv_comm must be 'all_gather' or 'all_to_all_v'")
2354 indexer_balance_block_size = resolve_indexer_balance_block_size(
2355 indexer_load_balance, indexer_balance_block_size,
2356 )
2357 validate_indexer_hybrid_threshold(
2358 indexer_load_balance, indexer_zigzag_min_length,
2359 )
2360 del module_fqn, context
2361 super().__init__(
2362 module,
2363 replace_indexer=True,
2364 use_fused_ops=use_fused_ops,
2365 )
2366 self.indexer_load_balance = indexer_load_balance
2367 self.indexer_balance_block_size = indexer_balance_block_size
2368 self.indexer_zigzag_min_length = indexer_zigzag_min_length
2369 self.main_kv_comm = main_kv_comm
2370
2371
2372__all__ = [
2373 "SharedCompressedAttentionCPContext",
2374 "SharedCompressedAttentionState",
2375 "SharedCompressedPackedSequence",
2376 "SharedCompressedAttentionTPContext",
2377 "SharedCompressedDSAAttention",
2378 "SharedCompressedDSAAttentionBase",
2379 "SharedCompressedDSAIndexer",
2380 "build_sliding_window_indices",
2381 "compressed_candidate_topk",
2382 "compressed_causal_topk_and_candidates",
2383 "compressed_causal_topk",
2384 "select_candidate_block_indices",
2385 "select_candidate_blocks",
2386 "shared_compressed_indexer_kl_loss",
2387 "reference_sparse_attention",
2388]