Upload folder using huggingface_hub
Browse files- modeling/__init__.py +5 -0
- modeling/__pycache__/__init__.cpython-310.pyc +0 -0
- modeling/__pycache__/__init__.cpython-311.pyc +0 -0
- modeling/__pycache__/embedding_model.cpython-310.pyc +0 -0
- modeling/__pycache__/embedding_model.cpython-311.pyc +0 -0
- modeling/__pycache__/gdn2_attention.cpython-310.pyc +0 -0
- modeling/__pycache__/gdn2_attention.cpython-311.pyc +0 -0
- modeling/__pycache__/gla_attention.cpython-311.pyc +0 -0
- modeling/__pycache__/hybrid_layer.cpython-310.pyc +0 -0
- modeling/__pycache__/hybrid_layer.cpython-311.pyc +0 -0
- modeling/__pycache__/hyperloop.cpython-310.pyc +0 -0
- modeling/__pycache__/hyperloop.cpython-311.pyc +0 -0
- modeling/__pycache__/mamba2_block.cpython-311.pyc +0 -0
- modeling/__pycache__/pipeline.cpython-310.pyc +0 -0
- modeling/__pycache__/pipeline.cpython-311.pyc +0 -0
- modeling/__pycache__/utils.cpython-310.pyc +0 -0
- modeling/__pycache__/utils.cpython-311.pyc +0 -0
- modeling/embedding_model.py +200 -0
- modeling/gdn2_attention.py +411 -0
- modeling/gla_attention.py +381 -0
- modeling/hybrid_layer.py +130 -0
- modeling/hyperloop.py +152 -0
- modeling/mamba2_block.py +244 -0
- modeling/pipeline.py +199 -0
- modeling/utils.py +224 -0
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
|