Rorical commited on
Commit
7c38aee
·
verified ·
1 Parent(s): 08d8b53

Fix inference code: hybrid.py

Browse files
Files changed (1) hide show
  1. hybrid.py +1409 -0
hybrid.py ADDED
@@ -0,0 +1,1409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hybrid attention modules: KDA, local SWA, CSA, and HCA.
2
+
3
+ CSA/HCA are DeepSeek-V4-style compressed global attentions. They compress
4
+ sequence-dimension KV entries with learned per-dimension pooling, then run
5
+ shared-KV MQA over the compressed entries. CSA uses light compression plus a
6
+ two-stage sparse recall path; HCA uses heavier compression plus dense global
7
+ recall. No snapshot memory path is used.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+ from .lm_loss import (
20
+ lm_cross_entropy_from_logits,
21
+ token_superposition_attention_mask,
22
+ token_superposition_embeddings,
23
+ )
24
+ from .linear import LinearConfig, KimiDeltaAttention
25
+ from .baseline import (
26
+ RMSNorm,
27
+ Attention,
28
+ RotaryEmbedding,
29
+ manual_attention,
30
+ SwiGLU,
31
+ MoELayer,
32
+ combine_lm_and_aux_loss,
33
+ init_moe_router_weights,
34
+ count_parameters,
35
+ model_summary,
36
+ )
37
+
38
+
39
+ _ATTN_TYPES = ("kda", "swa", "csa", "hca")
40
+
41
+
42
+ def normalize_attention_type(kind: str) -> str:
43
+ kind = kind.strip().lower()
44
+ aliases = {
45
+ "linear": "kda",
46
+ "local": "swa",
47
+ "sliding": "swa",
48
+ "sliding_window": "swa",
49
+ "compressed_sparse": "csa",
50
+ "compressed": "csa",
51
+ "heavily_compressed": "hca",
52
+ "global": "hca",
53
+ }
54
+ kind = aliases.get(kind, kind)
55
+ if kind not in _ATTN_TYPES:
56
+ raise ValueError(
57
+ f"Unknown attention type {kind!r}; expected one of {_ATTN_TYPES}."
58
+ )
59
+ return kind
60
+
61
+
62
+ def parse_attention_pattern(pattern: Optional[str]) -> List[str]:
63
+ if pattern is None:
64
+ return []
65
+ pattern = pattern.strip()
66
+ if not pattern:
67
+ return []
68
+ for sep in (";", "|"):
69
+ pattern = pattern.replace(sep, ",")
70
+ return [
71
+ normalize_attention_type(part)
72
+ for part in pattern.split(",")
73
+ if part.strip()
74
+ ]
75
+
76
+
77
+ def expand_attention_pattern(
78
+ pattern: Optional[str],
79
+ length: int,
80
+ *,
81
+ default: str,
82
+ ) -> List[str]:
83
+ if length < 0:
84
+ raise ValueError("length must be >= 0")
85
+ values = parse_attention_pattern(pattern)
86
+ if not values:
87
+ values = [normalize_attention_type(default)]
88
+ return [values[i % len(values)] for i in range(length)]
89
+
90
+
91
+ def _local_swa_kind(layer_idx: int, swa_every: int, swa_offset: int) -> str:
92
+ return "swa" if (layer_idx % swa_every) == swa_offset else "kda"
93
+
94
+
95
+ def default_hybrid_attention_pattern(config: "HybridConfig", length: int) -> List[str]:
96
+ return [
97
+ _local_swa_kind(i, config.swa_every, config.swa_offset)
98
+ for i in range(length)
99
+ ]
100
+
101
+
102
+ @dataclass
103
+ class HybridConfig(LinearConfig):
104
+ swa_window: int = 256
105
+ swa_every: int = 4
106
+ swa_offset: int = 3
107
+
108
+ # Compressed global attention. CSA defaults to 4-token compression and
109
+ # sparse top-k recall; HCA defaults to 128-token compression and dense
110
+ # recall over all compressed entries.
111
+ csa_compression: int = 4
112
+ csa_top_k: int = 1024
113
+ csa_indexer_heads: int = 4
114
+ csa_indexer_dim: int = 32
115
+ # Weight on the CSA indexer's attention-aligned KL loss. The indexer is a
116
+ # separate sparse-recall selector whose top-k is non-differentiable, so it
117
+ # receives zero gradient otherwise. The loss trains only indexer params
118
+ # (trunk inputs are detached), so the weight is forgiving; 1.0 matches the
119
+ # DeepSeek-V3.2 lightning-indexer recipe and is not decayed.
120
+ csa_indexer_loss_weight: float = 1.0
121
+ hca_compression: int = 128
122
+ compressed_query_dim: Optional[int] = None
123
+ compressed_head_dim: Optional[int] = None
124
+ # Two independent, EXPERIMENTAL rotary paths over the compressed attention.
125
+ # Both default OFF: the NSA-endorsed scheme is positionless pooled keys
126
+ # (intra-block position comes from the TokenCompressor pos_bias plus the
127
+ # causal group masks), so turning either on departs from the published
128
+ # evidence and changes training behavior.
129
+ #
130
+ # compressed_rope: rotate the compressed-attention scoring q/k. Queries
131
+ # rotate at their true token position; pooled keys at a per-group
132
+ # representative position (the group's last token). NSA (2502.11089)
133
+ # deliberately keeps COMPRESSED keys RoPE-free and ranks RoPE'd queries
134
+ # against non-RoPE'd compressed keys, so this is an experiment, not the
135
+ # evidence-backed default.
136
+ compressed_rope: bool = False
137
+ # indexer_rope: rotate the CSA indexer q/k. DSA / DeepSeek-V3.2's lightning
138
+ # indexer applies partial RoPE, but it ranks RAW tokens, not pooled groups
139
+ # — so rotating an indexer that scores pooled groups is unsupported by the
140
+ # evidence and stays OFF by default.
141
+ indexer_rope: bool = False
142
+
143
+ # Comma/semicolon-separated pattern, e.g. "hca,csa,csa,swa".
144
+ # If unset, Hybrid preserves the old structural KDA/SWA schedule.
145
+ attn_pattern: Optional[str] = None
146
+
147
+ def __post_init__(self):
148
+ super().__post_init__()
149
+ if self.swa_window < 1:
150
+ raise ValueError("swa_window must be >= 1")
151
+ if self.swa_every < 1:
152
+ raise ValueError("swa_every must be >= 1")
153
+ if not (0 <= self.swa_offset < self.swa_every):
154
+ raise ValueError(
155
+ f"swa_offset ({self.swa_offset}) must be in [0, swa_every={self.swa_every})"
156
+ )
157
+ if self.csa_compression < 1:
158
+ raise ValueError("csa_compression must be >= 1")
159
+ if self.csa_top_k < 1:
160
+ raise ValueError("csa_top_k must be >= 1")
161
+ if self.csa_indexer_heads < 1:
162
+ raise ValueError("csa_indexer_heads must be >= 1")
163
+ if self.csa_indexer_dim < 1:
164
+ raise ValueError("csa_indexer_dim must be >= 1")
165
+ if self.csa_indexer_loss_weight < 0:
166
+ raise ValueError("csa_indexer_loss_weight must be >= 0")
167
+ if self.hca_compression < 1:
168
+ raise ValueError("hca_compression must be >= 1")
169
+ if self.compressed_query_dim is not None and self.compressed_query_dim < 1:
170
+ raise ValueError("compressed_query_dim must be >= 1")
171
+ if self.compressed_head_dim is not None and self.compressed_head_dim < 1:
172
+ raise ValueError("compressed_head_dim must be >= 1")
173
+ parse_attention_pattern(self.attn_pattern)
174
+
175
+
176
+ try:
177
+ from torch.nn.attention.flex_attention import (
178
+ flex_attention as _flex_attention,
179
+ create_block_mask as _create_block_mask,
180
+ )
181
+ _HAS_FLEX = True
182
+ _flex_attention_fused = torch.compile(_flex_attention, dynamic=True)
183
+ except ImportError:
184
+ _HAS_FLEX = False
185
+ _flex_attention_fused = None
186
+ try:
187
+ import torch._dynamo.config as _dynamo_config
188
+ for _attr in ("recompile_limit", "cache_size_limit"):
189
+ if hasattr(_dynamo_config, _attr):
190
+ setattr(_dynamo_config, _attr, max(64, getattr(_dynamo_config, _attr)))
191
+ except Exception:
192
+ pass
193
+
194
+
195
+ class LocalAttention(Attention):
196
+ """Causal sliding-window MHA with a dense CPU fallback."""
197
+
198
+ def __init__(self, config: HybridConfig):
199
+ super().__init__(config)
200
+ self.window = config.swa_window
201
+ self._block_mask_cache: Dict[Tuple[int, bool, bool, str], Any] = {}
202
+
203
+ def _apply_rope_at_positions(
204
+ self,
205
+ x: torch.Tensor,
206
+ positions: torch.Tensor,
207
+ ) -> torch.Tensor:
208
+ if self.rope_dim >= x.shape[-1]:
209
+ return self.rotary.forward_at_positions(x, positions)
210
+ no_rope = x[..., :-self.rope_dim]
211
+ rope = self.rotary.forward_at_positions(x[..., -self.rope_dim:], positions)
212
+ return torch.cat([no_rope, rope], dim=-1)
213
+
214
+ @staticmethod
215
+ def _current_attention_mask(
216
+ attention_mask: Optional[torch.Tensor],
217
+ seq_len: int,
218
+ ) -> Optional[torch.Tensor]:
219
+ if attention_mask is None:
220
+ return None
221
+ if attention_mask.size(1) == seq_len:
222
+ return attention_mask
223
+ return attention_mask[:, -seq_len:]
224
+
225
+ def _build_mask(
226
+ self,
227
+ batch: int,
228
+ seq_len: int,
229
+ device: torch.device,
230
+ attention_mask: Optional[torch.Tensor],
231
+ is_causal: bool,
232
+ ) -> Optional[torch.Tensor]:
233
+ idx = torch.arange(seq_len, device=device)
234
+ rel = idx.unsqueeze(0) - idx.unsqueeze(1)
235
+ if is_causal:
236
+ window_mask = (rel <= 0) & (rel > -self.window)
237
+ else:
238
+ window_mask = rel.abs() < self.window
239
+ mask = window_mask.unsqueeze(0).unsqueeze(0)
240
+
241
+ if attention_mask is not None:
242
+ key_mask = attention_mask.unsqueeze(1).unsqueeze(2).bool()
243
+ key_mask = key_mask.expand(batch, 1, seq_len, seq_len)
244
+ mask = mask & key_mask
245
+
246
+ return mask
247
+
248
+ def _get_block_mask(
249
+ self,
250
+ seq_len: int,
251
+ has_sink: bool,
252
+ is_causal: bool,
253
+ device: torch.device,
254
+ ):
255
+ key = (seq_len, has_sink, is_causal, str(device))
256
+ bm = self._block_mask_cache.get(key)
257
+ if bm is not None:
258
+ return bm
259
+
260
+ window = self.window
261
+ if has_sink:
262
+ kv_len = seq_len + 1
263
+ if is_causal:
264
+ def mask_mod(b, h, q_idx, kv_idx):
265
+ is_sink = kv_idx == 0
266
+ real_kv = kv_idx - 1
267
+ in_window = (q_idx >= real_kv) & (q_idx - real_kv < window)
268
+ return is_sink | in_window
269
+ else:
270
+ def mask_mod(b, h, q_idx, kv_idx):
271
+ is_sink = kv_idx == 0
272
+ real_kv = kv_idx - 1
273
+ in_window = (real_kv - q_idx).abs() < window
274
+ return is_sink | in_window
275
+ else:
276
+ kv_len = seq_len
277
+ if is_causal:
278
+ def mask_mod(b, h, q_idx, kv_idx):
279
+ return (q_idx >= kv_idx) & (q_idx - kv_idx < window)
280
+ else:
281
+ def mask_mod(b, h, q_idx, kv_idx):
282
+ return (q_idx - kv_idx).abs() < window
283
+
284
+ bm = _create_block_mask(
285
+ mask_mod, B=None, H=None, Q_LEN=seq_len, KV_LEN=kv_len,
286
+ device=device,
287
+ )
288
+ self._block_mask_cache[key] = bm
289
+ return bm
290
+
291
+ def forward(
292
+ self,
293
+ x: torch.Tensor,
294
+ attention_mask: Optional[torch.Tensor] = None,
295
+ is_causal: bool = True,
296
+ cache: Optional[Dict[str, Any]] = None,
297
+ position_offset: int = 0,
298
+ ) -> torch.Tensor:
299
+ if cache is not None:
300
+ return self._forward_cached(
301
+ x,
302
+ attention_mask=attention_mask,
303
+ is_causal=is_causal,
304
+ cache=cache,
305
+ position_offset=position_offset,
306
+ )
307
+
308
+ if attention_mask is not None or not _HAS_FLEX or not x.is_cuda:
309
+ return super().forward(x, attention_mask=attention_mask, is_causal=is_causal)
310
+
311
+ batch, seq_len, _ = x.shape
312
+ q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
313
+ k = self.k_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
314
+ v = self.v_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
315
+ if self.qk_norm:
316
+ q = self.q_norm(q)
317
+ k = self.k_norm(k)
318
+ q = q.transpose(1, 2)
319
+ k = k.transpose(1, 2)
320
+ v = v.transpose(1, 2)
321
+ q = self._apply_rope(q, seq_len)
322
+ k = self._apply_rope(k, seq_len)
323
+
324
+ has_sink = self.attention_sink
325
+ if has_sink:
326
+ sink_k = torch.zeros(
327
+ batch, self.num_heads, 1, self.head_dim,
328
+ device=q.device, dtype=q.dtype,
329
+ )
330
+ sink_v = torch.zeros_like(sink_k)
331
+ k = torch.cat([sink_k, k], dim=2)
332
+ v = torch.cat([sink_v, v], dim=2)
333
+ sink_logit = self.sink_logit
334
+
335
+ def score_mod(score, b, h, q_idx, kv_idx):
336
+ sink = sink_logit[h].to(score.dtype)
337
+ return torch.where(kv_idx == 0, sink, score)
338
+ else:
339
+ score_mod = None
340
+
341
+ block_mask = self._get_block_mask(
342
+ seq_len, has_sink=has_sink, is_causal=is_causal, device=q.device,
343
+ )
344
+ out = _flex_attention_fused(
345
+ q, k, v, score_mod=score_mod, block_mask=block_mask,
346
+ )
347
+
348
+ out = out.transpose(1, 2).contiguous().view(batch, seq_len, self.d_model)
349
+ return self.out_proj(out)
350
+
351
+ def _forward_cached(
352
+ self,
353
+ x: torch.Tensor,
354
+ attention_mask: Optional[torch.Tensor],
355
+ is_causal: bool,
356
+ cache: Dict[str, Any],
357
+ position_offset: int,
358
+ ) -> torch.Tensor:
359
+ batch, seq_len, _ = x.shape
360
+ q = self.q_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
361
+ k = self.k_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
362
+ v = self.v_proj(x).view(batch, seq_len, self.num_heads, self.head_dim)
363
+ if self.qk_norm:
364
+ q = self.q_norm(q)
365
+ k = self.k_norm(k)
366
+ q = q.transpose(1, 2)
367
+ k = k.transpose(1, 2)
368
+ v = v.transpose(1, 2)
369
+
370
+ q_positions = torch.arange(
371
+ position_offset,
372
+ position_offset + seq_len,
373
+ device=x.device,
374
+ dtype=torch.long,
375
+ )
376
+ q = self._apply_rope_at_positions(q, q_positions)
377
+ k = self._apply_rope_at_positions(k, q_positions)
378
+
379
+ current_mask = self._current_attention_mask(attention_mask, seq_len)
380
+ past_k = cache.get("k")
381
+ past_v = cache.get("v")
382
+ past_positions = cache.get("positions")
383
+ past_key_mask = cache.get("key_mask")
384
+ if past_k is not None:
385
+ k_all = torch.cat([past_k, k], dim=2)
386
+ v_all = torch.cat([past_v, v], dim=2)
387
+ key_positions = torch.cat([past_positions.to(x.device), q_positions], dim=0)
388
+ if past_key_mask is not None or current_mask is not None:
389
+ if past_key_mask is None:
390
+ past_key_mask = torch.ones(
391
+ batch, past_k.size(2), device=x.device, dtype=torch.bool,
392
+ )
393
+ else:
394
+ past_key_mask = past_key_mask.to(x.device).bool()
395
+ if current_mask is None:
396
+ cur_key_mask = torch.ones(
397
+ batch, seq_len, device=x.device, dtype=torch.bool,
398
+ )
399
+ else:
400
+ cur_key_mask = current_mask.bool()
401
+ key_mask = torch.cat([past_key_mask, cur_key_mask], dim=1)
402
+ else:
403
+ key_mask = None
404
+ else:
405
+ k_all = k
406
+ v_all = v
407
+ key_positions = q_positions
408
+ key_mask = current_mask.bool() if current_mask is not None else None
409
+
410
+ if is_causal:
411
+ rel = q_positions.view(1, 1, seq_len, 1) - key_positions.view(1, 1, 1, -1)
412
+ mask = (rel >= 0) & (rel < self.window)
413
+ else:
414
+ rel = (q_positions.view(1, 1, seq_len, 1) - key_positions.view(1, 1, 1, -1)).abs()
415
+ mask = rel < self.window
416
+ if key_mask is not None:
417
+ mask = mask & key_mask.view(batch, 1, 1, -1)
418
+
419
+ if self.attention_sink:
420
+ out = manual_attention(q, k_all, v_all, mask=mask, sink_logit=self.sink_logit)
421
+ else:
422
+ out = F.scaled_dot_product_attention(
423
+ q,
424
+ k_all,
425
+ v_all,
426
+ attn_mask=mask,
427
+ is_causal=False,
428
+ )
429
+
430
+ keep = min(k_all.size(2), max(1, self.window))
431
+ cache["k"] = k_all[:, :, -keep:, :].detach()
432
+ cache["v"] = v_all[:, :, -keep:, :].detach()
433
+ cache["positions"] = key_positions[-keep:].detach()
434
+ cache["key_mask"] = (
435
+ key_mask[:, -keep:].detach() if key_mask is not None else None
436
+ )
437
+ cache["seen_tokens"] = int(position_offset + seq_len)
438
+
439
+ out = out.transpose(1, 2).contiguous().view(batch, seq_len, self.d_model)
440
+ return self.out_proj(out)
441
+
442
+
443
+ class TokenCompressor(nn.Module):
444
+ """Learned per-dimension compression over fixed-size token groups."""
445
+
446
+ def __init__(self, d_model: int, head_dim: int, compression: int, overlap: bool):
447
+ super().__init__()
448
+ self.head_dim = head_dim
449
+ self.compression = compression
450
+ self.overlap = overlap
451
+ self.kv_proj_a = nn.Linear(d_model, head_dim, bias=False)
452
+ self.weight_proj_a = nn.Linear(d_model, head_dim, bias=False)
453
+ self.pos_bias_a = nn.Parameter(torch.zeros(compression, head_dim))
454
+ if overlap:
455
+ self.kv_proj_b = nn.Linear(d_model, head_dim, bias=False)
456
+ self.weight_proj_b = nn.Linear(d_model, head_dim, bias=False)
457
+ self.pos_bias_b = nn.Parameter(torch.zeros(compression, head_dim))
458
+
459
+ def _group(
460
+ self,
461
+ x: torch.Tensor,
462
+ z: torch.Tensor,
463
+ mask: Optional[torch.Tensor],
464
+ pos_bias: torch.Tensor,
465
+ pad_front: int = 0,
466
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
467
+ B, T, D = x.shape
468
+ m = self.compression
469
+ if mask is None:
470
+ mask = torch.ones(B, T, device=x.device, dtype=torch.bool)
471
+ if pad_front > 0:
472
+ x = F.pad(x, (0, 0, pad_front, 0))
473
+ z = F.pad(z, (0, 0, pad_front, 0), value=float("-inf"))
474
+ mask = F.pad(mask, (pad_front, 0), value=0)
475
+ T = x.size(1)
476
+ pad = (m - T % m) % m
477
+ if pad > 0:
478
+ x = F.pad(x, (0, 0, 0, pad))
479
+ z = F.pad(z, (0, 0, 0, pad), value=float("-inf"))
480
+ mask = F.pad(mask, (0, pad), value=0)
481
+
482
+ G = x.size(1) // m
483
+ x_g = x.view(B, G, m, D)
484
+ z_g = z.view(B, G, m, D) + pos_bias.view(1, 1, m, D)
485
+ valid = mask.view(B, G, m).bool()
486
+ z_g = z_g.masked_fill(~valid.unsqueeze(-1), float("-inf"))
487
+ return x_g, z_g, valid
488
+
489
+ def forward(
490
+ self,
491
+ hidden: torch.Tensor,
492
+ attention_mask: Optional[torch.Tensor] = None,
493
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
494
+ kv_a = self.kv_proj_a(hidden)
495
+ z_a = self.weight_proj_a(hidden)
496
+ a_x, a_z, a_valid = self._group(
497
+ kv_a, z_a, attention_mask, self.pos_bias_a,
498
+ )
499
+
500
+ if self.overlap:
501
+ kv_b = self.kv_proj_b(hidden)
502
+ z_b = self.weight_proj_b(hidden)
503
+ b_x, b_z, b_valid = self._group(
504
+ kv_b, z_b, attention_mask, self.pos_bias_b,
505
+ pad_front=self.compression,
506
+ )
507
+ # b group 0 is pure left padding; b group i+1 overlaps a group i.
508
+ b_x = b_x[:, :a_x.size(1)]
509
+ b_z = b_z[:, :a_z.size(1)]
510
+ b_valid = b_valid[:, :a_valid.size(1)]
511
+ values = torch.cat([a_x, b_x], dim=2)
512
+ logits = torch.cat([a_z, b_z], dim=2)
513
+ valid = torch.cat([a_valid, b_valid], dim=2)
514
+ else:
515
+ values = a_x
516
+ logits = a_z
517
+ valid = a_valid
518
+
519
+ logits = logits.masked_fill(~valid.unsqueeze(-1), float("-inf"))
520
+ all_invalid = ~valid.any(dim=2, keepdim=True)
521
+ logits = torch.where(
522
+ all_invalid.unsqueeze(-1),
523
+ torch.zeros_like(logits),
524
+ logits,
525
+ )
526
+ weights = torch.softmax(logits.float(), dim=2).to(values.dtype)
527
+ weights = torch.where(valid.unsqueeze(-1), weights, torch.zeros_like(weights))
528
+ compressed = torch.sum(weights * values, dim=2)
529
+ group_valid = valid.any(dim=2)
530
+ return compressed, group_valid
531
+
532
+
533
+ class CompressedGlobalAttention(nn.Module):
534
+ """Shared-KV compressed attention used for CSA and HCA."""
535
+
536
+ def __init__(self, config: HybridConfig, *, mode: str):
537
+ super().__init__()
538
+ self.mode = mode
539
+ self.d_model = config.d_model
540
+ self.num_heads = config.num_heads
541
+ self.head_dim = config.compressed_head_dim or config.head_dim
542
+ self.query_dim = config.compressed_query_dim or self.head_dim
543
+
544
+ if mode == "csa":
545
+ self.compression = config.csa_compression
546
+ self.top_k = config.csa_top_k
547
+ self.sparse = True
548
+ overlap = True
549
+ elif mode == "hca":
550
+ self.compression = config.hca_compression
551
+ self.top_k = 0
552
+ self.sparse = False
553
+ overlap = False
554
+ else:
555
+ raise ValueError(f"Unknown compressed attention mode {mode!r}")
556
+
557
+ self.q_down = nn.Linear(config.d_model, self.query_dim, bias=False)
558
+ self.q_up = nn.Linear(self.query_dim, config.num_heads * self.head_dim, bias=False)
559
+ self.q_norm = RMSNorm(self.head_dim, eps=config.norm_eps)
560
+ self.kv_norm = RMSNorm(self.head_dim, eps=config.norm_eps)
561
+ self.compressor = TokenCompressor(
562
+ config.d_model, self.head_dim, self.compression, overlap=overlap,
563
+ )
564
+
565
+ # Partial RoPE for queries (rotated at true token positions) and pooled
566
+ # keys (rotated at a per-group representative position). Mirrors the
567
+ # baseline Attention partial-rope geometry; default rotates the full
568
+ # compressed head dim. Even-dim required by the rotate-half layout.
569
+ # EXPERIMENTAL and OFF by default — see HybridConfig.compressed_rope.
570
+ self.compressed_rope = config.compressed_rope
571
+ self.indexer_rope = config.indexer_rope
572
+ if self.compressed_rope:
573
+ rope_dim = config.partial_rope_dim
574
+ if rope_dim is None or rope_dim > self.head_dim:
575
+ rope_dim = self.head_dim
576
+ rope_dim -= rope_dim % 2
577
+ self.rope_dim = rope_dim
578
+ self.rotary = RotaryEmbedding(
579
+ rope_dim, config.max_seq_len, config.rope_base,
580
+ )
581
+
582
+ if self.sparse:
583
+ self.indexer_q_down = nn.Linear(config.d_model, self.query_dim, bias=False)
584
+ self.indexer_q_up = nn.Linear(
585
+ self.query_dim,
586
+ config.csa_indexer_heads * config.csa_indexer_dim,
587
+ bias=False,
588
+ )
589
+ self.indexer_k_proj = nn.Linear(self.head_dim, config.csa_indexer_dim, bias=False)
590
+ self.indexer_w = nn.Linear(config.d_model, config.csa_indexer_heads, bias=False)
591
+ self.indexer_heads = config.csa_indexer_heads
592
+ self.indexer_dim = config.csa_indexer_dim
593
+ self.indexer_loss_weight = config.csa_indexer_loss_weight
594
+ if self.indexer_rope:
595
+ # Cap the indexer rope sub-dim by partial_rope_dim (when set) and
596
+ # the indexer head dim; independent of compressed_rope so either
597
+ # flag works alone.
598
+ idx_rope_dim = self.indexer_dim
599
+ if config.partial_rope_dim is not None:
600
+ idx_rope_dim = min(idx_rope_dim, config.partial_rope_dim)
601
+ idx_rope_dim -= idx_rope_dim % 2
602
+ self.indexer_rope_dim = idx_rope_dim
603
+ if idx_rope_dim > 0:
604
+ self.indexer_rotary = RotaryEmbedding(
605
+ idx_rope_dim, config.max_seq_len, config.rope_base,
606
+ )
607
+
608
+ self.out_proj = nn.Linear(config.num_heads * self.head_dim, config.d_model, bias=False)
609
+ self.attention_sink = config.attention_sink
610
+ if self.attention_sink:
611
+ self.sink_logit = nn.Parameter(torch.zeros(config.num_heads))
612
+
613
+ def _group_positions(self, G: int, T: int, device: torch.device) -> torch.Tensor:
614
+ """Representative absolute position for each compressed group.
615
+
616
+ Group ``g`` aggregates raw tokens spanning the a-window ``[g*c, g*c+c)``
617
+ (and, when overlapping, the b-window to its left); its representative is
618
+ the a-window's LAST token ``g*c+c-1``, clamped to the final real token
619
+ ``T-1`` for the trailing partial group. This matches the causal boundary
620
+ used in scoring, so a query at ``t`` sees only groups whose rep position
621
+ ``<= t``. ``torch.arange`` keeps this torch.compile/XLA-safe.
622
+ """
623
+ g_idx = torch.arange(G, device=device, dtype=torch.long)
624
+ pos = (g_idx + 1) * self.compression - 1
625
+ return pos.clamp_max(T - 1)
626
+
627
+ def _rope_partial(
628
+ self, x: torch.Tensor, positions: torch.Tensor, rotary: nn.Module, rope_dim: int
629
+ ) -> torch.Tensor:
630
+ """Partial RoPE on the last ``rope_dim`` channels of ``x`` at ``positions``."""
631
+ if rope_dim >= x.shape[-1]:
632
+ return rotary.forward_at_positions(x, positions)
633
+ no_rope = x[..., :-rope_dim]
634
+ rope = rotary.forward_at_positions(x[..., -rope_dim:], positions)
635
+ return torch.cat([no_rope, rope], dim=-1)
636
+
637
+ def _build_scores(
638
+ self,
639
+ q: torch.Tensor,
640
+ kv: torch.Tensor,
641
+ kv_index: torch.Tensor,
642
+ group_valid: torch.Tensor,
643
+ attention_mask: Optional[torch.Tensor],
644
+ is_causal: bool,
645
+ hidden: torch.Tensor,
646
+ positions: torch.Tensor,
647
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
648
+ scores = torch.einsum("bhtd,bnd->bhtn", q, kv) / (self.head_dim ** 0.5)
649
+ B, H, T, G = scores.shape
650
+ index_loss = torch.zeros((), device=scores.device, dtype=scores.dtype)
651
+ if is_causal:
652
+ t_idx = torch.arange(T, device=hidden.device, dtype=torch.long)
653
+ g_idx = torch.arange(G, device=hidden.device, dtype=torch.long)
654
+ complete_groups = torch.div(
655
+ t_idx + 1, self.compression, rounding_mode="floor",
656
+ )
657
+ causal = g_idx.view(1, 1, 1, G) < complete_groups.view(1, 1, T, 1)
658
+ if T % self.compression:
659
+ # Match the old clamped end-position behavior for the final
660
+ # partial compression group without forming compression*g+c.
661
+ causal = causal | (
662
+ (t_idx.view(1, 1, T, 1) == (T - 1))
663
+ & (g_idx.view(1, 1, 1, G) == (G - 1))
664
+ )
665
+ scores = scores.masked_fill(~causal, float("-inf"))
666
+ scores = scores.masked_fill(~group_valid.view(B, 1, 1, G), float("-inf"))
667
+ if attention_mask is not None:
668
+ scores = scores.masked_fill(~attention_mask.view(B, 1, T, 1).bool(), float("-inf"))
669
+
670
+ if self.sparse:
671
+ # Per-query validity over groups (all heads masked => no valid key).
672
+ # The top-k mask below is non-differentiable, so the indexer would
673
+ # otherwise receive zero gradient. Feed it DETACHED trunk inputs so
674
+ # its KL loss trains only the indexer params (DSA-style isolation);
675
+ # this also makes the mask itself identical to the attached version.
676
+ invalid = torch.isinf(scores).all(dim=1) # [B, T, G]
677
+ q_i = self.indexer_q_up(self.indexer_q_down(hidden.detach()))
678
+ q_i = q_i.view(B, T, self.indexer_heads, self.indexer_dim)
679
+ k_i = self.indexer_k_proj(kv_index.detach())
680
+ if self.indexer_rope and self.indexer_rope_dim > 0:
681
+ # Same positional geometry as the compressed attention so the
682
+ # indexer ranks blocks the way the dense attention would: rotate
683
+ # indexer-q at per-token positions, indexer-k at group positions.
684
+ t_pos = torch.arange(T, device=q_i.device, dtype=torch.long)
685
+ q_i = self._rope_partial(
686
+ q_i.transpose(1, 2), t_pos, self.indexer_rotary, self.indexer_rope_dim,
687
+ ).transpose(1, 2)
688
+ k_i = self._rope_partial(
689
+ k_i, positions, self.indexer_rotary, self.indexer_rope_dim,
690
+ )
691
+ idx_scores = torch.einsum("bthd,bnd->bthn", q_i, k_i)
692
+ idx_scores = F.relu(idx_scores)
693
+ idx_weights = self.indexer_w(hidden.detach()).transpose(1, 2).unsqueeze(-1)
694
+ idx_scores = (idx_scores.transpose(1, 2) * idx_weights).sum(dim=1)
695
+ idx_scores = idx_scores.masked_fill(invalid, float("-inf"))
696
+
697
+ if self.training:
698
+ index_loss = self._indexer_kl_loss(scores, idx_scores, invalid)
699
+
700
+ k_sel = min(self.top_k, G)
701
+ if k_sel < G:
702
+ _, top_idx = idx_scores.topk(k_sel, dim=-1)
703
+ keep = torch.zeros_like(idx_scores, dtype=torch.bool)
704
+ keep.scatter_(-1, top_idx, True)
705
+ scores = scores.masked_fill(~keep.unsqueeze(1), float("-inf"))
706
+ return scores, index_loss
707
+
708
+ def _indexer_kl_loss(
709
+ self,
710
+ scores: torch.Tensor,
711
+ idx_scores: torch.Tensor,
712
+ invalid: torch.Tensor,
713
+ ) -> torch.Tensor:
714
+ """KL(teacher || indexer) aligning the indexer to the dense attention.
715
+
716
+ Teacher: per-head softmax over groups of the dense (pre-top-k) scores,
717
+ summed across heads then L1-renormalized over groups, in fp32 and
718
+ DETACHED. When ``attention_sink`` is on, the dense forward softmaxes the
719
+ group scores together with a learned per-head sink logit column and uses
720
+ only the group slice of those weights, so the teacher is built from that
721
+ SAME sink-augmented softmax: concat the sink column, softmax over
722
+ [groups, sink], then drop the sink column. Crucially the surviving
723
+ per-head group mass is kept (NOT renormalized back to 1 per head) before
724
+ the head-sum, then the head-summed mass is L1-renormalized once per query
725
+ -- i.e. the teacher over groups is the conditional distribution given
726
+ 'not sink'. (Renormalizing each head to 1 first would cancel the sink
727
+ exactly and leave the teacher sink-free; summing the sink-deducted masses
728
+ instead down-weights heads that route mass into the sink, matching the
729
+ attention the indexer must imitate.) Student: log-softmax of the
730
+ head-combined indexer scores over groups. Averaged over query rows that
731
+ have at least one valid group; the zero-valid case yields 0 via a clamped
732
+ denominator (no python-level branch on a tensor, so no Dynamo graph break
733
+ / device->host sync per CSA layer per step).
734
+ """
735
+ B, H, T, G = scores.shape
736
+ valid_row = (~invalid).any(dim=-1) # [B, T]
737
+ n_valid = valid_row.sum()
738
+
739
+ # Teacher distribution over groups (detached). Fully-masked rows softmax
740
+ # to NaN (all -inf), or to all-sink mass when the sink column is present;
741
+ # either way the group mass is zeroed and the row excluded via valid_row.
742
+ scores_f = scores.float()
743
+ if self.attention_sink:
744
+ sink = self.sink_logit.detach().float().view(1, H, 1, 1).expand(B, -1, T, -1)
745
+ aug = torch.cat([scores_f, sink], dim=-1) # [B, H, T, G+1]
746
+ per_head = F.softmax(aug, dim=-1)[..., :G] # [B, H, T, G]
747
+ else:
748
+ per_head = F.softmax(scores_f, dim=-1) # [B, H, T, G]
749
+ per_head = torch.nan_to_num(per_head, nan=0.0)
750
+ target = per_head.sum(dim=1) # [B, T, G]
751
+ target = target / target.sum(dim=-1, keepdim=True).clamp_min(1e-9)
752
+ target = target.detach()
753
+
754
+ student_logp = F.log_softmax(
755
+ torch.where(invalid, torch.full_like(idx_scores, float("-inf")), idx_scores).float(),
756
+ dim=-1,
757
+ )
758
+ student_logp = torch.nan_to_num(student_logp, neginf=0.0)
759
+
760
+ kl = (target * (target.clamp_min(1e-9).log() - student_logp)).sum(dim=-1) # [B, T]
761
+ kl = kl * valid_row.to(kl.dtype)
762
+ # Branch-free masked mean: clamp the denominator so an all-invalid batch
763
+ # (n_valid == 0, hence kl.sum() == 0) yields 0 instead of 0/0.
764
+ loss = kl.sum() / n_valid.to(kl.dtype).clamp_min(1.0)
765
+ return loss.to(scores.dtype)
766
+
767
+ @staticmethod
768
+ def _current_attention_mask(
769
+ attention_mask: Optional[torch.Tensor],
770
+ seq_len: int,
771
+ ) -> Optional[torch.Tensor]:
772
+ if attention_mask is None:
773
+ return None
774
+ if attention_mask.size(1) == seq_len:
775
+ return attention_mask
776
+ return attention_mask[:, -seq_len:]
777
+
778
+ @staticmethod
779
+ def _slice_mask(
780
+ attention_mask: Optional[torch.Tensor],
781
+ start: int,
782
+ end: int,
783
+ ) -> Optional[torch.Tensor]:
784
+ if attention_mask is None:
785
+ return None
786
+ return attention_mask[:, start:end]
787
+
788
+ @staticmethod
789
+ def _cat_optional_masks(
790
+ left: Optional[torch.Tensor],
791
+ right: Optional[torch.Tensor],
792
+ left_len: int,
793
+ right_len: int,
794
+ device: torch.device,
795
+ batch: int,
796
+ ) -> Optional[torch.Tensor]:
797
+ if left is None and right is None:
798
+ return None
799
+ if left is None:
800
+ left = torch.ones(batch, left_len, device=device, dtype=torch.bool)
801
+ else:
802
+ left = left.to(device).bool()
803
+ if right is None:
804
+ right = torch.ones(batch, right_len, device=device, dtype=torch.bool)
805
+ else:
806
+ right = right.to(device).bool()
807
+ return torch.cat([left, right], dim=1)
808
+
809
+ def _store_prefill_cache(
810
+ self,
811
+ cache: Dict[str, Any],
812
+ hidden: torch.Tensor,
813
+ attention_mask: Optional[torch.Tensor],
814
+ kv: torch.Tensor,
815
+ group_valid: torch.Tensor,
816
+ total_len: int,
817
+ ) -> None:
818
+ complete_groups = total_len // self.compression
819
+ pending_len = total_len % self.compression
820
+ complete_len = complete_groups * self.compression
821
+ cache["kv"] = kv[:, :complete_groups].detach()
822
+ cache["group_valid"] = group_valid[:, :complete_groups].detach()
823
+ cache["positions"] = (
824
+ torch.arange(complete_groups, device=hidden.device, dtype=torch.long)
825
+ * self.compression
826
+ + (self.compression - 1)
827
+ ).detach()
828
+ cache["pending_hidden"] = hidden[:, complete_len:, :].detach()
829
+ cache["pending_mask"] = self._slice_mask(
830
+ attention_mask, complete_len, total_len,
831
+ )
832
+ if cache["pending_mask"] is not None:
833
+ cache["pending_mask"] = cache["pending_mask"].detach()
834
+
835
+ if complete_groups > 0:
836
+ prev_start = complete_len - self.compression
837
+ cache["prev_group_hidden"] = hidden[:, prev_start:complete_len, :].detach()
838
+ prev_mask = self._slice_mask(attention_mask, prev_start, complete_len)
839
+ cache["prev_group_mask"] = prev_mask.detach() if prev_mask is not None else None
840
+ else:
841
+ cache["prev_group_hidden"] = None
842
+ cache["prev_group_mask"] = None
843
+ if pending_len == 0:
844
+ cache["pending_hidden"] = hidden[:, total_len:total_len, :].detach()
845
+ cache["pending_mask"] = None
846
+ cache["seen_tokens"] = int(total_len)
847
+
848
+ def _compress_pending_group(
849
+ self,
850
+ cache: Dict[str, Any],
851
+ pending_hidden: torch.Tensor,
852
+ pending_mask: Optional[torch.Tensor],
853
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
854
+ batch = pending_hidden.size(0)
855
+ prev_hidden = cache.get("prev_group_hidden") if self.sparse else None
856
+ if prev_hidden is not None:
857
+ prev_hidden = prev_hidden.to(pending_hidden.device)
858
+ hidden = torch.cat([prev_hidden, pending_hidden], dim=1)
859
+ mask = self._cat_optional_masks(
860
+ cache.get("prev_group_mask"),
861
+ pending_mask,
862
+ prev_hidden.size(1),
863
+ pending_hidden.size(1),
864
+ pending_hidden.device,
865
+ batch,
866
+ )
867
+ group_idx = 1
868
+ else:
869
+ hidden = pending_hidden
870
+ mask = pending_mask.bool() if pending_mask is not None else None
871
+ group_idx = 0
872
+ kv, group_valid = self.compressor(hidden, mask)
873
+ kv = self.kv_norm(kv)
874
+ return kv[:, group_idx:group_idx + 1], group_valid[:, group_idx:group_idx + 1]
875
+
876
+ def _build_cached_scores(
877
+ self,
878
+ q: torch.Tensor,
879
+ kv: torch.Tensor,
880
+ group_valid: torch.Tensor,
881
+ hidden: torch.Tensor,
882
+ q_positions: torch.Tensor,
883
+ group_positions: torch.Tensor,
884
+ is_causal: bool,
885
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
886
+ scores = torch.einsum("bhtd,bnd->bhtn", q, kv) / (self.head_dim ** 0.5)
887
+ B, H, T, G = scores.shape
888
+ index_loss = torch.zeros((), device=scores.device, dtype=scores.dtype)
889
+ if is_causal:
890
+ causal = group_positions.view(1, 1, 1, G) <= q_positions.view(1, 1, T, 1)
891
+ scores = scores.masked_fill(~causal, float("-inf"))
892
+ scores = scores.masked_fill(~group_valid.view(B, 1, 1, G), float("-inf"))
893
+
894
+ if self.sparse:
895
+ invalid = torch.isinf(scores).all(dim=1)
896
+ q_i = self.indexer_q_up(self.indexer_q_down(hidden.detach()))
897
+ q_i = q_i.view(B, T, self.indexer_heads, self.indexer_dim)
898
+ k_i = self.indexer_k_proj(kv.detach())
899
+ if self.indexer_rope and self.indexer_rope_dim > 0:
900
+ q_i = self._rope_partial(
901
+ q_i.transpose(1, 2), q_positions, self.indexer_rotary, self.indexer_rope_dim,
902
+ ).transpose(1, 2)
903
+ k_i = self._rope_partial(
904
+ k_i, group_positions, self.indexer_rotary, self.indexer_rope_dim,
905
+ )
906
+ idx_scores = torch.einsum("bthd,bnd->bthn", q_i, k_i)
907
+ idx_scores = F.relu(idx_scores)
908
+ idx_weights = self.indexer_w(hidden.detach()).transpose(1, 2).unsqueeze(-1)
909
+ idx_scores = (idx_scores.transpose(1, 2) * idx_weights).sum(dim=1)
910
+ idx_scores = idx_scores.masked_fill(invalid, float("-inf"))
911
+
912
+ if self.training:
913
+ index_loss = self._indexer_kl_loss(scores, idx_scores, invalid)
914
+
915
+ k_sel = min(self.top_k, G)
916
+ if k_sel < G:
917
+ _, top_idx = idx_scores.topk(k_sel, dim=-1)
918
+ keep = torch.zeros_like(idx_scores, dtype=torch.bool)
919
+ keep.scatter_(-1, top_idx, True)
920
+ scores = scores.masked_fill(~keep.unsqueeze(1), float("-inf"))
921
+ return scores, index_loss
922
+
923
+ def _forward_cached(
924
+ self,
925
+ hidden: torch.Tensor,
926
+ attention_mask: Optional[torch.Tensor],
927
+ is_causal: bool,
928
+ cache: Dict[str, Any],
929
+ position_offset: int,
930
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
931
+ B, T, _ = hidden.shape
932
+ if T != 1 and int(cache.get("seen_tokens", 0) or 0) > 0:
933
+ outs: List[torch.Tensor] = []
934
+ index_loss = torch.zeros((), device=hidden.device, dtype=hidden.dtype)
935
+ for i in range(T):
936
+ mask_i = None if attention_mask is None else attention_mask[:, i:i + 1]
937
+ out_i, loss_i = self._forward_cached(
938
+ hidden[:, i:i + 1, :],
939
+ mask_i,
940
+ is_causal,
941
+ cache,
942
+ position_offset + i,
943
+ )
944
+ outs.append(out_i)
945
+ index_loss = index_loss + loss_i
946
+ return torch.cat(outs, dim=1), index_loss
947
+
948
+ if int(cache.get("seen_tokens", 0) or 0) == 0:
949
+ out, index_loss, kv, group_valid = self._forward_full(
950
+ hidden,
951
+ attention_mask=attention_mask,
952
+ is_causal=is_causal,
953
+ position_offset=position_offset,
954
+ return_kv=True,
955
+ )
956
+ self._store_prefill_cache(
957
+ cache,
958
+ hidden,
959
+ attention_mask,
960
+ kv,
961
+ group_valid,
962
+ position_offset + T,
963
+ )
964
+ return out, index_loss
965
+
966
+ current_mask = self._current_attention_mask(attention_mask, T)
967
+ pending_hidden = cache.get("pending_hidden")
968
+ if pending_hidden is None:
969
+ pending_hidden = hidden[:, :0, :]
970
+ else:
971
+ pending_hidden = pending_hidden.to(hidden.device)
972
+ pending_mask = cache.get("pending_mask")
973
+ pending_hidden = torch.cat([pending_hidden, hidden], dim=1)
974
+ pending_mask = self._cat_optional_masks(
975
+ pending_mask,
976
+ current_mask,
977
+ pending_hidden.size(1) - T,
978
+ T,
979
+ hidden.device,
980
+ B,
981
+ )
982
+
983
+ cur_kv, cur_valid = self._compress_pending_group(cache, pending_hidden, pending_mask)
984
+ base_kv = cache.get("kv")
985
+ base_valid = cache.get("group_valid")
986
+ base_positions = cache.get("positions")
987
+ if base_kv is not None and base_kv.size(1) > 0:
988
+ kv = torch.cat([base_kv.to(hidden.device), cur_kv], dim=1)
989
+ group_valid = torch.cat([base_valid.to(hidden.device), cur_valid], dim=1)
990
+ cur_pos = torch.tensor([position_offset + T - 1], device=hidden.device, dtype=torch.long)
991
+ positions = torch.cat([base_positions.to(hidden.device), cur_pos], dim=0)
992
+ else:
993
+ kv = cur_kv
994
+ group_valid = cur_valid
995
+ positions = torch.tensor([position_offset + T - 1], device=hidden.device, dtype=torch.long)
996
+
997
+ q = self.q_up(self.q_down(hidden)).view(B, T, self.num_heads, self.head_dim)
998
+ q = self.q_norm(q).transpose(1, 2)
999
+ q_positions = torch.arange(
1000
+ position_offset,
1001
+ position_offset + T,
1002
+ device=hidden.device,
1003
+ dtype=torch.long,
1004
+ )
1005
+ if self.compressed_rope:
1006
+ q_score = self._rope_partial(q, q_positions, self.rotary, self.rope_dim)
1007
+ kv_score = self._rope_partial(kv, positions, self.rotary, self.rope_dim)
1008
+ else:
1009
+ q_score = q
1010
+ kv_score = kv
1011
+
1012
+ scores, index_loss = self._build_cached_scores(
1013
+ q_score,
1014
+ kv_score,
1015
+ group_valid,
1016
+ hidden,
1017
+ q_positions,
1018
+ positions,
1019
+ is_causal,
1020
+ )
1021
+ all_masked = torch.isinf(scores).all(dim=-1, keepdim=True)
1022
+ if self.attention_sink:
1023
+ sink = self.sink_logit.float().view(1, self.num_heads, 1, 1).expand(B, -1, T, -1)
1024
+ aug = torch.cat([scores.float(), sink], dim=-1)
1025
+ weights = F.softmax(aug, dim=-1)[..., :scores.size(-1)].to(kv.dtype)
1026
+ else:
1027
+ safe_scores = torch.where(all_masked, torch.zeros_like(scores), scores)
1028
+ weights = F.softmax(safe_scores.float(), dim=-1).to(kv.dtype)
1029
+ weights = torch.where(all_masked, torch.zeros_like(weights), weights)
1030
+ out = torch.einsum("bhtn,bnd->bhtd", weights, kv)
1031
+ if current_mask is not None:
1032
+ out = out * current_mask.view(B, 1, T, 1).to(out.dtype)
1033
+ out = out.transpose(1, 2).reshape(B, T, self.num_heads * self.head_dim)
1034
+ out = self.out_proj(out)
1035
+
1036
+ new_seen = int(position_offset + T)
1037
+ if pending_hidden.size(1) >= self.compression:
1038
+ new_kv = cur_kv.detach()
1039
+ new_valid = cur_valid.detach()
1040
+ new_pos = torch.tensor([new_seen - 1], device=hidden.device, dtype=torch.long)
1041
+ if base_kv is not None and base_kv.size(1) > 0:
1042
+ cache["kv"] = torch.cat([base_kv.to(hidden.device), new_kv], dim=1).detach()
1043
+ cache["group_valid"] = torch.cat([
1044
+ base_valid.to(hidden.device), new_valid,
1045
+ ], dim=1).detach()
1046
+ cache["positions"] = torch.cat([
1047
+ base_positions.to(hidden.device), new_pos,
1048
+ ], dim=0).detach()
1049
+ else:
1050
+ cache["kv"] = new_kv
1051
+ cache["group_valid"] = new_valid
1052
+ cache["positions"] = new_pos.detach()
1053
+ cache["prev_group_hidden"] = pending_hidden[:, -self.compression:, :].detach()
1054
+ if pending_mask is not None:
1055
+ cache["prev_group_mask"] = pending_mask[:, -self.compression:].detach()
1056
+ else:
1057
+ cache["prev_group_mask"] = None
1058
+ cache["pending_hidden"] = hidden[:, :0, :].detach()
1059
+ cache["pending_mask"] = None
1060
+ else:
1061
+ cache["pending_hidden"] = pending_hidden.detach()
1062
+ cache["pending_mask"] = pending_mask.detach() if pending_mask is not None else None
1063
+ cache["seen_tokens"] = new_seen
1064
+ return out, index_loss
1065
+
1066
+ def forward(
1067
+ self,
1068
+ hidden: torch.Tensor,
1069
+ attention_mask: Optional[torch.Tensor] = None,
1070
+ is_causal: bool = True,
1071
+ cache: Optional[Dict[str, Any]] = None,
1072
+ position_offset: int = 0,
1073
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1074
+ if cache is not None:
1075
+ return self._forward_cached(
1076
+ hidden,
1077
+ attention_mask=attention_mask,
1078
+ is_causal=is_causal,
1079
+ cache=cache,
1080
+ position_offset=position_offset,
1081
+ )
1082
+
1083
+ out, index_loss, _, _ = self._forward_full(
1084
+ hidden,
1085
+ attention_mask=attention_mask,
1086
+ is_causal=is_causal,
1087
+ position_offset=position_offset,
1088
+ return_kv=False,
1089
+ )
1090
+ return out, index_loss
1091
+
1092
+ def _forward_full(
1093
+ self,
1094
+ hidden: torch.Tensor,
1095
+ attention_mask: Optional[torch.Tensor] = None,
1096
+ is_causal: bool = True,
1097
+ position_offset: int = 0,
1098
+ return_kv: bool = False,
1099
+ ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
1100
+ B, T, _ = hidden.shape
1101
+ kv, group_valid = self.compressor(hidden, attention_mask)
1102
+ kv = self.kv_norm(kv)
1103
+ q = self.q_up(self.q_down(hidden)).view(B, T, self.num_heads, self.head_dim)
1104
+ q = self.q_norm(q).transpose(1, 2)
1105
+
1106
+ # RoPE (after q_norm/kv_norm, matching baseline order): queries rotate at
1107
+ # their true token position; pooled keys rotate at a per-group
1108
+ # representative position. The rotated kv is used ONLY for the score
1109
+ # dot-product; the value aggregation below keeps the un-rotated kv.
1110
+ positions = self._group_positions(kv.size(1), T, hidden.device)
1111
+ if position_offset:
1112
+ positions = positions + int(position_offset)
1113
+ if self.compressed_rope:
1114
+ t_pos = torch.arange(
1115
+ position_offset,
1116
+ position_offset + T,
1117
+ device=q.device,
1118
+ dtype=torch.long,
1119
+ )
1120
+ q_score = self._rope_partial(q, t_pos, self.rotary, self.rope_dim)
1121
+ kv_score = self._rope_partial(kv, positions, self.rotary, self.rope_dim)
1122
+ else:
1123
+ q_score = q
1124
+ kv_score = kv
1125
+
1126
+ scores, index_loss = self._build_scores(
1127
+ q_score, kv_score, kv, group_valid, attention_mask, is_causal, hidden,
1128
+ positions,
1129
+ )
1130
+ all_masked = torch.isinf(scores).all(dim=-1, keepdim=True)
1131
+ if self.attention_sink:
1132
+ sink = self.sink_logit.float().view(1, self.num_heads, 1, 1).expand(B, -1, T, -1)
1133
+ aug = torch.cat([scores.float(), sink], dim=-1)
1134
+ weights = F.softmax(aug, dim=-1)[..., :scores.size(-1)].to(kv.dtype)
1135
+ else:
1136
+ safe_scores = torch.where(all_masked, torch.zeros_like(scores), scores)
1137
+ weights = F.softmax(safe_scores.float(), dim=-1).to(kv.dtype)
1138
+ weights = torch.where(all_masked, torch.zeros_like(weights), weights)
1139
+ out = torch.einsum("bhtn,bnd->bhtd", weights, kv)
1140
+ if attention_mask is not None:
1141
+ out = out * attention_mask.view(B, 1, T, 1).to(out.dtype)
1142
+ out = out.transpose(1, 2).reshape(B, T, self.num_heads * self.head_dim)
1143
+ out = self.out_proj(out)
1144
+ # Return the raw (unweighted) indexer KL so the model can both surface
1145
+ # it for logging and weight it once before adding to the train loss.
1146
+ return out, index_loss, (kv if return_kv else None), (group_valid if return_kv else None)
1147
+
1148
+
1149
+ class HybridAttentionLayer(nn.Module):
1150
+ """Owns the requested attention variants and selects one per call."""
1151
+
1152
+ def __init__(self, config: HybridConfig, kinds: List[str]):
1153
+ super().__init__()
1154
+ unique = sorted(set(normalize_attention_type(k) for k in kinds))
1155
+ self.layers = nn.ModuleDict()
1156
+ for kind in unique:
1157
+ if kind == "kda":
1158
+ self.layers[kind] = KimiDeltaAttention(config)
1159
+ elif kind == "swa":
1160
+ self.layers[kind] = LocalAttention(config)
1161
+ elif kind in ("csa", "hca"):
1162
+ self.layers[kind] = CompressedGlobalAttention(config, mode=kind)
1163
+
1164
+ def forward(
1165
+ self,
1166
+ kind: str,
1167
+ x: torch.Tensor,
1168
+ attention_mask: Optional[torch.Tensor] = None,
1169
+ is_causal: bool = True,
1170
+ cache: Optional[Dict[str, Any]] = None,
1171
+ position_offset: int = 0,
1172
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1173
+ kind = normalize_attention_type(kind)
1174
+ layer = self.layers[kind]
1175
+ zero = torch.zeros((), device=x.device, dtype=x.dtype)
1176
+ if kind == "kda":
1177
+ return layer(x, attention_mask=attention_mask, cache=cache), zero
1178
+ if kind in ("csa", "hca"):
1179
+ return layer(
1180
+ x,
1181
+ attention_mask=attention_mask,
1182
+ is_causal=is_causal,
1183
+ cache=cache,
1184
+ position_offset=position_offset,
1185
+ )
1186
+ return layer(
1187
+ x,
1188
+ attention_mask=attention_mask,
1189
+ is_causal=is_causal,
1190
+ cache=cache,
1191
+ position_offset=position_offset,
1192
+ ), zero
1193
+
1194
+
1195
+ class HybridTransformerBlock(nn.Module):
1196
+ def __init__(self, config: HybridConfig, attention_kinds: List[str]):
1197
+ super().__init__()
1198
+ self.use_moe = config.use_moe
1199
+ self.attention_kinds = [normalize_attention_type(k) for k in attention_kinds]
1200
+ self.attn_norm = RMSNorm(config.d_model, eps=config.norm_eps)
1201
+ self.attn = HybridAttentionLayer(config, self.attention_kinds)
1202
+
1203
+ self.ffn_norm = RMSNorm(config.d_model, eps=config.norm_eps)
1204
+ if config.use_moe:
1205
+ self.ffn = MoELayer(config)
1206
+ else:
1207
+ self.ffn = SwiGLU(config.d_model, config.d_ff)
1208
+
1209
+ def forward(
1210
+ self,
1211
+ x: torch.Tensor,
1212
+ attention_kind: str,
1213
+ attention_mask: Optional[torch.Tensor] = None,
1214
+ is_causal: bool = True,
1215
+ cache: Optional[Dict[str, Any]] = None,
1216
+ ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor], torch.Tensor]:
1217
+ attn_out, index_loss = self.attn(
1218
+ attention_kind,
1219
+ self.attn_norm(x),
1220
+ attention_mask=attention_mask,
1221
+ is_causal=is_causal,
1222
+ cache=cache,
1223
+ )
1224
+ x = x + attn_out
1225
+
1226
+ if self.use_moe:
1227
+ ffn_out, aux_loss, topk_indices = self.ffn(self.ffn_norm(x))
1228
+ x = x + ffn_out
1229
+ return x, aux_loss, topk_indices, index_loss
1230
+ x = x + self.ffn(self.ffn_norm(x))
1231
+ zero = torch.zeros((), device=x.device, dtype=x.dtype)
1232
+ return x, zero, None, index_loss
1233
+
1234
+
1235
+ class HybridTransformer(nn.Module):
1236
+ def __init__(self, config: HybridConfig):
1237
+ super().__init__()
1238
+ self.config = config
1239
+
1240
+ self.token_emb = nn.Embedding(config.vocab_size, config.d_model)
1241
+
1242
+ if config.attn_pattern:
1243
+ self.attn_schedule = expand_attention_pattern(
1244
+ config.attn_pattern, config.num_layers, default="kda",
1245
+ )
1246
+ else:
1247
+ self.attn_schedule = default_hybrid_attention_pattern(
1248
+ config, config.num_layers,
1249
+ )
1250
+
1251
+ self.layers = nn.ModuleList([
1252
+ HybridTransformerBlock(config, [self.attn_schedule[i]])
1253
+ for i in range(config.num_layers)
1254
+ ])
1255
+
1256
+ self.final_norm = RMSNorm(config.d_model, eps=config.norm_eps)
1257
+ self.lm_head = nn.Linear(config.d_model, config.vocab_size, bias=False)
1258
+ self.lm_head.weight = self.token_emb.weight
1259
+
1260
+ self._init_weights()
1261
+
1262
+ def _init_weights(self):
1263
+ for module in self.modules():
1264
+ if isinstance(module, nn.Linear):
1265
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
1266
+ if module.bias is not None:
1267
+ torch.nn.init.zeros_(module.bias)
1268
+ elif isinstance(module, nn.Embedding):
1269
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
1270
+ init_moe_router_weights(self, self.config.router_init_std)
1271
+
1272
+ def forward(
1273
+ self,
1274
+ input_ids: torch.Tensor,
1275
+ attention_mask: Optional[torch.Tensor] = None,
1276
+ labels: Optional[torch.Tensor] = None,
1277
+ is_causal: bool = True,
1278
+ caches: Optional[List[Dict[str, Any]]] = None,
1279
+ token_superposition_bag_size: int = 1,
1280
+ ) -> Dict[str, Any]:
1281
+ x = token_superposition_embeddings(
1282
+ self.token_emb, input_ids, token_superposition_bag_size,
1283
+ )
1284
+ attention_mask = token_superposition_attention_mask(
1285
+ attention_mask, token_superposition_bag_size,
1286
+ )
1287
+
1288
+ aux_loss = torch.zeros((), device=input_ids.device, dtype=x.dtype)
1289
+ index_loss = torch.zeros((), device=input_ids.device, dtype=x.dtype)
1290
+ topk_indices_list: List[Optional[torch.Tensor]] = []
1291
+ for i, layer in enumerate(self.layers):
1292
+ kind = self.attn_schedule[i]
1293
+ layer_cache = caches[i] if (caches is not None and kind == "kda") else None
1294
+ x, layer_aux, layer_topk, layer_index = layer(
1295
+ x,
1296
+ attention_kind=kind,
1297
+ attention_mask=attention_mask,
1298
+ is_causal=is_causal,
1299
+ cache=layer_cache,
1300
+ )
1301
+ aux_loss = aux_loss + layer_aux
1302
+ index_loss = index_loss + layer_index
1303
+ topk_indices_list.append(layer_topk)
1304
+
1305
+ x = self.final_norm(x)
1306
+ logits = self.lm_head(x)
1307
+
1308
+ lm_loss: Optional[torch.Tensor] = None
1309
+ if labels is not None:
1310
+ lm_loss = lm_cross_entropy_from_logits(
1311
+ logits,
1312
+ labels,
1313
+ token_superposition_bag_size=token_superposition_bag_size,
1314
+ ignore_index=-100,
1315
+ )
1316
+ loss = combine_lm_and_aux_loss(
1317
+ lm_loss,
1318
+ aux_loss if self.config.use_moe else None,
1319
+ self.training,
1320
+ )
1321
+ if loss is not None and self.training:
1322
+ loss = loss + self.config.csa_indexer_loss_weight * index_loss
1323
+
1324
+ return {
1325
+ "logits": logits,
1326
+ "loss": loss,
1327
+ "lm_loss": lm_loss,
1328
+ "aux_loss": aux_loss if self.config.use_moe else None,
1329
+ "indexer_loss": index_loss,
1330
+ "topk_indices": topk_indices_list if self.config.use_moe else None,
1331
+ }
1332
+
1333
+ def update_router_biases(self, topk_indices_list: List[Optional[torch.Tensor]]) -> None:
1334
+ if not self.config.use_moe:
1335
+ return
1336
+ for layer, topk_indices in zip(self.layers, topk_indices_list):
1337
+ if topk_indices is not None and isinstance(layer.ffn, MoELayer):
1338
+ layer.ffn.update_bias(topk_indices)
1339
+
1340
+ @torch.no_grad()
1341
+ def get_balance_stats(self) -> Dict[str, float]:
1342
+ if not self.config.use_moe:
1343
+ return {}
1344
+ stats: Dict[str, float] = {}
1345
+ for idx, layer in enumerate(self.layers):
1346
+ if hasattr(layer.ffn, "bias"):
1347
+ bias = layer.ffn.bias
1348
+ kind = self.attn_schedule[idx]
1349
+ stats[f"layer{idx}_{kind}_bias_mean"] = bias.abs().mean().item()
1350
+ stats[f"layer{idx}_{kind}_bias_max"] = bias.abs().max().item()
1351
+ return stats
1352
+
1353
+ @torch.no_grad()
1354
+ def generate(
1355
+ self,
1356
+ input_ids: torch.Tensor,
1357
+ max_new_tokens: int = 100,
1358
+ temperature: float = 1.0,
1359
+ top_k: Optional[int] = None,
1360
+ attention_mask: Optional[torch.Tensor] = None,
1361
+ eos_token_id: Optional[int] = None,
1362
+ ) -> torch.Tensor:
1363
+ self.train(False)
1364
+ batch_size = input_ids.size(0)
1365
+
1366
+ for _ in range(max_new_tokens):
1367
+ outputs = self.forward(
1368
+ input_ids, attention_mask=attention_mask, is_causal=True,
1369
+ )
1370
+ logits = outputs["logits"][:, -1, :] / temperature
1371
+
1372
+ if top_k is not None:
1373
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
1374
+ logits = logits.masked_fill(logits < v[:, [-1]], float("-inf"))
1375
+
1376
+ probs = F.softmax(logits, dim=-1)
1377
+ next_token = torch.multinomial(probs, num_samples=1)
1378
+ input_ids = torch.cat([input_ids, next_token], dim=-1)
1379
+
1380
+ if attention_mask is not None:
1381
+ attention_mask = torch.cat([
1382
+ attention_mask,
1383
+ torch.ones(
1384
+ (batch_size, 1),
1385
+ device=attention_mask.device,
1386
+ dtype=attention_mask.dtype,
1387
+ ),
1388
+ ], dim=-1)
1389
+
1390
+ if eos_token_id is not None and (next_token == eos_token_id).all():
1391
+ break
1392
+
1393
+ return input_ids
1394
+
1395
+
1396
+ __all__ = [
1397
+ "HybridConfig",
1398
+ "LocalAttention",
1399
+ "TokenCompressor",
1400
+ "CompressedGlobalAttention",
1401
+ "HybridAttentionLayer",
1402
+ "HybridTransformerBlock",
1403
+ "HybridTransformer",
1404
+ "normalize_attention_type",
1405
+ "parse_attention_pattern",
1406
+ "expand_attention_pattern",
1407
+ "count_parameters",
1408
+ "model_summary",
1409
+ ]