tungns2408 commited on
Commit
05b48c6
·
verified ·
1 Parent(s): a833dc1

Upload folder using huggingface_hub

Browse files
modeling/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .embedding_model import DeepXEmbeddingModel
2
+ from .gdn2_attention import GatedDeltaNet2Attention
3
+ from .pipeline import DeepXPipeline
4
+
5
+ __all__ = ["DeepXEmbeddingModel", "GatedDeltaNet2Attention", "DeepXPipeline"]
modeling/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (340 Bytes). View file
 
modeling/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (413 Bytes). View file
 
modeling/__pycache__/embedding_model.cpython-310.pyc ADDED
Binary file (6.11 kB). View file
 
modeling/__pycache__/embedding_model.cpython-311.pyc ADDED
Binary file (11.2 kB). View file
 
modeling/__pycache__/gdn2_attention.cpython-310.pyc ADDED
Binary file (10.2 kB). View file
 
modeling/__pycache__/gdn2_attention.cpython-311.pyc ADDED
Binary file (17.5 kB). View file
 
modeling/__pycache__/gla_attention.cpython-311.pyc ADDED
Binary file (17.7 kB). View file
 
modeling/__pycache__/hybrid_layer.cpython-310.pyc ADDED
Binary file (3.32 kB). View file
 
modeling/__pycache__/hybrid_layer.cpython-311.pyc ADDED
Binary file (5.16 kB). View file
 
modeling/__pycache__/hyperloop.cpython-310.pyc ADDED
Binary file (4.42 kB). View file
 
modeling/__pycache__/hyperloop.cpython-311.pyc ADDED
Binary file (7.7 kB). View file
 
modeling/__pycache__/mamba2_block.cpython-311.pyc ADDED
Binary file (8.94 kB). View file
 
modeling/__pycache__/pipeline.cpython-310.pyc ADDED
Binary file (6.42 kB). View file
 
modeling/__pycache__/pipeline.cpython-311.pyc ADDED
Binary file (10.7 kB). View file
 
modeling/__pycache__/utils.cpython-310.pyc ADDED
Binary file (7.95 kB). View file
 
modeling/__pycache__/utils.cpython-311.pyc ADDED
Binary file (14.5 kB). View file
 
