Download amor.py from FlyinGodzilla/AMOR-GatedDeltaNet-440M: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/FlyinGodzilla/AMOR-GatedDeltaNet-440M/resolve/main/amor.py
- Command line
-
hf download hf://FlyinGodzilla/AMOR-GatedDeltaNet-440M/amor.py
-
curl -L -o amor.py https://huggingface.co/FlyinGodzilla/AMOR-GatedDeltaNet-440M/resolve/main/amor.py
25.4 kB
| """ | |
| AMOR: Adaptive Metacognitive Output Router. | |
| A post-hoc hybrid architecture that gates attention by output prediction entropy. | |
| A recurrent backbone (Mamba-2 or Gated DeltaNet) is followed by K stacked AMOR | |
| blocks; each block measures the entropy of softmax(lm_head(h)) and, where the | |
| backbone is uncertain, fires causal self-attention to refine the residual stream. | |
| Input -> [Backbone Mixer + SwiGLU MLP] x N -> [AMOR Block] x K -> Norm -> LM Head | |
| | | |
| Entropy Gate (fire if uncertain) | |
| | | |
| Attention (Q,K,V from residual) | |
| The 6 architecture variants are flag combinations of a single class: | |
| backbone in {'mamba2', 'gdn'} # recurrent mixer | |
| n_amor_blocks in {1, 3} # stack depth | |
| residual_mode in {'classic', 'h'} # block-0 pre-norm placement | |
| K, V are standard linear projections of the residual stream (not SSM internal | |
| state). Training uses dense attention with output masking ('full_with_mask'); | |
| 'true_sparse' is provided for inference. Each block has its own EntropyGate | |
| (EMA-tracked threshold), Q/K/V/O, RoPE on Q/K, and a per-channel alpha fusion. | |
| """ | |
| import math | |
| from typing import Tuple, Dict, Any, Optional | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| try: | |
| import mamba_ssm | |
| if not hasattr(mamba_ssm, 'mamba_chunk_scan_combined'): | |
| from mamba_ssm.ops.triton.ssd_combined import ( | |
| mamba_chunk_scan_combined, | |
| mamba_split_conv1d_scan_combined, | |
| ) | |
| mamba_ssm.mamba_chunk_scan_combined = mamba_chunk_scan_combined | |
| mamba_ssm.mamba_split_conv1d_scan_combined = mamba_split_conv1d_scan_combined | |
| except ImportError: | |
| pass | |
| try: | |
| from transformers import Mamba2Config, Mamba2ForCausalLM | |
| except ImportError: | |
| Mamba2Config = None | |
| Mamba2ForCausalLM = None | |
| try: | |
| from fla.layers import GatedDeltaNet as FLAGatedDeltaNet | |
| except ImportError: | |
| FLAGatedDeltaNet = None | |
| # ============================================================================= | |
| # Building blocks | |
| # ============================================================================= | |
| class RMSNorm(nn.Module): | |
| def __init__(self, d_model: int, eps: float = 1e-6): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.ones(d_model)) | |
| self.eps = eps | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| rms = torch.sqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + self.eps) | |
| return self.weight * x / rms | |
| class SwiGLUMLP(nn.Module): | |
| def __init__(self, d_model: int, d_ff: int): | |
| super().__init__() | |
| self.gate_proj = nn.Linear(d_model, d_ff, bias=False) | |
| self.up_proj = nn.Linear(d_model, d_ff, bias=False) | |
| self.down_proj = nn.Linear(d_ff, d_model, bias=False) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) | |
| class RotaryPositionEmbedding(nn.Module): | |
| def __init__(self, head_dim: int, max_seq_len: int = 8192, base: float = 10000.0): | |
| super().__init__() | |
| self.head_dim = head_dim | |
| inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim)) | |
| self.register_buffer('inv_freq', inv_freq, persistent=False) | |
| self._build_cache(max_seq_len) | |
| def _build_cache(self, seq_len: int): | |
| t = torch.arange(seq_len, dtype=self.inv_freq.dtype, device=self.inv_freq.device) | |
| freqs = torch.outer(t, self.inv_freq) | |
| emb = torch.cat([freqs, freqs], dim=-1) | |
| self.register_buffer('cos_cached', emb.cos(), persistent=False) | |
| self.register_buffer('sin_cached', emb.sin(), persistent=False) | |
| def forward(self, q: torch.Tensor, k: torch.Tensor, offset: int = 0): | |
| seq_len = q.shape[2] | |
| cos = self.cos_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0) | |
| sin = self.sin_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0) | |
| return self._apply_rotary(q, cos, sin), self._apply_rotary(k, cos, sin) | |
| def _apply_rotary(x, cos, sin): | |
| d = x.shape[-1] | |
| x1, x2 = x[..., :d // 2], x[..., d // 2:] | |
| return x * cos + torch.cat([-x2, x1], dim=-1) * sin | |
| class KVProjection(nn.Module): | |
| def __init__(self, d_model: int, n_heads: int = 12): | |
| super().__init__() | |
| self.d_model = d_model | |
| self.n_heads = n_heads | |
| self.head_dim = d_model // n_heads | |
| self.k_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.v_proj = nn.Linear(d_model, d_model, bias=False) | |
| def forward(self, h: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: | |
| return self.k_proj(h), self.v_proj(h) | |
| # ============================================================================= | |
| # Entropy gate | |
| # ============================================================================= | |
| class EntropyGate(nn.Module): | |
| """ | |
| Hard binary gate driven by output-level prediction entropy. | |
| Threshold is an EMA of the batch entropy median (no gradient). Two offset | |
| modes: 'fixed' adds a constant; 'adaptive' adds offset_k * EMA(std), giving | |
| a scale-independent fire rate as the entropy distribution width changes | |
| across model scale and training stage. | |
| entropy = -sum(p * log(p)) / log(vocab_size) | |
| threshold = EMA(median) + offset # fixed | |
| threshold = EMA(median) + offset_k * EMA(std) # adaptive | |
| gate = (entropy > threshold) # hard binary | |
| """ | |
| def __init__( | |
| self, | |
| vocab_size: int, | |
| init_threshold: float = 0.3, | |
| ema_momentum: float = 0.99, | |
| offset: float = 0.03, | |
| offset_mode: str = 'adaptive', | |
| offset_k: float = 0.2, | |
| ): | |
| super().__init__() | |
| self.max_entropy = math.log(vocab_size) | |
| self.ema_momentum = ema_momentum | |
| self.offset = offset | |
| self.offset_mode = offset_mode | |
| self.offset_k = offset_k | |
| self.register_buffer('running_threshold', torch.tensor(init_threshold)) | |
| self.register_buffer('running_std', torch.tensor(0.1)) | |
| self.router = None | |
| def forward(self, logits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: | |
| # Detached: entropy drives gating decisions only. CE loss trains the | |
| # backbone via the final logits path, never through the gate. | |
| if self.router is not None: | |
| if self.training: | |
| raise RuntimeError("Fitted routers are for inference; detach them before training.") | |
| with torch.no_grad(): | |
| entropy = self.router(logits.detach()) | |
| else: | |
| logits_f32 = logits.detach().float() | |
| probs = F.softmax(logits_f32, dim=-1) | |
| raw_entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1) | |
| entropy = raw_entropy / self.max_entropy | |
| if self.training: | |
| entropy_detached = entropy.detach() | |
| batch_median = entropy_detached.median() | |
| self.running_threshold.mul_(self.ema_momentum).add_( | |
| batch_median, alpha=1.0 - self.ema_momentum | |
| ) | |
| if self.offset_mode == 'adaptive': | |
| batch_std = entropy_detached.std() | |
| self.running_std.mul_(self.ema_momentum).add_( | |
| batch_std, alpha=1.0 - self.ema_momentum | |
| ) | |
| if self.offset_mode == 'adaptive': | |
| threshold = self.running_threshold + self.offset_k * self.running_std | |
| else: | |
| threshold = self.running_threshold + self.offset | |
| gate = (entropy > threshold).float() | |
| return gate, entropy | |
| # ============================================================================= | |
| # AMOR block | |
| # ============================================================================= | |
| class AMORBlock(nn.Module): | |
| """ | |
| Entropy-gated attention block. | |
| gate = EntropyGate(lm_head(h_normed)) | |
| attn_out = Attention(h_normed) * gate | |
| output = h_normed + alpha * attn_out | |
| Attention modes: | |
| 'full_with_mask': dense attention over all positions, output masked by | |
| the gate. Used for training (GPU-friendly). | |
| 'true_sparse': Q and the attention computation only run for firing | |
| positions. Output is bit-identical given the same | |
| weights/inputs. Used for inference. | |
| Zero-initialized out_proj + per-channel alpha (sigmoid(raw_alpha), clamped | |
| floor of ~0.27) handle output magnitude scaling. RoPE on Q, K. | |
| """ | |
| def __init__( | |
| self, | |
| d_model: int, | |
| vocab_size: int, | |
| n_heads: int = 12, | |
| init_alpha: float = 0.0, | |
| init_threshold: float = 0.3, | |
| gate_offset: float = 0.03, | |
| offset_mode: str = 'adaptive', | |
| offset_k: float = 0.2, | |
| attention_mode: str = 'full_with_mask', | |
| max_seq_len: int = 4096, | |
| ): | |
| super().__init__() | |
| self.d_model = d_model | |
| self.n_heads = n_heads | |
| self.head_dim = d_model // n_heads | |
| self.attention_mode = attention_mode | |
| self.gate = EntropyGate( | |
| vocab_size, | |
| init_threshold=init_threshold, | |
| offset=gate_offset, | |
| offset_mode=offset_mode, | |
| offset_k=offset_k, | |
| ) | |
| self.rope = RotaryPositionEmbedding(self.head_dim, max_seq_len) | |
| self.q_proj = nn.Linear(d_model, d_model, bias=False) | |
| self.kv_proj = KVProjection(d_model, n_heads) | |
| self.out_proj = nn.Linear(d_model, d_model, bias=False) | |
| nn.init.zeros_(self.out_proj.weight) | |
| if self.out_proj.bias is not None: | |
| nn.init.zeros_(self.out_proj.bias) | |
| # Per-channel raw alpha; sigmoid + lower clamp at -1 (≈0.27 floor) to | |
| # prevent collapse. Channels learn their own residual mixing weights. | |
| self.raw_alpha = nn.Parameter(torch.full((d_model,), float(init_alpha))) | |
| def get_alpha(self) -> torch.Tensor: | |
| return torch.sigmoid(torch.clamp(self.raw_alpha, min=-1.0)) | |
| def _attention_full_with_mask(self, h_norm: torch.Tensor, gate: torch.Tensor) -> torch.Tensor: | |
| batch, seq, _ = h_norm.shape | |
| Q = self.q_proj(h_norm).view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) | |
| K, V = self.kv_proj(h_norm) | |
| K = K.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) | |
| V = V.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) | |
| Q, K = self.rope(Q, K) | |
| attn_out = F.scaled_dot_product_attention(Q, K, V, is_causal=True) | |
| attn_out = attn_out.transpose(1, 2).contiguous().view(batch, seq, self.d_model) | |
| attn_out = self.out_proj(attn_out) | |
| return attn_out * gate.unsqueeze(-1) | |
| def _attention_true_sparse(self, h_norm: torch.Tensor, gate: torch.Tensor) -> torch.Tensor: | |
| batch, seq, _ = h_norm.shape | |
| device = h_norm.device | |
| K, V = self.kv_proj(h_norm) | |
| K = K.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) | |
| V = V.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2) | |
| cos_all = self.rope.cos_cached[:seq].unsqueeze(0).unsqueeze(0) | |
| sin_all = self.rope.sin_cached[:seq].unsqueeze(0).unsqueeze(0) | |
| K = RotaryPositionEmbedding._apply_rotary(K, cos_all, sin_all) | |
| active_mask = gate > 0.5 | |
| if not active_mask.any(): | |
| return torch.zeros(batch, seq, self.d_model, device=device, dtype=h_norm.dtype) | |
| active_indices = active_mask.nonzero(as_tuple=False) | |
| num_active = active_indices.shape[0] | |
| batch_indices = active_indices[:, 0] | |
| seq_indices = active_indices[:, 1] | |
| h_active = h_norm[active_mask] | |
| Q_active = self.q_proj(h_active).view(num_active, self.n_heads, self.head_dim) | |
| cos_q = self.rope.cos_cached[seq_indices].unsqueeze(1) | |
| sin_q = self.rope.sin_cached[seq_indices].unsqueeze(1) | |
| Q_active = RotaryPositionEmbedding._apply_rotary(Q_active, cos_q, sin_q) | |
| K_selected = K[batch_indices] | |
| V_selected = V[batch_indices] | |
| Q_active = Q_active.unsqueeze(2) | |
| scores = torch.matmul(Q_active, K_selected.transpose(-2, -1)) / math.sqrt(self.head_dim) | |
| positions = seq_indices.view(num_active, 1, 1, 1) | |
| key_positions = torch.arange(seq, device=device).view(1, 1, 1, seq) | |
| scores = scores.masked_fill(key_positions > positions, float('-inf')) | |
| attn_weights = torch.nan_to_num(F.softmax(scores, dim=-1), 0.0) | |
| out_active = torch.matmul(attn_weights, V_selected).squeeze(2) | |
| out_active = out_active.contiguous().view(num_active, self.d_model) | |
| out_active = self.out_proj(out_active) | |
| attn_out = torch.zeros(batch, seq, self.d_model, device=device, dtype=h_norm.dtype) | |
| attn_out[active_mask] = out_active | |
| return attn_out | |
| def forward( | |
| self, | |
| h: torch.Tensor, | |
| backbone_logits: torch.Tensor, | |
| ) -> Tuple[torch.Tensor, Dict[str, Any]]: | |
| gate, entropy = self.gate(backbone_logits) | |
| if self.attention_mode == 'true_sparse': | |
| attn_out = self._attention_true_sparse(h, gate) | |
| else: | |
| attn_out = self._attention_full_with_mask(h, gate) | |
| alpha = self.get_alpha() | |
| h_out = h + alpha * attn_out | |
| info = { | |
| 'gate': gate, | |
| 'entropy': entropy, | |
| 'alpha': alpha.mean(), | |
| 'alpha_std': alpha.std(), | |
| 'fire_rate': gate.mean(), | |
| } | |
| return h_out, info | |
| # ============================================================================= | |
| # GDN backbone block (used when backbone='gdn') | |
| # ============================================================================= | |
| class GDNBlock(nn.Module): | |
| """Pre-norm Gated DeltaNet + pre-norm SwiGLU MLP. Requires `fla` library.""" | |
| def __init__(self, d_model: int, d_ff: int, head_dim: int = 64, expand_v: float = 2.0): | |
| super().__init__() | |
| if FLAGatedDeltaNet is None: | |
| raise ImportError( | |
| "flash-linear-attention is required for backbone='gdn'. " | |
| "Install with: pip install flash-linear-attention" | |
| ) | |
| self.d_model = d_model | |
| n_heads = int(0.75 * d_model / head_dim) | |
| assert n_heads * head_dim == int(0.75 * d_model), \ | |
| f"0.75 * d_model={d_model} must be divisible by head_dim={head_dim}" | |
| self.norm_ssm = RMSNorm(d_model) | |
| self.gdn = FLAGatedDeltaNet( | |
| hidden_size=d_model, | |
| num_heads=n_heads, | |
| head_dim=head_dim, | |
| expand_v=expand_v, | |
| mode='chunk', | |
| ) | |
| self.norm_mlp = RMSNorm(d_model) | |
| self.mlp = SwiGLUMLP(d_model, d_ff) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| h = self.norm_ssm(x) | |
| h = self.gdn(h)[0] | |
| x = x + h | |
| x = x + self.mlp(self.norm_mlp(x)) | |
| return x | |
| # ============================================================================= | |
| # Unified AMOR model | |
| # ============================================================================= | |
| class AMOR(nn.Module): | |
| """ | |
| Unified AMOR model. Six published variants are flag combinations: | |
| backbone='mamba2', n_amor_blocks=3, residual_mode='classic' # AMOR | |
| backbone='mamba2', n_amor_blocks=1, residual_mode='classic' # AMOR-classic | |
| backbone='mamba2', n_amor_blocks=3, residual_mode='h' # AMOR-h | |
| backbone='gdn', n_amor_blocks=3, residual_mode='classic' # AMOR-GDN | |
| backbone='gdn', n_amor_blocks=1, residual_mode='classic' # AMOR-classic-GDN | |
| backbone='gdn', n_amor_blocks=3, residual_mode='h' # AMOR-GDN-h | |
| Architecture: | |
| Embedding | |
| -> [Backbone Mixer + SwiGLU MLP] x n_layer | |
| -> norm_f # applied to residual iff residual_mode='classic' | |
| -> [pre_norm -> AMOR Block -> residual fusion] x n_amor_blocks | |
| -> final_norm | |
| -> LM Head (tied to embedding) | |
| residual_mode='classic': | |
| norm_f is applied once to the residual stream after the backbone. Block 0 | |
| therefore has no extra pre-norm and its residual wraps the norm_f'd | |
| stream; blocks 1+ each carry their own pre-norm and their residual wraps | |
| the un-normed h. | |
| residual_mode='h': | |
| norm_f is reused inline as block 0's pre-norm; the residual stream stays | |
| un-normed from the end of the backbone through every AMOR block. All | |
| blocks are symmetric: pre-norm -> attention -> residual wraps un-normed h. | |
| Only final_norm sees the residual, immediately before lm_head. | |
| Default dimensions match a ~180M-param scale (d_model=768, n_layer=12, | |
| d_ff=1216, head_dim=64). | |
| """ | |
| def __init__( | |
| self, | |
| # Architecture flags | |
| backbone: str = 'mamba2', | |
| n_amor_blocks: int = 3, | |
| residual_mode: str = 'classic', | |
| # Common dims | |
| vocab_size: int = 128256, | |
| d_model: int = 768, | |
| n_layer: int = 12, | |
| d_ff: int = 1216, | |
| head_dim: int = 64, | |
| # Mamba-2 backbone params | |
| d_state: int = 128, | |
| d_conv: int = 4, | |
| expand: int = 2, | |
| n_groups: int = 1, | |
| # GDN backbone params | |
| expand_v: float = 2.0, | |
| # AMOR block params | |
| n_heads: int = 12, | |
| init_alpha: float = 0.0, | |
| init_threshold: float = 0.3, | |
| gate_offset: float = 0.03, | |
| offset_mode: str = 'adaptive', | |
| offset_k: float = 0.2, | |
| attention_mode: str = 'full_with_mask', | |
| max_seq_len: int = 4096, | |
| ): | |
| super().__init__() | |
| if backbone not in ('mamba2', 'gdn'): | |
| raise ValueError(f"backbone must be 'mamba2' or 'gdn', got {backbone!r}") | |
| if residual_mode not in ('classic', 'h'): | |
| raise ValueError(f"residual_mode must be 'classic' or 'h', got {residual_mode!r}") | |
| if n_amor_blocks < 1: | |
| raise ValueError(f"n_amor_blocks must be >= 1, got {n_amor_blocks}") | |
| self.backbone = backbone | |
| self.residual_mode = residual_mode | |
| self.n_amor_blocks = n_amor_blocks | |
| self.vocab_size = vocab_size | |
| self.d_model = d_model | |
| self.n_layer = n_layer | |
| self.d_ff = d_ff | |
| self._build_backbone( | |
| d_model=d_model, n_layer=n_layer, d_ff=d_ff, vocab_size=vocab_size, | |
| head_dim=head_dim, d_state=d_state, d_conv=d_conv, expand=expand, | |
| n_groups=n_groups, expand_v=expand_v, | |
| ) | |
| self.lm_head = nn.Linear(d_model, vocab_size, bias=False) | |
| self.lm_head.weight = self.embed.weight | |
| self.amor_blocks = nn.ModuleList() | |
| self.amor_pre_norms = nn.ModuleList() | |
| for i in range(n_amor_blocks): | |
| self.amor_blocks.append(AMORBlock( | |
| d_model=d_model, | |
| vocab_size=vocab_size, | |
| n_heads=n_heads, | |
| init_alpha=init_alpha, | |
| init_threshold=init_threshold, | |
| gate_offset=gate_offset, | |
| offset_mode=offset_mode, | |
| offset_k=offset_k, | |
| attention_mode=attention_mode, | |
| max_seq_len=max_seq_len, | |
| )) | |
| # Block 0's pre-norm slot is None for both modes: | |
| # 'classic' -> input is already norm_f'd, no pre-norm needed. | |
| # 'h' -> norm_f is reused inline as block 0's pre-norm. | |
| if i == 0: | |
| self.amor_pre_norms.append(None) | |
| else: | |
| self.amor_pre_norms.append(RMSNorm(d_model)) | |
| self.final_norm = RMSNorm(d_model) | |
| self._init_weights() | |
| def _build_backbone( | |
| self, d_model, n_layer, d_ff, vocab_size, head_dim, | |
| d_state, d_conv, expand, n_groups, expand_v, | |
| ): | |
| if self.backbone == 'mamba2': | |
| if Mamba2ForCausalLM is None: | |
| raise ImportError( | |
| "transformers (with Mamba2 support) is required for backbone='mamba2'. " | |
| "Install with: pip install 'transformers>=4.43' mamba-ssm causal-conv1d" | |
| ) | |
| ssm_n_heads = (d_model * expand) // head_dim | |
| self.mamba_config = Mamba2Config( | |
| vocab_size=vocab_size, | |
| hidden_size=d_model, | |
| num_hidden_layers=n_layer, | |
| state_size=d_state, | |
| conv_kernel=d_conv, | |
| expand=expand, | |
| head_dim=head_dim, | |
| num_heads=ssm_n_heads, | |
| n_groups=n_groups, | |
| ) | |
| mamba_model = Mamba2ForCausalLM(self.mamba_config) | |
| self.embed = mamba_model.backbone.embeddings | |
| self.ssd_layers = mamba_model.backbone.layers | |
| self.norm_f = mamba_model.backbone.norm_f | |
| self.mlp_norms = nn.ModuleList([RMSNorm(d_model) for _ in range(n_layer)]) | |
| self.mlps = nn.ModuleList([SwiGLUMLP(d_model, d_ff) for _ in range(n_layer)]) | |
| del mamba_model | |
| else: | |
| self.embed = nn.Embedding(vocab_size, d_model) | |
| self.layers = nn.ModuleList([ | |
| GDNBlock(d_model=d_model, d_ff=d_ff, head_dim=head_dim, expand_v=expand_v) | |
| for _ in range(n_layer) | |
| ]) | |
| self.norm_f = RMSNorm(d_model) | |
| def _init_weights(self): | |
| std = 0.02 | |
| nn.init.normal_(self.embed.weight, mean=0.0, std=std) | |
| if self.backbone == 'mamba2': | |
| for layer in self.ssd_layers: | |
| for m in layer.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.normal_(m.weight, mean=0.0, std=std) | |
| for mlp in self.mlps: | |
| for m in mlp.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.normal_(m.weight, mean=0.0, std=std) | |
| else: | |
| for layer in self.layers: | |
| for m in layer.gdn.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.normal_(m.weight, mean=0.0, std=std) | |
| for m in layer.mlp.modules(): | |
| if isinstance(m, nn.Linear): | |
| nn.init.normal_(m.weight, mean=0.0, std=std) | |
| for amor_block in self.amor_blocks: | |
| nn.init.normal_(amor_block.q_proj.weight, mean=0.0, std=std) | |
| nn.init.normal_(amor_block.kv_proj.k_proj.weight, mean=0.0, std=std) | |
| nn.init.normal_(amor_block.kv_proj.v_proj.weight, mean=0.0, std=std) | |
| # out_proj stays zero-initialized. | |
| def _run_backbone(self, input_ids: torch.Tensor) -> torch.Tensor: | |
| h = self.embed(input_ids) | |
| if self.backbone == 'mamba2': | |
| for ssd_layer, mlp_norm, mlp in zip(self.ssd_layers, self.mlp_norms, self.mlps): | |
| h = ssd_layer(h) | |
| h = h + mlp(mlp_norm(h)) | |
| else: | |
| for layer in self.layers: | |
| h = layer(h) | |
| return h | |
| def forward( | |
| self, | |
| input_ids: torch.Tensor, | |
| return_details: bool = False, | |
| ): | |
| h = self._run_backbone(input_ids) | |
| if self.residual_mode == 'classic': | |
| # norm_f flows into the residual; block 0 reads the norm_f'd stream | |
| # directly, blocks 1+ apply their own pre-norm. | |
| h = self.norm_f(h) | |
| all_info = {} | |
| for i, (amor_block, pre_norm) in enumerate(zip(self.amor_blocks, self.amor_pre_norms)): | |
| if pre_norm is not None: | |
| h_normed = pre_norm(h) | |
| elif self.residual_mode == 'h': | |
| # Block 0 in 'h' mode: norm_f is the inline pre-norm; residual | |
| # stays un-normed. | |
| h_normed = self.norm_f(h) | |
| else: | |
| # Block 0 in 'classic' mode: input is already norm_f'd. | |
| h_normed = h | |
| logits_for_gate = (h_normed if amor_block.gate.router is not None | |
| else self.lm_head(h_normed)) | |
| h_out, info = amor_block(h_normed, logits_for_gate) | |
| del logits_for_gate | |
| if self.residual_mode == 'h': | |
| # All blocks symmetric: residual wraps un-normed h. | |
| h = h + (h_out - h_normed) | |
| else: | |
| if pre_norm is not None: | |
| # Residual wraps un-normed h (h was not pre-normed for this block). | |
| h = h + (h_out - h_normed) | |
| else: | |
| # Block 0: residual wraps the norm_f'd stream (h == h_normed here). | |
| h = h_out | |
| if return_details: | |
| all_info[f'block_{i}'] = info | |
| h = self.final_norm(h) | |
| logits = self.lm_head(h) | |
| if return_details: | |
| fire_rates = [v['fire_rate'] for v in all_info.values() if 'fire_rate' in v] | |
| alphas = [v['alpha'] for v in all_info.values() if 'alpha' in v] | |
| details = { | |
| 'per_block': all_info, | |
| 'fire_rate': sum(fire_rates) / len(fire_rates) if fire_rates else 0.0, | |
| 'alpha': sum(alphas) / len(alphas) if alphas else 0.0, | |
| } | |
| return logits, details | |
| return logits | |