modeling/embedding_model.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepX v0.7: Gated DeltaNet-2 Hyperloop Backbone + ColBERT Head.
3
+
4
+ Architecture:
5
+ Begin(4 NarrowA) → Phase1×2 [WideA + NarrowA×4] → Phase2×4 [NarrowB×4 + WideB]
6
+ → End(1 WideB) = 35 compute passes
7
+
8
+ Unique layers: 9 (4 begin + 4 shared cores + 1 end)
9
+ Per-loop differentiation: LoRA + RoDE
10
+
11
+ Outputs:
12
+ 1. Single vector (1536-d) via attention pooling
13
+ 2. Token vectors (T × 128-d) via ColBERT head
14
+
15
+ Weight Init: ~95% from Gemma 4 E2B via direct copy + SVD LoRA.
16
+ """
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+ import logging
22
+ from typing import Optional, Tuple, Union
23
+
24
+ from config import DeepXConfig
25
+ from .hybrid_layer import (
26
+ DeepXLayer, make_narrow_a_layer, make_narrow_b_layer,
27
+ make_wide_a_layer, make_wide_b_layer,
28
+ )
29
+ from .hyperloop import HyperloopPhase
30
+ from .utils import RMSNorm, RoDE
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class ColBERTHead(nn.Module):
36
+ """Projects hidden states to low-dimensional token vectors for MaxSim."""
37
+
38
+ def __init__(self, hidden_size: int, colbert_dim: int = 128):
39
+ super().__init__()
40
+ self.linear = nn.Linear(hidden_size, colbert_dim, bias=False)
41
+
42
+ def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
43
+ token_embeds = self.linear(hidden_states)
44
+ token_embeds = F.normalize(token_embeds, p=2, dim=-1)
45
+ if attention_mask is not None:
46
+ token_embeds = token_embeds * attention_mask.unsqueeze(-1).to(token_embeds.dtype)
47
+ return token_embeds
48
+
49
+
50
+ class DeepXEmbeddingModel(nn.Module):
51
+ """
52
+ DeepX v0.7 Backbone — Gated DeltaNet-2 Hyperloop + ColBERT.
53
+
54
+ Receives hidden_states from external frozen token embedding.
55
+ """
56
+
57
+ def __init__(self, config: DeepXConfig):
58
+ super().__init__()
59
+ self.config = config
60
+
61
+ # ═══ 1. Begin Block: 4 unique NarrowA layers (direct copy from Gemma 0-3) ═══
62
+ self.begin_blocks = nn.ModuleList([
63
+ make_narrow_a_layer(config, layer_idx=i)
64
+ for i in range(config.begin_layers)
65
+ ])
66
+
67
+ # ═══ 2. Phase1 Loop: 2 iterations × [WideA + NarrowA×4] ═══
68
+ self.shared_narrow_a = make_narrow_a_layer(config, layer_idx=config.begin_layers)
69
+ self.shared_wide_a = make_wide_a_layer(config, layer_idx=config.begin_layers + 1)
70
+
71
+ # Attach RoDE to shared cores
72
+ if config.use_rode:
73
+ self.shared_narrow_a.self_attn._rode = RoDE(
74
+ dim=config.depth_rotary_dims, num_loops=config.phase1_loops
75
+ )
76
+ self.shared_wide_a.self_attn._rode = RoDE(
77
+ dim=config.depth_rotary_dims, num_loops=config.phase1_loops
78
+ )
79
+
80
+ self.phase1 = HyperloopPhase(
81
+ config=config,
82
+ shared_narrow=self.shared_narrow_a,
83
+ shared_wide=self.shared_wide_a,
84
+ num_loops=config.phase1_loops,
85
+ narrow_num_heads=config.narrow_a_heads,
86
+ narrow_kv_heads=config.narrow_a_kv_heads,
87
+ narrow_head_dim=config.narrow_a_head_dim,
88
+ narrow_intermediate=config.narrow_a_intermediate,
89
+ wide_num_heads=config.wide_a_heads,
90
+ wide_kv_heads=config.wide_a_kv_heads,
91
+ wide_head_dim=config.wide_a_head_dim,
92
+ wide_intermediate=config.wide_a_intermediate,
93
+ wide_first=True, # [WideA, NarrowA×4]
94
+ )
95
+
96
+ # ═══ 3. Phase2 Loop: 4 iterations × [NarrowB×4 + WideB] ═══
97
+ self.shared_narrow_b = make_narrow_b_layer(config, layer_idx=config.begin_layers + 2)
98
+ self.shared_wide_b = make_wide_b_layer(config, layer_idx=config.begin_layers + 3)
99
+
100
+ if config.use_rode:
101
+ self.shared_narrow_b.self_attn._rode = RoDE(
102
+ dim=config.depth_rotary_dims, num_loops=config.phase2_loops
103
+ )
104
+ self.shared_wide_b.self_attn._rode = RoDE(
105
+ dim=config.depth_rotary_dims, num_loops=config.phase2_loops
106
+ )
107
+
108
+ self.phase2 = HyperloopPhase(
109
+ config=config,
110
+ shared_narrow=self.shared_narrow_b,
111
+ shared_wide=self.shared_wide_b,
112
+ num_loops=config.phase2_loops,
113
+ narrow_num_heads=config.narrow_b_heads,
114
+ narrow_kv_heads=config.narrow_b_kv_heads,
115
+ narrow_head_dim=config.narrow_b_head_dim,
116
+ narrow_intermediate=config.narrow_b_intermediate,
117
+ wide_num_heads=config.wide_b_heads,
118
+ wide_kv_heads=config.wide_b_kv_heads,
119
+ wide_head_dim=config.wide_b_head_dim,
120
+ wide_intermediate=config.wide_b_intermediate,
121
+ wide_first=False, # [NarrowB×4, WideB]
122
+ )
123
+
124
+ # ═══ 4. End Block: 1 unique WideB layer (layer 34 direct copy) ═══
125
+ self.end_block = make_wide_b_layer(config, layer_idx=config.begin_layers + 4)
126
+
127
+ # ═══ Final norm ═══
128
+ self.norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
129
+
130
+ # ═══ Output Head 1: Attention Pooling ═══
131
+ self.pooling_strategy = config.pooling_strategy
132
+ if config.pooling_strategy == "attention":
133
+ self.pool_query = nn.Parameter(torch.randn(1, 1, config.hidden_size) * 0.02)
134
+
135
+ # ═══ Output Head 2: ColBERT ═══
136
+ self.use_colbert = config.use_colbert
137
+ if config.use_colbert:
138
+ self.colbert_head = ColBERTHead(config.hidden_size, config.colbert_dim)
139
+
140
+ self.to(config.torch_dtype)
141
+
142
+ def _pool(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
143
+ if self.pooling_strategy == "attention":
144
+ scale = hidden_states.shape[-1] ** -0.5
145
+ scores = (hidden_states * self.pool_query).sum(dim=-1) * scale
146
+ if attention_mask is not None:
147
+ scores = scores.masked_fill(attention_mask == 0, float("-inf"))
148
+ weights = F.softmax(scores, dim=-1).unsqueeze(-1)
149
+ return (hidden_states * weights).sum(dim=1)
150
+ # Mean pooling fallback
151
+ if attention_mask is not None:
152
+ mask = attention_mask.unsqueeze(-1).to(hidden_states.dtype)
153
+ return (hidden_states * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1e-9)
154
+ return hidden_states.mean(dim=1)
155
+
156
+ def forward(
157
+ self,
158
+ hidden_states: torch.Tensor,
159
+ attention_mask: Optional[torch.Tensor] = None,
160
+ normalize: bool = True,
161
+ truncate_dim: Optional[int] = None,
162
+ return_colbert: bool = False,
163
+ ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
164
+ B, T, _ = hidden_states.shape
165
+ position_ids = torch.arange(T, device=hidden_states.device).unsqueeze(0).expand(B, -1)
166
+
167
+ # 1. Begin blocks (4 unique NarrowA)
168
+ for layer in self.begin_blocks:
169
+ hidden_states = layer(hidden_states, attention_mask=attention_mask, position_ids=position_ids)
170
+
171
+ # 2. Phase1 loop: 2 × [WideA + NarrowA×4] = 10 passes
172
+ hidden_states = self.phase1(hidden_states, attention_mask=attention_mask, position_ids=position_ids)
173
+
174
+ # 3. Phase2 loop: 4 × [NarrowB×4 + WideB] = 20 passes
175
+ hidden_states = self.phase2(hidden_states, attention_mask=attention_mask, position_ids=position_ids)
176
+
177
+ # 4. End block (1 unique WideB)
178
+ hidden_states = self.end_block(hidden_states, attention_mask=attention_mask, position_ids=position_ids)
179
+
180
+ # 5. Final norm
181
+ hidden_states = self.norm(hidden_states)
182
+
183
+ # --- Output 1: Single vector ---
184
+ single_embed = self._pool(hidden_states, attention_mask)
185
+ if truncate_dim is not None:
186
+ single_embed = single_embed[:, :truncate_dim]
187
+ if normalize:
188
+ single_embed = F.normalize(single_embed, p=2, dim=-1)
189
+
190
+ # --- Output 2: ColBERT token vectors ---
191
+ if return_colbert and self.use_colbert:
192
+ token_embeds = self.colbert_head(hidden_states, attention_mask)
193
+ return single_embed, token_embeds
194
+
195
+ return single_embed
196
+
197
+ def count_parameters(self) -> dict:
198
+ total = sum(p.numel() for p in self.parameters())
199
+ trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
200
+ return {"backbone_total": total, "trainable": trainable}
modeling/gdn2_attention.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gated DeltaNet-2 Dual-Path Attention.
3
+
4
+ Dual-path design:
5
+ Path 1 (Softmax): Standard chunked softmax attention with RoPE
6
+ → Uses Gemma Q,K,V weights directly, 100% compatible
7
+ Path 2 (GDN-2): Gated Delta Rule-2 with channel-wise erase/write gates
8
+ → Learns linear attention gradually (init as near-no-op)
9
+
10
+ Merge: output = α × softmax_path + (1-α) × gdn2_path
11
+ α init ≈ 1.0 → model starts as pure softmax (Gemma weights work perfectly)
12
+ α learned → shifts to GDN-2 as training progresses (O(T) inference)
13
+
14
+ Gated Delta Rule-2 (per timestep):
15
+ 1. Decay: A_t = diag(α_t) @ A_{t-1}
16
+ 2. Erase: u_t = b_t ⊙ k_t (channel-wise erase gate)
17
+ 3. Write: A_t -= u_t^T @ (u_t @ A_{t-1} - w_t ⊙ v_t)
18
+ 4. Read: o_t = q_t @ A_t
19
+
20
+ Reference: Hatamizadeh et al. "Gated DeltaNet-2" (NVIDIA, May 2026)
21
+ """
22
+
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.nn.functional as F
26
+ import math
27
+ from typing import Optional
28
+ from einops import rearrange
29
+
30
+ from .utils import RMSNorm, YaRNRotaryEmbedding, apply_rotary_pos_emb, apply_depth_rotary_emb
31
+
32
+
33
+ class ShortConv1d(nn.Module):
34
+ """Causal 1D convolution for Q,K preprocessing (init as identity pass-through)."""
35
+
36
+ def __init__(self, dim: int, kernel_size: int = 4):
37
+ super().__init__()
38
+ self.conv = nn.Conv1d(dim, dim, kernel_size, padding=kernel_size - 1, groups=dim)
39
+ # Init as identity: center weight = 1, rest = 0
40
+ nn.init.zeros_(self.conv.weight)
41
+ nn.init.zeros_(self.conv.bias)
42
+ # Set last position (causal) to 1 for identity
43
+ with torch.no_grad():
44
+ self.conv.weight[:, :, -1] = 1.0
45
+
46
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
47
+ """x: (B, T, D) → (B, T, D)"""
48
+ x = x.transpose(1, 2) # (B, D, T)
49
+ x = self.conv(x)[..., :x.shape[-1]] # causal: trim future padding
50
+ return x.transpose(1, 2) # (B, T, D)
51
+
52
+
53
+ class GatedDeltaNet2Attention(nn.Module):
54
+ """
55
+ Dual-Path: Softmax + Gated DeltaNet-2.
56
+
57
+ Init strategy: α ≈ 1 (pure softmax) → Gemma weights work immediately.
58
+ Training shifts α towards GDN-2 path for O(T) inference.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ hidden_size: int,
64
+ num_heads: int,
65
+ num_kv_heads: int,
66
+ head_dim: int,
67
+ chunk_size: int = 64,
68
+ attention_dropout: float = 0.0,
69
+ layer_idx: int = 0,
70
+ use_dual_path: bool = True,
71
+ softmax_init_alpha: float = 5.0,
72
+ use_short_conv: bool = True,
73
+ conv_kernel_size: int = 4,
74
+ # RoPE params
75
+ max_position_embeddings: int = 131072,
76
+ rope_theta: float = 1000000.0,
77
+ rope_scaling_factor: float = 32.0,
78
+ rope_original_max_position: int = 4096,
79
+ rope_beta_fast: int = 32,
80
+ rope_beta_slow: int = 1,
81
+ ):
82
+ super().__init__()
83
+ self.hidden_size = hidden_size
84
+ self.num_heads = num_heads
85
+ self.num_kv_heads = num_kv_heads
86
+ self.head_dim = head_dim
87
+ self.num_kv_groups = num_heads // num_kv_heads
88
+ self.layer_idx = layer_idx
89
+ self.chunk_size = chunk_size
90
+ self.use_dual_path = use_dual_path
91
+
92
+ # === Shared projections (from Gemma 4 E2B) ===
93
+ self.q_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
94
+ self.k_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=False)
95
+ self.v_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=False)
96
+ self.o_proj = nn.Linear(num_heads * head_dim, hidden_size, bias=False)
97
+
98
+ # === Q/K normalization (L2 norm per head — GDN-2 best practice) ===
99
+ self.q_norm = RMSNorm(head_dim)
100
+ self.k_norm = RMSNorm(head_dim)
101
+
102
+ # === RoPE for softmax path ===
103
+ self.rotary_emb = YaRNRotaryEmbedding(
104
+ dim=head_dim,
105
+ max_position_embeddings=max_position_embeddings,
106
+ base=rope_theta,
107
+ scaling_factor=rope_scaling_factor,
108
+ original_max_position=rope_original_max_position,
109
+ beta_fast=rope_beta_fast,
110
+ beta_slow=rope_beta_slow,
111
+ )
112
+
113
+ # === Short conv for GDN-2 path Q,K (init as identity) ===
114
+ if use_short_conv:
115
+ self.q_conv = ShortConv1d(num_heads * head_dim, conv_kernel_size)
116
+ self.k_conv = ShortConv1d(num_kv_heads * head_dim, conv_kernel_size)
117
+ else:
118
+ self.q_conv = None
119
+ self.k_conv = None
120
+
121
+ # === GDN-2 specific params (all NEW, init for near-no-op) ===
122
+ # Erase gate: b_t = sigmoid(W_b @ x_t) ∈ [0,1]^{d_k}
123
+ self.erase_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=True)
124
+ nn.init.zeros_(self.erase_proj.weight)
125
+ nn.init.zeros_(self.erase_proj.bias) # sigmoid(0)=0.5 → moderate erase
126
+
127
+ # Write gate: w_t = sigmoid(W_w @ x_t) ∈ [0,1]^{d_v}
128
+ self.write_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=True)
129
+ nn.init.zeros_(self.write_proj.weight)
130
+ nn.init.zeros_(self.write_proj.bias) # sigmoid(0)=0.5 → moderate write
131
+
132
+ # Decay (channel-wise, log-parameterized for stability)
133
+ # g_t = -exp(a) * softplus(W_f @ x_t + δ)
134
+ # α_t = exp(g_t) ∈ (0, 1]
135
+ # Init: a=-5, δ=5 → g≈0 → α≈1 (no decay initially)
136
+ self.decay_a = nn.Parameter(torch.full((num_kv_heads * head_dim,), -5.0))
137
+ self.decay_delta = nn.Parameter(torch.full((num_kv_heads * head_dim,), 5.0))
138
+ self.decay_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=False)
139
+ nn.init.zeros_(self.decay_proj.weight) # → softplus(0+5)≈5, g=-exp(-5)*5≈-0.03, α≈0.97
140
+
141
+ # === Dual-path mix: α (softmax weight) ===
142
+ # Init large positive → sigmoid ≈ 1 → pure softmax at start
143
+ self.path_mix_logit = nn.Parameter(torch.full((num_heads,), softmax_init_alpha))
144
+
145
+ # === Output gate (SiLU gating like GDN-2 paper) ===
146
+ self.output_gate_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=True)
147
+
148
+ # === RoDE placeholder ===
149
+ self._rode = None
150
+
151
+ self.dropout = nn.Dropout(attention_dropout)
152
+
153
+ def _expand_kv(self, x: torch.Tensor) -> torch.Tensor:
154
+ """Expand KV heads for GQA: (B, H_kv, T, D) → (B, H, T, D)"""
155
+ if self.num_kv_groups == 1:
156
+ return x
157
+ B, H_kv, T, D = x.shape
158
+ x = x.unsqueeze(2).expand(-1, -1, self.num_kv_groups, -1, -1)
159
+ return x.reshape(B, self.num_heads, T, D)
160
+
161
+ # ── Path 1: Standard Softmax Attention (uses RoPE) ──────────
162
+
163
+ def _softmax_attention(
164
+ self,
165
+ q: torch.Tensor, # (B, H, T, D)
166
+ k: torch.Tensor, # (B, H, T, D) — already expanded for GQA
167
+ v: torch.Tensor, # (B, H, T, D)
168
+ attention_mask: Optional[torch.Tensor] = None,
169
+ ) -> torch.Tensor:
170
+ B, H, T, D = q.shape
171
+ scale = 1.0 / math.sqrt(D)
172
+
173
+ # Full attention for short seqs, chunked for long
174
+ scores = torch.matmul(q, k.transpose(-1, -2)) * scale # (B, H, T, T)
175
+
176
+ if attention_mask is not None:
177
+ # attention_mask: (B, T) → (B, 1, 1, T)
178
+ mask = attention_mask[:, None, None, :].to(scores.dtype)
179
+ scores = scores.masked_fill(mask == 0, float("-inf"))
180
+
181
+ attn_weights = F.softmax(scores, dim=-1)
182
+ attn_weights = torch.nan_to_num(attn_weights, nan=0.0)
183
+ attn_weights = self.dropout(attn_weights)
184
+
185
+ return torch.matmul(attn_weights, v)
186
+
187
+ # ── Path 2: Gated DeltaNet-2 (Recurrent, O(T) inference) ───
188
+
189
+ def _gdn2_attention(
190
+ self,
191
+ q: torch.Tensor, # (B, H, T, D) — L2 normalized
192
+ k: torch.Tensor, # (B, H_kv, T, D) — L2 normalized
193
+ v: torch.Tensor, # (B, H_kv, T, D)
194
+ erase_gate: torch.Tensor, # (B, T, d_kv)
195
+ write_gate: torch.Tensor, # (B, T, d_kv)
196
+ decay: torch.Tensor, # (B, T, d_kv) — α_t values
197
+ attention_mask: Optional[torch.Tensor] = None,
198
+ ) -> torch.Tensor:
199
+ """
200
+ Gated Delta Rule-2 via FLA chunk-parallel kernel (O(T) training).
201
+ Falls back to sequential recurrence if FLA not available.
202
+ """
203
+ B, H_kv, T, D = k.shape
204
+ H = self.num_heads
205
+ dtype = q.dtype
206
+
207
+ # Expand KV for GQA
208
+ k_exp = self._expand_kv(k) # (B, H, T, D)
209
+ v_exp = self._expand_kv(v) # (B, H, T, D)
210
+
211
+ # Reshape gates for FLA: (B, T, d_kv) → (B, H, T, D) → reshape for kernel
212
+ erase = rearrange(erase_gate, "b t (h d) -> b h t d", h=H_kv)
213
+ write = rearrange(write_gate, "b t (h d) -> b h t d", h=H_kv)
214
+ alpha = rearrange(decay, "b t (h d) -> b h t d", h=H_kv)
215
+
216
+ erase_exp = self._expand_kv(erase) # (B, H, T, D)
217
+ write_exp = self._expand_kv(write) # (B, H, T, D)
218
+ alpha_exp = self._expand_kv(alpha) # (B, H, T, D)
219
+
220
+ try:
221
+ # Use FLA chunk kernel for all sequences
222
+ from fla.ops.gated_delta_rule import chunk_gated_delta_rule as fla_chunk_gdn
223
+
224
+ # FLA expects: q,k = [B, T, H, D], v = [B, T, HV, V]
225
+ # g = [B, T, HV] (log-space forget gate, per-head)
226
+ # beta = [B, T, HV] (update gate, per-head, 0-1)
227
+
228
+ # Transpose from (B, H, T, D) → (B, T, H, D) and ensure BF16
229
+ q_fla = q.transpose(1, 2).contiguous().to(torch.bfloat16)
230
+ k_fla = k_exp.transpose(1, 2).contiguous().to(torch.bfloat16)
231
+
232
+ # Apply write gate to values, then transpose
233
+ v_gated = (write_exp * v_exp) # (B, H, T, D)
234
+ v_fla = v_gated.transpose(1, 2).contiguous().to(torch.bfloat16)
235
+
236
+ # g: per-head log-decay (MUST be float32)
237
+ g_per_head = torch.log(alpha_exp.float().clamp(min=1e-6)).mean(dim=-1) # (B, H, T)
238
+ g_fla = g_per_head.transpose(1, 2).contiguous() # (B, T, H) float32
239
+
240
+ # beta: per-head erase strength (MUST be float32)
241
+ beta_per_head = erase_exp.float().mean(dim=-1) # (B, H, T)
242
+ beta_fla = beta_per_head.transpose(1, 2).contiguous() # (B, T, H) float32
243
+
244
+ # Call FLA kernel outside autocast
245
+ with torch.amp.autocast(device_type="cuda", enabled=False):
246
+ output, _ = fla_chunk_gdn(
247
+ q=q_fla,
248
+ k=k_fla,
249
+ v=v_fla,
250
+ g=g_fla,
251
+ beta=beta_fla,
252
+ scale=1.0,
253
+ use_qk_l2norm_in_kernel=False,
254
+ )
255
+ # output: (B, T, H, D) → transpose back to (B, H, T, D)
256
+ return output.transpose(1, 2).to(dtype)
257
+
258
+ except (ImportError, RuntimeError) as e:
259
+ # Sequential recurrence fallback
260
+ return self._gdn2_sequential(q, k_exp, v_exp, erase_exp, write_exp, alpha_exp, attention_mask)
261
+
262
+ def _gdn2_sequential(
263
+ self,
264
+ q: torch.Tensor, # (B, H, T, D)
265
+ k: torch.Tensor, # (B, H, T, D)
266
+ v: torch.Tensor, # (B, H, T, D)
267
+ erase: torch.Tensor, # (B, H, T, D)
268
+ write: torch.Tensor, # (B, H, T, D)
269
+ alpha: torch.Tensor, # (B, H, T, D)
270
+ attention_mask: Optional[torch.Tensor] = None,
271
+ ) -> torch.Tensor:
272
+ """Sequential fallback for GDN-2 (used when FLA kernel unavailable)."""
273
+ B, H, T, D = q.shape
274
+ device = q.device
275
+ dtype = q.dtype
276
+
277
+ state = torch.zeros(B, H, D, D, device=device, dtype=torch.float32)
278
+ outputs = []
279
+
280
+ for t in range(T):
281
+ if attention_mask is not None and t < attention_mask.shape[1]:
282
+ mask_t = attention_mask[:, t].view(B, 1, 1, 1).float()
283
+ else:
284
+ mask_t = 1.0
285
+
286
+ q_t = q[:, :, t, :]
287
+ k_t = k[:, :, t, :]
288
+ v_t = v[:, :, t, :]
289
+ b_t = erase[:, :, t, :]
290
+ w_t = write[:, :, t, :]
291
+ a_t = alpha[:, :, t, :]
292
+
293
+ state = state * a_t.unsqueeze(-1).float()
294
+ u_t = (b_t * k_t).float()
295
+ old_val = torch.einsum("bhd,bhde->bhe", u_t, state)
296
+ target = (w_t * v_t).float()
297
+ error = target - old_val
298
+ state = state + torch.einsum("bhd,bhe->bhde", u_t, error) * mask_t
299
+ o_t = torch.einsum("bhd,bhde->bhe", q_t.float(), state)
300
+ outputs.append(o_t)
301
+
302
+ output = torch.stack(outputs, dim=2).to(dtype)
303
+ return output
304
+
305
+ # ── Forward ─────────────────────────────────────────────────
306
+
307
+ def forward(
308
+ self,
309
+ hidden_states: torch.Tensor,
310
+ attention_mask: Optional[torch.Tensor] = None,
311
+ position_ids: Optional[torch.Tensor] = None,
312
+ loop_idx: Optional[int] = None,
313
+ lora_deltas: Optional[dict] = None,
314
+ ) -> torch.Tensor:
315
+ B, T, _ = hidden_states.shape
316
+
317
+ # === Projections (with optional LoRA) ===
318
+ def proj_with_lora(proj, x, name):
319
+ if lora_deltas and name in lora_deltas:
320
+ return F.linear(x, proj.weight + lora_deltas[name], proj.bias if proj.bias is not None else None)
321
+ return proj(x)
322
+
323
+ q_raw = proj_with_lora(self.q_proj, hidden_states, "q_proj") # (B, T, H*D)
324
+ k_raw = proj_with_lora(self.k_proj, hidden_states, "k_proj") # (B, T, H_kv*D)
325
+ v = proj_with_lora(self.v_proj, hidden_states, "v_proj") # (B, T, H_kv*D)
326
+
327
+ # === Path 1: Softmax (standard RoPE attention) ===
328
+ q1 = rearrange(q_raw, "b t (h d) -> b h t d", h=self.num_heads)
329
+ k1 = rearrange(k_raw, "b t (h d) -> b h t d", h=self.num_kv_heads)
330
+ v1 = rearrange(v, "b t (h d) -> b h t d", h=self.num_kv_heads)
331
+
332
+ # RoDE depth signal
333
+ if loop_idx is not None and self._rode is not None:
334
+ cos_d, sin_d = self._rode(q1, loop_idx)
335
+ q1 = apply_depth_rotary_emb(q1.flatten(0, 1), cos_d, sin_d).view(B, self.num_heads, T, -1)
336
+ k1 = apply_depth_rotary_emb(k1.flatten(0, 1), cos_d, sin_d).view(B, self.num_kv_heads, T, -1)
337
+
338
+ # Q/K norm
339
+ q1 = self.q_norm(q1)
340
+ k1 = self.k_norm(k1)
341
+
342
+ # RoPE
343
+ cos, sin = self.rotary_emb(q1, position_ids)
344
+ k1_expanded = self._expand_kv(k1)
345
+ v1_expanded = self._expand_kv(v1)
346
+ q1_rope, k1_rope = apply_rotary_pos_emb(q1, k1_expanded, cos, sin)
347
+ # Determine if we can skip softmax (alpha=0 means pure GDN-2)
348
+ skip_softmax = (hasattr(self, '_alpha_override') and self._alpha_override is not None
349
+ and self._alpha_override == 0.0)
350
+
351
+ if not skip_softmax:
352
+ o_softmax = self._softmax_attention(q1_rope, k1_rope, v1_expanded, attention_mask)
353
+
354
+ if not self.use_dual_path:
355
+ # Pure softmax mode (no GDN-2)
356
+ attn_output = o_softmax
357
+ else:
358
+ # === Path 2: GDN-2 ===
359
+ # Apply short conv to Q,K for GDN-2 path
360
+ if self.q_conv is not None:
361
+ q2_raw = F.silu(self.q_conv(q_raw))
362
+ k2_raw = F.silu(self.k_conv(k_raw))
363
+ else:
364
+ q2_raw = q_raw
365
+ k2_raw = k_raw
366
+
367
+ q2 = rearrange(q2_raw, "b t (h d) -> b h t d", h=self.num_heads)
368
+ k2 = rearrange(k2_raw, "b t (h d) -> b h t d", h=self.num_kv_heads)
369
+ v2 = rearrange(v, "b t (h d) -> b h t d", h=self.num_kv_heads)
370
+
371
+ # L2 normalize Q,K for GDN-2 (best practice)
372
+ q2 = F.normalize(q2, p=2, dim=-1)
373
+ k2 = F.normalize(k2, p=2, dim=-1)
374
+
375
+ # Compute gates
376
+ erase_gate = torch.sigmoid(self.erase_proj(hidden_states)) # (B, T, d_kv)
377
+ write_gate = torch.sigmoid(self.write_proj(hidden_states)) # (B, T, d_kv)
378
+
379
+ # Compute decay: α_t = exp(-exp(a) * softplus(W_f @ x + δ))
380
+ decay_input = self.decay_proj(hidden_states) + self.decay_delta # (B, T, d_kv)
381
+ g_t = -torch.exp(self.decay_a) * F.softplus(decay_input) # (B, T, d_kv)
382
+ decay = torch.exp(g_t) # ∈ (0, 1]
383
+
384
+ o_gdn2 = self._gdn2_attention(q2, k2, v2, erase_gate, write_gate, decay, attention_mask)
385
+
386
+ # === Merge paths ===
387
+ # Use scheduled alpha if set, otherwise learnable
388
+ if hasattr(self, '_alpha_override') and self._alpha_override is not None:
389
+ alpha = self._alpha_override
390
+ else:
391
+ alpha = torch.sigmoid(self.path_mix_logit).view(1, self.num_heads, 1, 1)
392
+
393
+ if skip_softmax:
394
+ attn_output = o_gdn2
395
+ elif isinstance(alpha, float) and alpha == 0.0:
396
+ attn_output = o_gdn2
397
+ else:
398
+ attn_output = alpha * o_softmax + (1.0 - alpha) * o_gdn2
399
+
400
+ # === Output gate (SiLU) ===
401
+ gate = F.silu(rearrange(
402
+ self.output_gate_proj(hidden_states), "b t (h d) -> b h t d", h=self.num_heads
403
+ ))
404
+ attn_output = gate * attn_output
405
+
406
+ # === Output projection ===
407
+ attn_output = rearrange(attn_output, "b h t d -> b t (h d)")
408
+ attn_output = proj_with_lora(self.o_proj, attn_output, "o_proj")
409
+ attn_output = self.dropout(attn_output)
410
+
411
+ return attn_output
modeling/gla_attention.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dual-Path Gated Linear Attention (v2.0).
3
+
4
+ Solves the "RoPE through feature map" problem by splitting into two parallel paths
5
+ on the SAME full 64-dim Q/K:
6
+
7
+ Path 1 (RoPE Chunked Attention):
8
+ Q_rope, K_rope = RoPE(Q, K)
9
+ O_rope = ChunkedSoftmaxAttention(Q_rope, K_rope, V, chunk_size=C)
10
+ Complexity: O(N × C) — local positional accuracy
11
+
12
+ Path 2 (Decay Linear Attention — Parallel Associative Scan):
13
+ Q_lin, K_lin = φ(Q), φ(K) ← Q/K BEFORE RoPE
14
+ Full T×T decay matrix: M[t,i] = exp(cumsum(logγ)[t] - cumsum(logγ)[i])
15
+ O_decay = (Q·K^T ⊙ M) · V / normalizer ← bidirectional
16
+ Complexity: O(T² × H) memory/compute — fast for T ≤ 2048, Blackwell-optimized
17
+
18
+ Merge: O = α · O_rope + (1-α) · O_decay ← α learnable per head
19
+ """
20
+
21
+ import torch
22
+ import torch.nn as nn
23
+ import torch.nn.functional as F
24
+ import math
25
+ from typing import Optional, List
26
+ from einops import rearrange
27
+
28
+ from .utils import RMSNorm, YaRNRotaryEmbedding, apply_rotary_pos_emb, RoDE, apply_depth_rotary_emb
29
+
30
+
31
+ class DualPathGLAAttention(nn.Module):
32
+ def __init__(
33
+ self,
34
+ hidden_size: int,
35
+ num_heads: int,
36
+ num_kv_heads: int,
37
+ head_dim: int,
38
+ feature_map: str = "relu",
39
+ attention_dropout: float = 0.0,
40
+ layer_idx: int = 0,
41
+ chunk_size: int = 2048,
42
+ decay_init: List[float] = None,
43
+ use_data_dependent_decay: bool = False,
44
+ # RoPE params
45
+ max_position_embeddings: int = 131072,
46
+ rope_theta: float = 1000000.0,
47
+ rope_scaling_factor: float = 4.0,
48
+ rope_original_max_position: int = 32768,
49
+ rope_beta_fast: int = 32,
50
+ rope_beta_slow: int = 1,
51
+ ):
52
+ super().__init__()
53
+ self.hidden_size = hidden_size
54
+ self.num_heads = num_heads
55
+ self.num_kv_heads = num_kv_heads
56
+ self.head_dim = head_dim
57
+ self.num_kv_groups = num_heads // num_kv_heads
58
+ self.layer_idx = layer_idx
59
+ self.chunk_size = chunk_size
60
+ self.use_data_dependent_decay = use_data_dependent_decay
61
+
62
+ # Shared projections (transplanted from Gemma 4)
63
+ self.q_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=False)
64
+ self.k_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=False)
65
+ self.v_proj = nn.Linear(hidden_size, num_kv_heads * head_dim, bias=False)
66
+ self.o_proj = nn.Linear(num_heads * head_dim, hidden_size, bias=False)
67
+
68
+ # Gate (new param — not from Gemma 4)
69
+ self.gate_proj = nn.Linear(hidden_size, num_heads * head_dim, bias=True)
70
+
71
+ # Q/K normalization
72
+ self.q_norm = RMSNorm(head_dim)
73
+ self.k_norm = RMSNorm(head_dim)
74
+
75
+ # RoPE for Path 1
76
+ self.rotary_emb = YaRNRotaryEmbedding(
77
+ dim=head_dim,
78
+ max_position_embeddings=max_position_embeddings,
79
+ base=rope_theta,
80
+ scaling_factor=rope_scaling_factor,
81
+ original_max_position=rope_original_max_position,
82
+ beta_fast=rope_beta_fast,
83
+ beta_slow=rope_beta_slow,
84
+ )
85
+
86
+ # Per-head decay γ for Path 2 — init from config
87
+ if decay_init is None:
88
+ decay_init = [0.99] * num_heads
89
+ init_logits = torch.tensor([math.log(g / (1.0 - g)) for g in decay_init])
90
+ self.log_decay = nn.Parameter(init_logits) # sigmoid(log_decay) = γ
91
+
92
+ # Data-dependent decay projection (init zeros for stability)
93
+ if use_data_dependent_decay:
94
+ self.decay_proj = nn.Linear(hidden_size, num_heads, bias=False)
95
+ nn.init.zeros_(self.decay_proj.weight)
96
+
97
+ # Per-head stream mix α — init 0 → sigmoid(0) = 0.5
98
+ self.stream_mix_logit = nn.Parameter(torch.zeros(num_heads))
99
+
100
+ # Per-path RMSNorm before merge (prevents dead path from variance mismatch)
101
+ self.rope_path_norm = RMSNorm(head_dim)
102
+ self.decay_path_norm = RMSNorm(head_dim)
103
+
104
+ # Post-GLA RMSNorm (new param)
105
+ self.post_attn_norm = RMSNorm(head_dim)
106
+
107
+ # RoDE: Rotary Depth Embedding applied to Q/K (not hidden_states)
108
+ # Only created if depth_rotary_dims is set in config (injected from outside via loop_idx)
109
+ self._rode = None # Will be set by HyperloopSegment if needed
110
+
111
+ self.feature_map_type = feature_map
112
+ self.dropout = nn.Dropout(attention_dropout)
113
+
114
+ # Backward weight: learnable per-layer, init sigmoid(-2.2) ≈ 0.1
115
+ # Model learns optimal forward/backward balance per layer
116
+ self.backward_logit = nn.Parameter(torch.tensor(-2.2))
117
+
118
+ def _feature_map(self, x: torch.Tensor) -> torch.Tensor:
119
+ if self.feature_map_type == "elu":
120
+ return F.elu(x) + 1.0
121
+ return F.relu(x) + 1e-4 # default: relu + eps
122
+
123
+ def _expand_kv(self, x: torch.Tensor) -> torch.Tensor:
124
+ if self.num_kv_groups == 1:
125
+ return x
126
+ x = x.unsqueeze(2).expand(-1, -1, self.num_kv_groups, -1, -1)
127
+ return x.reshape(x.shape[0], self.num_heads, x.shape[3], x.shape[4])
128
+
129
+ # ── Path 1: RoPE Chunked Attention ──────────────────────────
130
+
131
+ def _chunked_attention(
132
+ self,
133
+ q: torch.Tensor, # (B, H, T, D) — already RoPE'd
134
+ k: torch.Tensor, # (B, H, T, D)
135
+ v: torch.Tensor, # (B, H, T, D)
136
+ attention_mask: Optional[torch.Tensor] = None,
137
+ ) -> torch.Tensor:
138
+ B, H, T, D = q.shape
139
+ C = self.chunk_size
140
+
141
+ # Pad to multiple of chunk_size
142
+ pad_len = (C - T % C) % C
143
+ if pad_len > 0:
144
+ q = F.pad(q, (0, 0, 0, pad_len))
145
+ k = F.pad(k, (0, 0, 0, pad_len))
146
+ v = F.pad(v, (0, 0, 0, pad_len))
147
+ if attention_mask is not None:
148
+ attention_mask = F.pad(attention_mask, (0, pad_len), value=0)
149
+
150
+ T_padded = q.shape[2]
151
+ num_chunks = T_padded // C
152
+
153
+ # Reshape into chunks: (B, H, num_chunks, C, D)
154
+ q_c = q.view(B, H, num_chunks, C, D)
155
+ k_c = k.view(B, H, num_chunks, C, D)
156
+ v_c = v.view(B, H, num_chunks, C, D)
157
+
158
+ # Attention scores within each chunk: (B, H, num_chunks, C, C)
159
+ scale = 1.0 / math.sqrt(D)
160
+ scores = torch.einsum("bhnid,bhnjd->bhnij", q_c, k_c) * scale
161
+
162
+ # Mask padding within chunks
163
+ if attention_mask is not None:
164
+ chunk_mask = attention_mask.view(B, num_chunks, C) # (B, nc, C)
165
+ # (B, 1, nc, C, 1) × (B, 1, nc, 1, C) → (B, 1, nc, C, C)
166
+ mask_2d = chunk_mask[:, None, :, :, None] * chunk_mask[:, None, :, None, :]
167
+ scores = scores.masked_fill(mask_2d == 0, float("-inf"))
168
+
169
+ attn_weights = F.softmax(scores, dim=-1)
170
+ attn_weights = torch.nan_to_num(attn_weights, nan=0.0)
171
+ attn_weights = self.dropout(attn_weights)
172
+
173
+ # Weighted sum: (B, H, num_chunks, C, D)
174
+ out = torch.einsum("bhnij,bhnjd->bhnid", attn_weights, v_c)
175
+
176
+ # Reshape back: (B, H, T_padded, D) → trim to (B, H, T, D)
177
+ out = out.view(B, H, T_padded, D)[:, :, :T, :]
178
+ return out
179
+
180
+ # ── Path 2: Decay Linear Attention (Bidirectional) ──────────
181
+
182
+ def _decay_attention_one_direction(
183
+ self,
184
+ q: torch.Tensor, # (B, H, T, D) — feature-mapped, NOT RoPE'd
185
+ k: torch.Tensor,
186
+ v: torch.Tensor,
187
+ gamma: torch.Tensor, # (B, H) or (B, T, H) for data-dependent
188
+ attention_mask: Optional[torch.Tensor] = None,
189
+ reverse: bool = False,
190
+ ) -> torch.Tensor:
191
+ """
192
+ Parallel Associative Scan implementation of Decay Linear Attention.
193
+ Complexity: O(T^2) memory/compute — extremely fast for T <= 2048.
194
+ Bypasses the slow O(T) Python loop and avoids Blackwell Triton hangs.
195
+ """
196
+ B, H, T, D = q.shape
197
+ D_v = v.shape[-1]
198
+
199
+ if reverse:
200
+ q = q.flip(dims=[2])
201
+ k = k.flip(dims=[2])
202
+ v = v.flip(dims=[2])
203
+ if attention_mask is not None:
204
+ attention_mask = attention_mask.flip(dims=[1])
205
+
206
+ # FP32 for numerical stability
207
+ orig_dtype = q.dtype
208
+ q, k, v = q.float(), k.float(), v.float()
209
+
210
+ # Gamma handling: (H,) or (B, T, H)
211
+ if gamma.dim() == 1:
212
+ # Broadcast (H,) to (B, T, H)
213
+ g = gamma.view(1, 1, H).expand(B, T, H)
214
+ elif gamma.dim() == 2:
215
+ # Broadcast (B, H) to (B, T, H)
216
+ g = gamma.unsqueeze(1).expand(B, T, H)
217
+ else:
218
+ g = gamma # (B, T, H)
219
+
220
+ # 1. Compute cumulative decay logs: log_g = log(gamma)
221
+ log_g = torch.log(g.clamp(min=1e-8))
222
+
223
+ # 2. Compute Decay Matrix M where M[t, i] = prod_{j=i+1}^t gamma_j
224
+ # M[t, i] = exp(cumsum(log_g)[t] - cumsum(log_g)[i])
225
+ # We use a T x T matrix approach for maximum speed at small T
226
+
227
+ # cum_log_g: (B, T, H)
228
+ cum_log_g = torch.cumsum(log_g, dim=1)
229
+
230
+ # (B, H, T, 1)
231
+ clg = cum_log_g.transpose(1, 2).unsqueeze(-1)
232
+ # diff[t, i] = cum_log_g[t] - cum_log_g[i] -> (B, H, T, T)
233
+ diff = clg - clg.transpose(-1, -2)
234
+
235
+ decay_matrix = torch.exp(diff)
236
+
237
+ # Causal mask: only i <= t
238
+ m_idx = torch.arange(T, device=q.device)
239
+ mask = (m_idx.view(-1, 1) >= m_idx.view(1, -1)).to(q.dtype)
240
+ decay_matrix = decay_matrix * mask.view(1, 1, T, T)
241
+
242
+ if attention_mask is not None:
243
+ # decay_matrix[t, i] should be 0 if token i is masked
244
+ decay_matrix = decay_matrix * attention_mask.view(B, 1, 1, T)
245
+
246
+ # 3. Compute output: O_t = sum_{i=0}^t M[t, i] * (Q_t * K_i^T) * V_i
247
+
248
+ # dot_qk: (B, H, T, T)
249
+ dot_qk = torch.einsum("bhtd,bhid->bhti", q, k)
250
+ # weights: (B, H, T, T)
251
+ weights = dot_qk * decay_matrix
252
+
253
+ # normalizer: sum of decay * K
254
+ # z_t = sum_{i=0}^t M[t, i] * K_i
255
+ z_scores = torch.einsum("bhti,bhid->bhtd", decay_matrix, k)
256
+ norm = torch.einsum("bhtd,bhtd->bht", q, z_scores).unsqueeze(-1).clamp(min=1e-6)
257
+
258
+ # out: (B, H, T, D_v)
259
+ out = torch.einsum("bhti,bhid->bhtd", weights, v)
260
+ out = (out / norm).to(orig_dtype)
261
+
262
+ if reverse:
263
+ out = out.flip(dims=[2])
264
+ return out
265
+
266
+ @property
267
+ def backward_weight(self):
268
+ return torch.sigmoid(self.backward_logit)
269
+
270
+ def _decay_attention(
271
+ self,
272
+ q: torch.Tensor,
273
+ k: torch.Tensor,
274
+ v: torch.Tensor,
275
+ gamma: torch.Tensor,
276
+ attention_mask: Optional[torch.Tensor] = None,
277
+ backward_weight: Optional[torch.Tensor] = None,
278
+ ) -> torch.Tensor:
279
+ bw = backward_weight if backward_weight is not None else self.backward_weight
280
+ fwd = self._decay_attention_one_direction(q, k, v, gamma, attention_mask, reverse=False)
281
+ bwd = self._decay_attention_one_direction(q, k, v, gamma, attention_mask, reverse=True)
282
+ return (1.0 - bw) * fwd + bw * bwd
283
+
284
+ # ── Forward ─────────────────────────────────────────────────
285
+
286
+ def forward(
287
+ self,
288
+ hidden_states: torch.Tensor,
289
+ attention_mask: Optional[torch.Tensor] = None,
290
+ position_ids: Optional[torch.Tensor] = None,
291
+ backward_weight: Optional[torch.Tensor] = None,
292
+ stream_mix_alpha: Optional[torch.Tensor] = None,
293
+ loop_idx: Optional[int] = None,
294
+ lora_deltas: Optional[dict] = None,
295
+ ) -> torch.Tensor:
296
+ B, T, _ = hidden_states.shape
297
+
298
+ # === Shared projections (with optional per-loop LoRA) ===
299
+ if lora_deltas and "q_proj" in lora_deltas:
300
+ q = F.linear(hidden_states, self.q_proj.weight + lora_deltas["q_proj"])
301
+ else:
302
+ q = self.q_proj(hidden_states)
303
+ q = rearrange(q, "b t (h d) -> b h t d", h=self.num_heads)
304
+
305
+ if lora_deltas and "k_proj" in lora_deltas:
306
+ k = F.linear(hidden_states, self.k_proj.weight + lora_deltas["k_proj"])
307
+ else:
308
+ k = self.k_proj(hidden_states)
309
+ k = rearrange(k, "b t (h d) -> b h t d", h=self.num_kv_heads)
310
+
311
+ if lora_deltas and "v_proj" in lora_deltas:
312
+ v = F.linear(hidden_states, self.v_proj.weight + lora_deltas["v_proj"])
313
+ else:
314
+ v = self.v_proj(hidden_states)
315
+ v = rearrange(v, "b t (h d) -> b h t d", h=self.num_kv_heads)
316
+
317
+ q = self.q_norm(q)
318
+ k = self.k_norm(k)
319
+
320
+ # === RoDE: inject depth signal into Q and K (correct location vs hidden_states) ===
321
+ # Q/K are in (B, H, T, D). RoDE xoay 8 dims đầu của head_dim.
322
+ if loop_idx is not None and self._rode is not None:
323
+ cos_d, sin_d = self._rode(q, loop_idx) # (1,1,1, depth_dims)
324
+ q = apply_depth_rotary_emb(q.flatten(0,1), cos_d, sin_d).view(B, self.num_heads, T, -1)
325
+ k = apply_depth_rotary_emb(k.flatten(0,1), cos_d, sin_d).view(B, self.num_kv_heads, T, -1)
326
+
327
+ # Expand KV for GQA BEFORE both paths
328
+ k_expanded = self._expand_kv(k)
329
+ v_expanded = self._expand_kv(v)
330
+
331
+ # === Path 1: RoPE Chunked Attention ===
332
+ cos, sin = self.rotary_emb(q, position_ids)
333
+ q_rope, k_rope = apply_rotary_pos_emb(q, self._expand_kv(k), cos, sin)
334
+ o_rope = self._chunked_attention(q_rope, k_rope, v_expanded, attention_mask)
335
+
336
+ # === Path 2: Decay Linear Attention (on original Q/K, no RoPE) ===
337
+ q_lin = self._feature_map(q)
338
+ k_lin = self._feature_map(k_expanded)
339
+
340
+ # Compute gamma
341
+ gamma = torch.sigmoid(self.log_decay) # (H,)
342
+ if self.use_data_dependent_decay and hasattr(self, 'decay_proj'):
343
+ # gamma_t = sigmoid(base_logit + data_term)
344
+ data_term = self.decay_proj(hidden_states) # (B, T, H)
345
+ gamma = torch.sigmoid(
346
+ self.log_decay.view(1, 1, -1) + data_term
347
+ ) # (B, T, H) — will be handled in scan
348
+
349
+ o_decay = self._decay_attention(q_lin, k_lin, v_expanded, gamma, attention_mask, backward_weight=backward_weight)
350
+
351
+ # === Normalize each path before merge (prevents dead path) ===
352
+ o_rope = self.rope_path_norm(o_rope)
353
+ o_decay = self.decay_path_norm(o_decay)
354
+
355
+ # === Merge: α · O_rope + (1-α) · O_decay ===
356
+ alpha = stream_mix_alpha if stream_mix_alpha is not None else torch.sigmoid(self.stream_mix_logit)
357
+ alpha = alpha.view(1, self.num_heads, 1, 1)
358
+ attn_output = alpha * o_rope + (1.0 - alpha) * o_decay
359
+
360
+ # Post-GLA RMSNorm (final stabilization)
361
+ attn_output = self.post_attn_norm(attn_output)
362
+
363
+ # Gate
364
+ gate = F.silu(
365
+ rearrange(self.gate_proj(hidden_states), "b t (h d) -> b h t d", h=self.num_heads)
366
+ )
367
+ attn_output = gate * attn_output
368
+
369
+ # Store gate for weighted pooling (detached)
370
+ # gate: (B, H, T, D) → rearrange to (B, T, H*D) to match hidden_size
371
+ self._last_gate = rearrange(gate, "b h t d -> b t (h d)").detach()
372
+
373
+ # Output projection (with optional per-loop LoRA)
374
+ attn_output = rearrange(attn_output, "b h t d -> b t (h d)")
375
+ if lora_deltas and "o_proj" in lora_deltas:
376
+ attn_output = F.linear(attn_output, self.o_proj.weight + lora_deltas["o_proj"])
377
+ else:
378
+ attn_output = self.o_proj(attn_output)
379
+ attn_output = self.dropout(attn_output)
380
+
381
+ return attn_output
modeling/hybrid_layer.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepX v0.7 Layer: GDN-2 Attention + SwiGLU MLP with pre-norm residual.
3
+
4
+ Supports both narrow (8h) and wide (16h) configurations.
5
+ """
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from typing import Optional
10
+
11
+ from .gdn2_attention import GatedDeltaNet2Attention
12
+ from .utils import RMSNorm, SwiGLUMLP
13
+
14
+
15
+ class DeepXLayer(nn.Module):
16
+ """Gated DeltaNet-2 Attention + SwiGLU MLP with pre-norm residual."""
17
+
18
+ def __init__(
19
+ self,
20
+ hidden_size: int,
21
+ num_heads: int,
22
+ num_kv_heads: int,
23
+ head_dim: int,
24
+ intermediate_size: int,
25
+ config,
26
+ layer_idx: int = 0,
27
+ ):
28
+ super().__init__()
29
+ self.input_norm = RMSNorm(hidden_size, config.rms_norm_eps)
30
+ self.self_attn = GatedDeltaNet2Attention(
31
+ hidden_size=hidden_size,
32
+ num_heads=num_heads,
33
+ num_kv_heads=num_kv_heads,
34
+ head_dim=head_dim,
35
+ chunk_size=config.chunk_size,
36
+ attention_dropout=config.attention_dropout,
37
+ layer_idx=layer_idx,
38
+ use_dual_path=config.use_dual_path,
39
+ softmax_init_alpha=config.softmax_init_alpha,
40
+ use_short_conv=config.use_short_conv,
41
+ conv_kernel_size=config.conv_kernel_size,
42
+ max_position_embeddings=config.max_position_embeddings,
43
+ rope_theta=config.rope_theta,
44
+ rope_scaling_factor=config.rope_scaling_factor,
45
+ rope_original_max_position=config.rope_original_max_position,
46
+ rope_beta_fast=config.rope_beta_fast,
47
+ rope_beta_slow=config.rope_beta_slow,
48
+ )
49
+ self.post_attn_norm = RMSNorm(hidden_size, config.rms_norm_eps)
50
+ self.mlp = SwiGLUMLP(hidden_size, intermediate_size)
51
+
52
+ def forward(
53
+ self,
54
+ x: torch.Tensor,
55
+ attention_mask: Optional[torch.Tensor] = None,
56
+ position_ids: Optional[torch.Tensor] = None,
57
+ loop_idx: Optional[int] = None,
58
+ lora_deltas: Optional[dict] = None,
59
+ ) -> torch.Tensor:
60
+ # Pre-norm + Attention + Residual
61
+ residual = x
62
+ x = self.input_norm(x)
63
+ x = self.self_attn(
64
+ x,
65
+ attention_mask=attention_mask,
66
+ position_ids=position_ids,
67
+ loop_idx=loop_idx,
68
+ lora_deltas=lora_deltas,
69
+ )
70
+ x = residual + x
71
+
72
+ # Pre-norm + MLP + Residual
73
+ residual = x
74
+ x = self.post_attn_norm(x)
75
+ x = self.mlp(x, lora_deltas=lora_deltas)
76
+ x = residual + x
77
+
78
+ return x
79
+
80
+
81
+ def make_narrow_a_layer(config, layer_idx: int = 0) -> DeepXLayer:
82
+ """Create NarrowA layer (8h, 1kv, MLP 6144)."""
83
+ return DeepXLayer(
84
+ hidden_size=config.hidden_size,
85
+ num_heads=config.narrow_a_heads,
86
+ num_kv_heads=config.narrow_a_kv_heads,
87
+ head_dim=config.narrow_a_head_dim,
88
+ intermediate_size=config.narrow_a_intermediate,
89
+ config=config,
90
+ layer_idx=layer_idx,
91
+ )
92
+
93
+
94
+ def make_narrow_b_layer(config, layer_idx: int = 0) -> DeepXLayer:
95
+ """Create NarrowB layer (8h, 1kv, MLP 12288)."""
96
+ return DeepXLayer(
97
+ hidden_size=config.hidden_size,
98
+ num_heads=config.narrow_b_heads,
99
+ num_kv_heads=config.narrow_b_kv_heads,
100
+ head_dim=config.narrow_b_head_dim,
101
+ intermediate_size=config.narrow_b_intermediate,
102
+ config=config,
103
+ layer_idx=layer_idx,
104
+ )
105
+
106
+
107
+ def make_wide_a_layer(config, layer_idx: int = 0) -> DeepXLayer:
108
+ """Create WideA layer (16h, 2kv, MLP 6144)."""
109
+ return DeepXLayer(
110
+ hidden_size=config.hidden_size,
111
+ num_heads=config.wide_a_heads,
112
+ num_kv_heads=config.wide_a_kv_heads,
113
+ head_dim=config.wide_a_head_dim,
114
+ intermediate_size=config.wide_a_intermediate,
115
+ config=config,
116
+ layer_idx=layer_idx,
117
+ )
118
+
119
+
120
+ def make_wide_b_layer(config, layer_idx: int = 0) -> DeepXLayer:
121
+ """Create WideB layer (16h, 2kv, MLP 12288)."""
122
+ return DeepXLayer(
123
+ hidden_size=config.hidden_size,
124
+ num_heads=config.wide_b_heads,
125
+ num_kv_heads=config.wide_b_kv_heads,
126
+ head_dim=config.wide_b_head_dim,
127
+ intermediate_size=config.wide_b_intermediate,
128
+ config=config,
129
+ layer_idx=layer_idx,
130
+ )
modeling/hyperloop.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hyperloop Segment v0.7 — Per-Loop LoRA + RoDE for Gated DeltaNet-2.
3
+
4
+ Each loop iteration = [1 Wide + 4 Narrow] layers, with:
5
+ 1. RoDE: Rotary depth signal on Q/K inside attention
6
+ 2. Per-loop LoRA on all projections (Q/K/V/O + MLP gate/up/down)
7
+ 3. Stochastic depth for robustness
8
+
9
+ Two phase types:
10
+ Phase1: WideA(16h, MLP6144) + NarrowA×4(8h, MLP6144)
11
+ Phase2: NarrowB×4(8h, MLP12288) + WideB(16h, MLP12288)
12
+ """
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ from typing import Optional, Dict
17
+
18
+
19
+ class PerLoopLoRA(nn.Module):
20
+ """Per-loop low-rank adaptation for all projections."""
21
+
22
+ def __init__(self, num_loops: int, proj_shapes: Dict[str, tuple], rank: int = 16):
23
+ super().__init__()
24
+ self.num_loops = num_loops
25
+ self.rank = rank
26
+ self.proj_names = list(proj_shapes.keys())
27
+
28
+ for name, (out_dim, in_dim) in proj_shapes.items():
29
+ a_tensors = nn.ParameterList([
30
+ nn.Parameter(torch.zeros(out_dim, rank))
31
+ for _ in range(num_loops)
32
+ ])
33
+ b_tensors = nn.ParameterList([
34
+ nn.Parameter(torch.zeros(rank, in_dim))
35
+ for _ in range(num_loops)
36
+ ])
37
+ setattr(self, f"lora_A_{name}", a_tensors)
38
+ setattr(self, f"lora_B_{name}", b_tensors)
39
+
40
+ def get_delta(self, proj_name: str, loop_idx: int) -> torch.Tensor:
41
+ A = getattr(self, f"lora_A_{proj_name}")[loop_idx]
42
+ B = getattr(self, f"lora_B_{proj_name}")[loop_idx]
43
+ return A @ B
44
+
45
+
46
+ class HyperloopPhase(nn.Module):
47
+ """
48
+ Multi-iteration loop with [Wide + Narrow×4] pattern per iteration.
49
+
50
+ Each iteration:
51
+ 1. Forward through shared_wide (1 pass)
52
+ 2. Forward through shared_narrow × 4 (4 passes)
53
+ Total per iteration: 5 passes
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ config,
59
+ shared_narrow: nn.Module,
60
+ shared_wide: nn.Module,
61
+ num_loops: int,
62
+ narrow_num_heads: int,
63
+ narrow_kv_heads: int,
64
+ narrow_head_dim: int,
65
+ narrow_intermediate: int,
66
+ wide_num_heads: int,
67
+ wide_kv_heads: int,
68
+ wide_head_dim: int,
69
+ wide_intermediate: int,
70
+ wide_first: bool = True, # True: [Wide, Narrow×4], False: [Narrow×4, Wide]
71
+ ):
72
+ super().__init__()
73
+ self.shared_narrow = shared_narrow
74
+ self.shared_wide = shared_wide
75
+ self.num_loops = num_loops
76
+ self.drop_path_rate = config.drop_path_rate
77
+ self.wide_first = wide_first
78
+ H = config.hidden_size
79
+
80
+ # Per-loop LoRA for narrow layers (applied 4× per iteration)
81
+ narrow_proj_shapes = {
82
+ "q_proj": (narrow_num_heads * narrow_head_dim, H),
83
+ "k_proj": (narrow_kv_heads * narrow_head_dim, H),
84
+ "v_proj": (narrow_kv_heads * narrow_head_dim, H),
85
+ "o_proj": (H, narrow_num_heads * narrow_head_dim),
86
+ "gate_proj": (narrow_intermediate, H),
87
+ "up_proj": (narrow_intermediate, H),
88
+ "down_proj": (H, narrow_intermediate),
89
+ }
90
+ # Per-loop LoRA for wide layers (applied 1× per iteration)
91
+ wide_proj_shapes = {
92
+ "q_proj": (wide_num_heads * wide_head_dim, H),
93
+ "k_proj": (wide_kv_heads * wide_head_dim, H),
94
+ "v_proj": (wide_kv_heads * wide_head_dim, H),
95
+ "o_proj": (H, wide_num_heads * wide_head_dim),
96
+ "gate_proj": (wide_intermediate, H),
97
+ "up_proj": (wide_intermediate, H),
98
+ "down_proj": (H, wide_intermediate),
99
+ }
100
+
101
+ # LoRA for each iteration (narrow layers share LoRA within iteration)
102
+ self.narrow_lora = PerLoopLoRA(num_loops, narrow_proj_shapes, config.lora_rank)
103
+ self.wide_lora = PerLoopLoRA(num_loops, wide_proj_shapes, config.lora_rank)
104
+
105
+ def forward(
106
+ self,
107
+ hidden_states: torch.Tensor,
108
+ attention_mask: Optional[torch.Tensor] = None,
109
+ position_ids: Optional[torch.Tensor] = None,
110
+ ) -> torch.Tensor:
111
+
112
+ for i in range(self.num_loops):
113
+ # Stochastic depth
114
+ if self.training and self.drop_path_rate > 0:
115
+ drop_prob = self.drop_path_rate * (i + 1) / self.num_loops
116
+ if torch.rand(1).item() < drop_prob:
117
+ continue
118
+
119
+ # Get LoRA deltas for this iteration
120
+ narrow_deltas = {
121
+ name: self.narrow_lora.get_delta(name, i)
122
+ for name in self.narrow_lora.proj_names
123
+ }
124
+ wide_deltas = {
125
+ name: self.wide_lora.get_delta(name, i)
126
+ for name in self.wide_lora.proj_names
127
+ }
128
+
129
+ if self.wide_first:
130
+ # [Wide, Narrow×4]
131
+ hidden_states = self.shared_wide(
132
+ hidden_states, attention_mask=attention_mask,
133
+ position_ids=position_ids, loop_idx=i, lora_deltas=wide_deltas,
134
+ )
135
+ for _ in range(4):
136
+ hidden_states = self.shared_narrow(
137
+ hidden_states, attention_mask=attention_mask,
138
+ position_ids=position_ids, loop_idx=i, lora_deltas=narrow_deltas,
139
+ )
140
+ else:
141
+ # [Narrow×4, Wide]
142
+ for _ in range(4):
143
+ hidden_states = self.shared_narrow(
144
+ hidden_states, attention_mask=attention_mask,
145
+ position_ids=position_ids, loop_idx=i, lora_deltas=narrow_deltas,
146
+ )
147
+ hidden_states = self.shared_wide(
148
+ hidden_states, attention_mask=attention_mask,
149
+ position_ids=position_ids, loop_idx=i, lora_deltas=wide_deltas,
150
+ )
151
+
152
+ return hidden_states
modeling/mamba2_block.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mamba2 Block wrapper for hybrid architecture.
3
+
4
+ Mamba2 improves on Mamba1 with:
5
+ - Multi-head SSM (similar to multi-head attention)
6
+ - Larger effective state size
7
+ - Hardware-efficient SSD (Structured State Space Duality) algorithm
8
+ - 2-8x faster than Mamba1
9
+
10
+ For embedding (bidirectional), we run Mamba2 in both directions
11
+ and combine the outputs.
12
+ """
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from .utils import RMSNorm
18
+
19
+ try:
20
+ from mamba_ssm import Mamba2 as Mamba2Core
21
+ MAMBA2_AVAILABLE = True
22
+ except ImportError:
23
+ MAMBA2_AVAILABLE = False
24
+
25
+
26
+ class Mamba2Fallback(nn.Module):
27
+ """
28
+ Pure PyTorch fallback when mamba_ssm is not installed.
29
+ Implements simplified SSM: y = SSM(Conv1d(Linear(x)))
30
+ Not as fast as CUDA kernel but functionally equivalent.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ d_model: int,
36
+ d_state: int = 128,
37
+ d_conv: int = 4,
38
+ expand: int = 2,
39
+ headdim: int = 64,
40
+ ngroups: int = 8,
41
+ ):
42
+ super().__init__()
43
+ self.d_model = d_model
44
+ self.d_inner = d_model * expand
45
+ self.d_state = d_state
46
+ self.d_conv = d_conv
47
+ self.headdim = headdim
48
+ self.nheads = self.d_inner // headdim
49
+ self.ngroups = ngroups
50
+
51
+ assert self.d_inner % headdim == 0, (
52
+ f"d_inner ({self.d_inner}) must be divisible by headdim ({headdim})"
53
+ )
54
+ assert self.nheads % ngroups == 0, (
55
+ f"nheads ({self.nheads}) must be divisible by ngroups ({ngroups})"
56
+ )
57
+
58
+ # Input projection: x → (z, x_ssm)
59
+ self.in_proj = nn.Linear(d_model, self.d_inner * 2, bias=False)
60
+
61
+ # Conv1d for local context
62
+ self.conv1d = nn.Conv1d(
63
+ self.d_inner, self.d_inner,
64
+ kernel_size=d_conv, padding=d_conv - 1,
65
+ groups=self.d_inner, bias=True,
66
+ )
67
+
68
+ # SSM parameters
69
+ self.dt_proj = nn.Linear(self.d_inner, self.nheads, bias=True)
70
+ # A must be negative for SSM stability: A = -exp(A_log)
71
+ # Init A_log ~ log(uniform(1, 16)) per Mamba paper
72
+ A_init = torch.log(torch.rand(self.nheads) * 15 + 1) # log(U(1,16))
73
+ self.A_log = nn.Parameter(A_init)
74
+ self.D = nn.Parameter(torch.ones(self.nheads))
75
+
76
+ # B, C projections (input-dependent)
77
+ self.B_proj = nn.Linear(self.d_inner, self.ngroups * d_state, bias=False)
78
+ self.C_proj = nn.Linear(self.d_inner, self.ngroups * d_state, bias=False)
79
+
80
+ # Output projection
81
+ self.out_proj = nn.Linear(self.d_inner, d_model, bias=False)
82
+ self.norm = nn.LayerNorm(self.d_inner)
83
+
84
+ def _ssm_scan(self, x, dt, A, B, C, D):
85
+ """
86
+ Parallel Associative Scan implementation of SSD (Structured State Space Duality).
87
+ Bypasses the slow O(T) Python loop and avoids Blackwell Triton hangs.
88
+ """
89
+ orig_dtype = x.dtype
90
+ x = x.float()
91
+ dt = dt.float()
92
+ A = A.float()
93
+ B = B.float()
94
+ C = C.float()
95
+
96
+ B_seq, T, d_inner = x.shape
97
+ nheads = self.nheads
98
+ headdim = self.headdim
99
+
100
+ x = x.view(B_seq, T, nheads, headdim)
101
+ dt = F.softplus(dt) # (B, T, nheads)
102
+ A = -torch.exp(A) # (nheads,)
103
+
104
+ # 1. Compute dA: A_bar[t] = dt[t] * A
105
+ log_dA = dt.unsqueeze(-1) * A.view(1, 1, nheads, 1) # (B, T, H, 1)
106
+
107
+ # 2. Compute cumulative product of dA using cumsum of logs
108
+ # M[t, i] = prod_{j=i+1}^t exp(log_dA[j]) = exp(cumsum(log_dA)[t] - cumsum(log_dA)[i])
109
+ cum_log_dA = torch.cumsum(log_dA, dim=1) # (B, T, H, 1)
110
+
111
+ # clg: (B, H, T, 1)
112
+ clg = cum_log_dA.permute(0, 2, 1, 3)
113
+ # diff[t, i] = cum_log_dA[t] - cum_log_dA[i] -> (B, H, T, T)
114
+ diff = clg - clg.transpose(-1, -2)
115
+ decay_matrix = torch.exp(diff)
116
+
117
+ # Causal mask
118
+ m_idx = torch.arange(T, device=x.device)
119
+ mask = (m_idx.view(-1, 1) >= m_idx.view(1, -1)).to(x.dtype)
120
+ decay_matrix = decay_matrix * mask.view(1, 1, T, T)
121
+
122
+ # 3. Compute B_bar * x: (B, T, H, S) * (B, T, H, D) -> states
123
+ B = B.view(B_seq, T, self.ngroups, self.d_state)
124
+ heads_per_group = nheads // self.ngroups
125
+ B = B.repeat_interleave(heads_per_group, dim=2) # (B, T, H, S)
126
+
127
+ # dB_x: (B, T, H, S, D)
128
+ dB = dt.unsqueeze(-1) * B # (B, T, H, S)
129
+ bx = torch.einsum("bths,bthd->bthsd", dB, x)
130
+
131
+ # 4. Apply decay matrix to states: h_t = sum_{i=0}^t M[t, i] * (dB_i * x_i)
132
+ # bx_flat: (B, H, T, S*D)
133
+ bx_flat = bx.permute(0, 2, 1, 3, 4).reshape(B_seq, nheads, T, -1)
134
+ h_flat = torch.einsum("bhti,bhis->bhts", decay_matrix, bx_flat)
135
+ h = h_flat.view(B_seq, nheads, T, self.d_state, headdim)
136
+
137
+ # 5. Output: y_t = C_t * h_t
138
+ C = C.view(B_seq, T, self.ngroups, self.d_state)
139
+ C = C.repeat_interleave(heads_per_group, dim=2) # (B, T, H, S)
140
+
141
+ # h_perm: (B, T, H, S, D)
142
+ h_perm = h.permute(0, 2, 1, 3, 4)
143
+ # y: (B, T, H, D)
144
+ y = torch.einsum("bths,bthsd->bthd", C, h_perm)
145
+
146
+ # Add D skip connection
147
+ y = y + D.view(1, 1, nheads, 1) * x
148
+
149
+ return y.reshape(B_seq, T, d_inner).to(orig_dtype)
150
+
151
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
152
+ if not hasattr(self, '_warned') and x.shape[1] > 512:
153
+ import warnings
154
+ warnings.warn(
155
+ f"Mamba2Fallback: sequential SSM scan with seq_len={x.shape[1]} will be slow. "
156
+ f"Install mamba_ssm for CUDA-accelerated scan.",
157
+ stacklevel=2,
158
+ )
159
+ self._warned = True
160
+
161
+ B, T, D = x.shape
162
+
163
+ # Input projection
164
+ xz = self.in_proj(x) # (B, T, 2*d_inner)
165
+ x_ssm, z = xz.chunk(2, dim=-1)
166
+
167
+ # Conv1d
168
+ x_conv = x_ssm.transpose(1, 2) # (B, d_inner, T)
169
+ x_conv = self.conv1d(x_conv)[:, :, :T] # trim padding
170
+ x_conv = x_conv.transpose(1, 2) # (B, T, d_inner)
171
+ x_conv = F.silu(x_conv)
172
+
173
+ # SSM parameters from input
174
+ dt = self.dt_proj(x_conv)
175
+ B_param = self.B_proj(x_conv)
176
+ C_param = self.C_proj(x_conv)
177
+
178
+ # SSM scan
179
+ y = self._ssm_scan(x_conv, dt, self.A_log, B_param, C_param, self.D)
180
+
181
+ # Gate with z
182
+ y = y * F.silu(z)
183
+ y = self.out_proj(self.norm(y))
184
+
185
+ return y
186
+
187
+
188
+ class Mamba2Block(nn.Module):
189
+ """
190
+ Bidirectional Mamba2 block for embedding.
191
+ Runs Mamba2 forward + backward, combines outputs.
192
+ """
193
+
194
+ def __init__(
195
+ self,
196
+ d_model: int,
197
+ d_state: int = 128,
198
+ d_conv: int = 4,
199
+ expand: int = 2,
200
+ headdim: int = 64,
201
+ ngroups: int = 8,
202
+ bidirectional: bool = True,
203
+ ):
204
+ super().__init__()
205
+ self.bidirectional = bidirectional
206
+
207
+ mamba_cls = Mamba2Core if MAMBA2_AVAILABLE else Mamba2Fallback
208
+ mamba_kwargs = dict(
209
+ d_model=d_model,
210
+ d_state=d_state,
211
+ d_conv=d_conv,
212
+ expand=expand,
213
+ headdim=headdim,
214
+ ngroups=ngroups,
215
+ )
216
+
217
+ self.forward_mamba = mamba_cls(**mamba_kwargs)
218
+
219
+ if bidirectional:
220
+ self.backward_mamba = mamba_cls(**mamba_kwargs)
221
+ self.merge_proj = nn.Linear(d_model * 2, d_model, bias=False)
222
+
223
+ def forward(self, x: torch.Tensor, attention_mask: torch.Tensor = None) -> torch.Tensor:
224
+ mask = None
225
+ if attention_mask is not None:
226
+ mask = attention_mask.unsqueeze(-1).to(x.dtype)
227
+ x = x * mask
228
+
229
+ fwd_out = self.forward_mamba(x)
230
+
231
+ if self.bidirectional:
232
+ x_flip = x.flip(dims=[1])
233
+ if mask is not None:
234
+ x_flip = x_flip * mask.flip(dims=[1])
235
+ bwd_out = self.backward_mamba(x_flip).flip(dims=[1])
236
+ out = self.merge_proj(torch.cat([fwd_out, bwd_out], dim=-1))
237
+ else:
238
+ out = fwd_out
239
+
240
+ # Re-apply mask: SSM hidden state decay để lại artifact tại padding positions
241
+ if mask is not None:
242
+ out = out * mask
243
+
244
+ return out
modeling/pipeline.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DeepX v0.6 Full Pipeline.
3
+
4
+ Combines:
5
+ 1. Frozen Gemma 4 E2B token embedding (loaded from pretrained/gemma4_e2b_embed.pt)
6
+ 2. Pure GLA Hyperloop backbone + ColBERT head (PureGLAEmbeddingModel)
7
+
8
+ Outputs:
9
+ - encode() → single vector (1536-d) for fast ANN retrieval
10
+ - encode_colbert() → token vectors (T × 128-d) for MaxSim reranking
11
+ - encode_multi() → both single + token vectors in one forward pass
12
+
13
+ Weight Init: ~90% of backbone can be copied from Gemma 4 E2B.
14
+ """
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+ import logging
19
+ import dataclasses
20
+ from typing import Optional, Tuple
21
+ from pathlib import Path
22
+
23
+ from config import HybridEmbeddingConfig
24
+ from .embedding_model import DeepXEmbeddingModel
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class DeepXPipeline(nn.Module):
30
+ """
31
+ Full DeepX v0.6 embedding pipeline.
32
+
33
+ Token embedding is frozen and loaded from a pre-extracted file.
34
+ Only the backbone (PureGLAEmbeddingModel) is trained.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ config: HybridEmbeddingConfig,
40
+ embed_path: str = "pretrained/gemma4_e2b_embed.pt",
41
+ ):
42
+ super().__init__()
43
+ self.config = config
44
+
45
+ # --- Frozen Token Embedding (Gemma 4 E2B) ---
46
+ embed_path = Path(embed_path)
47
+ if not embed_path.exists():
48
+ raise FileNotFoundError(
49
+ f"Token embedding not found at '{embed_path}'.\n"
50
+ f"Please run: python scripts/extract_gemma_embedding.py"
51
+ )
52
+
53
+ logger.info(f"Loading frozen token embedding from {embed_path} ...")
54
+ weight = torch.load(embed_path, weights_only=True)
55
+
56
+ assert weight.shape == (config.vocab_size, config.hidden_size), (
57
+ f"Embedding shape mismatch: expected ({config.vocab_size}, {config.hidden_size}), "
58
+ f"got {tuple(weight.shape)}. Check config.hidden_size matches E2B."
59
+ )
60
+
61
+ self.token_embedding = nn.Embedding(config.vocab_size, config.hidden_size)
62
+ self.token_embedding.weight.data = weight.to(config.torch_dtype)
63
+ self.token_embedding.requires_grad_(False)
64
+ logger.info(f"Token embedding frozen. Shape: {weight.shape}, dtype: {config.torch_dtype}")
65
+
66
+ # --- Trainable Backbone ---
67
+ self.backbone = DeepXEmbeddingModel(config)
68
+
69
+ def forward(
70
+ self,
71
+ input_ids: torch.Tensor,
72
+ attention_mask: Optional[torch.Tensor] = None,
73
+ normalize: bool = True,
74
+ truncate_dim: Optional[int] = None,
75
+ return_colbert: bool = False,
76
+ ):
77
+ """
78
+ Full forward pass.
79
+
80
+ Returns:
81
+ If return_colbert=False: single_embed (B, D)
82
+ If return_colbert=True: (single_embed (B, D), token_embeds (B, T, colbert_dim))
83
+ """
84
+ with torch.no_grad():
85
+ hidden_states = self.token_embedding(input_ids)
86
+
87
+ return self.backbone(
88
+ hidden_states,
89
+ attention_mask=attention_mask,
90
+ normalize=normalize,
91
+ truncate_dim=truncate_dim,
92
+ return_colbert=return_colbert,
93
+ )
94
+
95
+ def encode(
96
+ self,
97
+ input_ids: torch.Tensor,
98
+ attention_mask: Optional[torch.Tensor] = None,
99
+ truncate_dim: Optional[int] = None,
100
+ ) -> torch.Tensor:
101
+ """Single vector encoding for fast ANN retrieval."""
102
+ with torch.no_grad():
103
+ return self.forward(input_ids, attention_mask, normalize=True, truncate_dim=truncate_dim)
104
+
105
+ def encode_colbert(
106
+ self,
107
+ input_ids: torch.Tensor,
108
+ attention_mask: Optional[torch.Tensor] = None,
109
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
110
+ """
111
+ ColBERT encoding — returns both single vector and token vectors.
112
+
113
+ Returns:
114
+ single_embed: (B, 1536) for coarse retrieval
115
+ token_embeds: (B, T, 128) for MaxSim reranking
116
+ """
117
+ with torch.no_grad():
118
+ return self.forward(input_ids, attention_mask, normalize=True, return_colbert=True)
119
+
120
+ def encode_multi(
121
+ self,
122
+ input_ids: torch.Tensor,
123
+ attention_mask: Optional[torch.Tensor] = None,
124
+ truncate_dim: Optional[int] = None,
125
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
126
+ """Alias for encode_colbert with optional truncation on single vector."""
127
+ with torch.no_grad():
128
+ hidden_states = self.token_embedding(input_ids)
129
+ return self.backbone(
130
+ hidden_states,
131
+ attention_mask=attention_mask,
132
+ normalize=True,
133
+ truncate_dim=truncate_dim,
134
+ return_colbert=True,
135
+ )
136
+
137
+ def freeze_embedder(self):
138
+ """Ensure token embedding stays frozen."""
139
+ self.token_embedding.requires_grad_(False)
140
+
141
+ def unfreeze_embedder(self):
142
+ """Unfreeze token embedding for fine-tuning (use with very small LR ~1e-6)."""
143
+ self.token_embedding.requires_grad_(True)
144
+ logger.warning("Token embedding UNFROZEN. Use lr ~1e-6.")
145
+
146
+ def count_parameters(self) -> dict:
147
+ embed_params = self.token_embedding.weight.numel()
148
+ backbone_counts = self.backbone.count_parameters()
149
+ return {
150
+ "embedding_frozen": embed_params,
151
+ "backbone_trainable": backbone_counts["trainable"],
152
+ "backbone_total": backbone_counts["backbone_total"],
153
+ "grand_total": embed_params + backbone_counts["backbone_total"],
154
+ }
155
+
156
+ def save_backbone(self, path: str) -> None:
157
+ """Save only the trained backbone weights."""
158
+ out = Path(path)
159
+ out.parent.mkdir(parents=True, exist_ok=True)
160
+ torch.save({
161
+ "state_dict": self.backbone.state_dict(),
162
+ "config": dataclasses.asdict(self.config),
163
+ "version": "0.7",
164
+ "architecture": "gdn2_hyperloop_colbert",
165
+ "embed_source": "gemma4_e2b",
166
+ "hidden_size": self.config.hidden_size,
167
+ "vocab_size": self.config.vocab_size,
168
+ "colbert_dim": self.config.colbert_dim,
169
+ }, out)
170
+ size_mb = out.stat().st_size / 1024 / 1024
171
+ logger.info(f"Backbone saved to {out} ({size_mb:.1f} MB)")
172
+
173
+ @classmethod
174
+ def from_pretrained(
175
+ cls,
176
+ config: HybridEmbeddingConfig,
177
+ embed_path: str,
178
+ backbone_path: str,
179
+ ) -> "DeepXPipeline":
180
+ """Load pipeline from 2 .pt files for deployment."""
181
+ backbone_path = Path(backbone_path)
182
+ if not backbone_path.exists():
183
+ raise FileNotFoundError(f"Backbone weights not found at '{backbone_path}'.")
184
+
185
+ pipeline = cls(config, embed_path=embed_path)
186
+
187
+ logger.info(f"Loading backbone from {backbone_path} ...")
188
+ checkpoint = torch.load(backbone_path, weights_only=True, map_location="cpu")
189
+
190
+ if isinstance(checkpoint, dict) and "state_dict" in checkpoint:
191
+ state_dict = checkpoint["state_dict"]
192
+ else:
193
+ state_dict = checkpoint
194
+
195
+ pipeline.backbone.load_state_dict(state_dict)
196
+ pipeline.backbone.to(config.torch_dtype)
197
+ logger.info("Backbone loaded successfully.")
198
+
199
+ return pipeline
modeling/utils.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+ from typing import Optional, Tuple
6
+
7
+
8
+ class RMSNorm(nn.Module):
9
+ def __init__(self, hidden_size: int, eps: float = 1e-6):
10
+ super().__init__()
11
+ self.weight = nn.Parameter(torch.ones(hidden_size))
12
+ self.eps = eps
13
+
14
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
15
+ input_dtype = x.dtype
16
+ x = x.float()
17
+ variance = x.pow(2).mean(-1, keepdim=True)
18
+ x = x * torch.rsqrt(variance + self.eps)
19
+ return (self.weight.float() * x).to(input_dtype)
20
+
21
+
22
+ class SwiGLUMLP(nn.Module):
23
+ def __init__(self, hidden_size: int, intermediate_size: int):
24
+ super().__init__()
25
+ self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
26
+ self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
27
+ self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
28
+
29
+ def forward(self, x: torch.Tensor, lora_deltas: dict = None) -> torch.Tensor:
30
+ if lora_deltas:
31
+ gate = F.linear(x, self.gate_proj.weight + lora_deltas.get("gate_proj", 0))
32
+ up = F.linear(x, self.up_proj.weight + lora_deltas.get("up_proj", 0))
33
+ down_weight = self.down_proj.weight + lora_deltas.get("down_proj", 0)
34
+ return F.linear(F.silu(gate) * up, down_weight)
35
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
36
+
37
+
38
+ def _yarn_find_correction_dim(
39
+ num_rotations: int, dim: int, base: float = 10000.0, max_position: int = 2048
40
+ ) -> float:
41
+ """Find correction dimension for YaRN interpolation."""
42
+ return (dim * math.log(max_position / (num_rotations * 2 * math.pi))) / (2 * math.log(base))
43
+
44
+
45
+ def _yarn_find_correction_range(
46
+ low_rot: int, high_rot: int, dim: int, base: float = 10000.0, max_position: int = 2048
47
+ ) -> Tuple[int, int]:
48
+ """Find the range of dimensions to apply YaRN correction."""
49
+ low = math.floor(_yarn_find_correction_dim(low_rot, dim, base, max_position))
50
+ high = math.ceil(_yarn_find_correction_dim(high_rot, dim, base, max_position))
51
+ return max(low, 0), min(high, dim - 1)
52
+
53
+
54
+ def _yarn_linear_ramp_mask(low: int, high: int, dim: int, dtype: torch.dtype) -> torch.Tensor:
55
+ """Create linear ramp mask for smooth interpolation between dimensions."""
56
+ if low == high:
57
+ high += 0.001
58
+ linear_func = (torch.arange(dim, dtype=dtype) - low) / (high - low)
59
+ return linear_func.clamp(0, 1)
60
+
61
+
62
+ class YaRNRotaryEmbedding(nn.Module):
63
+ """
64
+ RoPE with YaRN (Yet another RoPE extensioN) scaling.
65
+
66
+ Same approach as Gemma 4 E2B:
67
+ - Base theta = 1,000,000
68
+ - YaRN scaling for context extension to 128K
69
+ - Splits dimensions into 3 regions:
70
+ 1. Low freq dims: apply NTK-aware interpolation
71
+ 2. Medium freq dims: smooth ramp between interpolation and extrapolation
72
+ 3. High freq dims: no scaling (extrapolation)
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ dim: int,
78
+ max_position_embeddings: int = 131072,
79
+ base: float = 1000000.0,
80
+ scaling_factor: float = 4.0,
81
+ original_max_position: int = 32768,
82
+ beta_fast: int = 32,
83
+ beta_slow: int = 1,
84
+ ):
85
+ super().__init__()
86
+ self.dim = dim
87
+ self.max_position_embeddings = max_position_embeddings
88
+ self.base = base
89
+ self.scaling_factor = scaling_factor
90
+ self.original_max_position = original_max_position
91
+ self.beta_fast = beta_fast
92
+ self.beta_slow = beta_slow
93
+
94
+ self._build_yarn_cache()
95
+
96
+ def _build_yarn_cache(self):
97
+ """Compute YaRN-adjusted inverse frequencies."""
98
+ dim = self.dim
99
+ # Standard RoPE inverse frequencies
100
+ inv_freq = 1.0 / (
101
+ self.base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)
102
+ )
103
+
104
+ # YaRN correction
105
+ low, high = _yarn_find_correction_range(
106
+ self.beta_slow, self.beta_fast, dim, self.base, self.original_max_position
107
+ )
108
+ inv_freq_mask = 1.0 - _yarn_linear_ramp_mask(low, high, dim // 2, torch.float32)
109
+
110
+ # Interpolated frequencies (for extending context)
111
+ inv_freq_interpolated = inv_freq / self.scaling_factor
112
+
113
+ # Blend: high freq dims keep original, low freq dims get interpolated
114
+ inv_freq_yarn = inv_freq_interpolated * (1 - inv_freq_mask) + inv_freq * inv_freq_mask
115
+
116
+ self.register_buffer("inv_freq", inv_freq_yarn, persistent=False)
117
+
118
+ # Attention scaling factor (magnitude correction)
119
+ self.attn_scale = 0.1 * math.log(self.scaling_factor) + 1.0
120
+
121
+ def forward(
122
+ self, x: torch.Tensor, position_ids: Optional[torch.Tensor] = None
123
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
124
+ """
125
+ Args:
126
+ x: (B, H, T, D) — used only for device/dtype
127
+ position_ids: (B, T) or None (auto-generate 0..T-1)
128
+ Returns:
129
+ cos, sin: (B, T, D) in same dtype as x
130
+ """
131
+ B, H, T, D = x.shape
132
+
133
+ if position_ids is None:
134
+ position_ids = torch.arange(T, device=x.device).unsqueeze(0).expand(B, -1)
135
+
136
+ # Compute in float32 for precision, cast output to match x
137
+ inv_freq = self.inv_freq.to(device=x.device, dtype=torch.float32)
138
+ freqs = position_ids.unsqueeze(-1).float() * inv_freq.unsqueeze(0).unsqueeze(0)
139
+
140
+ emb = torch.cat([freqs, freqs], dim=-1)
141
+
142
+ cos = (emb.cos() * self.attn_scale).to(x.dtype)
143
+ sin = (emb.sin() * self.attn_scale).to(x.dtype)
144
+
145
+ return cos, sin
146
+
147
+
148
+ def apply_rotary_pos_emb(
149
+ q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
150
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
151
+ """
152
+ Apply RoPE rotation to Q and K.
153
+
154
+ Args:
155
+ q: (B, H, T, D)
156
+ k: (B, H_kv, T, D)
157
+ cos: (B, T, D)
158
+ sin: (B, T, D)
159
+ Returns:
160
+ q_rotated, k_rotated: same shapes
161
+ """
162
+ # (B, T, D) → (B, 1, T, D) for broadcasting with heads
163
+ cos = cos.unsqueeze(1)
164
+ sin = sin.unsqueeze(1)
165
+
166
+ q_rotated = (q * cos) + (_rotate_half(q) * sin)
167
+ k_rotated = (k * cos) + (_rotate_half(k) * sin)
168
+
169
+ return q_rotated, k_rotated
170
+
171
+
172
+ def _rotate_half(x: torch.Tensor) -> torch.Tensor:
173
+ """Rotate half the hidden dims: [x1, x2] → [-x2, x1]"""
174
+ x1 = x[..., : x.shape[-1] // 2]
175
+ x2 = x[..., x.shape[-1] // 2 :]
176
+ return torch.cat((-x2, x1), dim=-1)
177
+
178
+
179
+ def apply_depth_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
180
+ """
181
+ Apply depth rotation to a subset of dimensions.
182
+ Args:
183
+ x: (B, T, D)
184
+ cos, sin: (1, 1, d_rode) where d_rode is the number of rotated dimensions.
185
+ """
186
+ d_rode = cos.shape[-1]
187
+ x_rode = x[..., :d_rode]
188
+ x_rest = x[..., d_rode:]
189
+
190
+ # Apply rotation to the first d_rode dimensions
191
+ x_rotated = (x_rode * cos) + (_rotate_half(x_rode) * sin)
192
+
193
+ return torch.cat([x_rotated, x_rest], dim=-1)
194
+
195
+
196
+ class RoDE(nn.Module):
197
+ """
198
+ Rotary Depth Embedding (RoDE).
199
+ Provides a depth signal for shared weights in Hyperloop.
200
+ """
201
+ def __init__(self, dim: int, num_loops: int, base: float = 10000.0):
202
+ super().__init__()
203
+ self.dim = dim
204
+ self.num_loops = num_loops
205
+ self.base = base
206
+
207
+ # Pre-compute sin/cos for each loop index
208
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
209
+
210
+ # (num_loops, dim // 2)
211
+ loop_ids = torch.arange(num_loops).float()
212
+ freqs = loop_ids.unsqueeze(-1) * inv_freq.unsqueeze(0)
213
+
214
+ # (num_loops, dim)
215
+ emb = torch.cat([freqs, freqs], dim=-1)
216
+ self.register_buffer("cos", emb.cos(), persistent=False)
217
+ self.register_buffer("sin", emb.sin(), persistent=False)
218
+
219
+ def forward(self, x: torch.Tensor, loop_idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
220
+ # Pick the pre-computed sin/cos for the current loop index
221
+ # Shape: (1, 1, dim) for broadcasting
222
+ cos = self.cos[loop_idx].view(1, 1, -1).to(x.dtype)
223
+ sin = self.sin[loop_idx].view(1, 1, -1).to(x.dtype)
224
+ return cos, sin