FlyinGodzilla commited on
Commit
def39ed
·
verified ·
1 Parent(s): 0c77ba6

Upload canonical AMOR checkpoint with provenance

Browse files
Files changed (12) hide show
  1. LICENSE +21 -0
  2. README.md +58 -0
  3. amor.py +618 -0
  4. amor_decode.py +268 -0
  5. config.json +34 -0
  6. export_metadata.json +48 -0
  7. load_model.py +18 -0
  8. manifest.json +46 -0
  9. model.safetensors +3 -0
  10. requirements.txt +7 -0
  11. training_config.json +37 -0
  12. training_summary.json +19 -0
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anonymous Authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language: en
3
+ tags:
4
+ - amor
5
+ - custom-code
6
+ - pretrained
7
+ datasets:
8
+ - HuggingFaceFW/fineweb-edu
9
+ ---
10
+ # AMOR-Mamba2-440M
11
+
12
+ Private release preparation of the canonical three-block AMOR checkpoint.
13
+ This is a pretrained text-completion model, not an instruction-tuned assistant.
14
+
15
+ ## Architecture and training
16
+ Backbone: Mamba2; parameters: 448,650,496; context: 3072 tokens.
17
+ Training seed: 42; completed steps: 18,255.
18
+ Training settings and summary are included as JSON.
19
+ Three entropy-gated attention blocks follow the complete recurrent backbone.
20
+ The native entropy gate and its frozen threshold buffers are included.
21
+ No distilled routers, optimizer state, or training data are included.
22
+
23
+ ## Loading
24
+ This repository uses custom PyTorch code, not Transformers AutoModel.
25
+ Download the repository files, install the backbone-specific dependencies,
26
+ and run from that directory:
27
+
28
+ ```python
29
+ import torch
30
+ from load_model import load_model
31
+ from transformers import AutoTokenizer
32
+
33
+ model = load_model(device="cuda", dtype=torch.float32)
34
+ tokenizer = AutoTokenizer.from_pretrained("teknium/Llama-3.1-AlternateTokenizer", revision="5590cc63198b0f111f4289daf006da0a12044173")
35
+ inputs = tokenizer("The Eiffel Tower is", return_tensors="pt").to("cuda")
36
+ with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
37
+ logits = model(inputs["input_ids"])
38
+ next_id = logits[:, -1].argmax(-1)
39
+ print(tokenizer.decode(next_id))
40
+ ```
41
+
42
+ The implementation and cache-based decoder are in amor.py and amor_decode.py.
43
+ The tokenizer is referenced, not redistributed, and pinned to the revision used
44
+ for release validation. The original training-time tokenizer revision was not recorded. Efficient inference requires appropriate
45
+ CUDA kernels; this upload does not provide PocketPal/GGUF compatibility.
46
+
47
+ ## Validation and limitations
48
+ All exported tensors are compared exactly with the source checkpoint.
49
+ See export_metadata.json for the current strict-loading and generation status.
50
+ Export checks alone do not establish end-to-end inference correctness.
51
+ CPU validation used torch 2.6.0 and transformers 4.51.3. CUDA kernels and their
52
+ optional dependency versions were not revalidated during this export.
53
+ Outputs may be inaccurate or inherit biases from the training data.
54
+ Training uses dense attention with an output mask and does not save attention FLOPs.
55
+
56
+ ## License
57
+ The included implementation retains its existing MIT LICENSE.
58
+ Checkpoint redistribution licensing must be confirmed before public release.
amor.py ADDED
@@ -0,0 +1,618 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AMOR: Adaptive Metacognitive Output Router.
3
+
4
+ A post-hoc hybrid architecture that gates attention by output prediction entropy.
5
+ A recurrent backbone (Mamba-2 or Gated DeltaNet) is followed by K stacked AMOR
6
+ blocks; each block measures the entropy of softmax(lm_head(h)) and, where the
7
+ backbone is uncertain, fires causal self-attention to refine the residual stream.
8
+
9
+ Input -> [Backbone Mixer + SwiGLU MLP] x N -> [AMOR Block] x K -> Norm -> LM Head
10
+ |
11
+ Entropy Gate (fire if uncertain)
12
+ |
13
+ Attention (Q,K,V from residual)
14
+
15
+ The 6 architecture variants are flag combinations of a single class:
16
+
17
+ backbone in {'mamba2', 'gdn'} # recurrent mixer
18
+ n_amor_blocks in {1, 3} # stack depth
19
+ residual_mode in {'classic', 'h'} # block-0 pre-norm placement
20
+
21
+ K, V are standard linear projections of the residual stream (not SSM internal
22
+ state). Training uses dense attention with output masking ('full_with_mask');
23
+ 'true_sparse' is provided for inference. Each block has its own EntropyGate
24
+ (EMA-tracked threshold), Q/K/V/O, RoPE on Q/K, and a per-channel alpha fusion.
25
+ """
26
+
27
+ import math
28
+ from typing import Tuple, Dict, Any, Optional
29
+
30
+ import torch
31
+ import torch.nn as nn
32
+ import torch.nn.functional as F
33
+
34
+ try:
35
+ import mamba_ssm
36
+ if not hasattr(mamba_ssm, 'mamba_chunk_scan_combined'):
37
+ from mamba_ssm.ops.triton.ssd_combined import (
38
+ mamba_chunk_scan_combined,
39
+ mamba_split_conv1d_scan_combined,
40
+ )
41
+ mamba_ssm.mamba_chunk_scan_combined = mamba_chunk_scan_combined
42
+ mamba_ssm.mamba_split_conv1d_scan_combined = mamba_split_conv1d_scan_combined
43
+ except ImportError:
44
+ pass
45
+
46
+ try:
47
+ from transformers import Mamba2Config, Mamba2ForCausalLM
48
+ except ImportError:
49
+ Mamba2Config = None
50
+ Mamba2ForCausalLM = None
51
+
52
+ try:
53
+ from fla.layers import GatedDeltaNet as FLAGatedDeltaNet
54
+ except ImportError:
55
+ FLAGatedDeltaNet = None
56
+
57
+
58
+ # =============================================================================
59
+ # Building blocks
60
+ # =============================================================================
61
+
62
+ class RMSNorm(nn.Module):
63
+ def __init__(self, d_model: int, eps: float = 1e-6):
64
+ super().__init__()
65
+ self.weight = nn.Parameter(torch.ones(d_model))
66
+ self.eps = eps
67
+
68
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
69
+ rms = torch.sqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + self.eps)
70
+ return self.weight * x / rms
71
+
72
+
73
+ class SwiGLUMLP(nn.Module):
74
+ def __init__(self, d_model: int, d_ff: int):
75
+ super().__init__()
76
+ self.gate_proj = nn.Linear(d_model, d_ff, bias=False)
77
+ self.up_proj = nn.Linear(d_model, d_ff, bias=False)
78
+ self.down_proj = nn.Linear(d_ff, d_model, bias=False)
79
+
80
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
81
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
82
+
83
+
84
+ class RotaryPositionEmbedding(nn.Module):
85
+ def __init__(self, head_dim: int, max_seq_len: int = 8192, base: float = 10000.0):
86
+ super().__init__()
87
+ self.head_dim = head_dim
88
+ inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
89
+ self.register_buffer('inv_freq', inv_freq, persistent=False)
90
+ self._build_cache(max_seq_len)
91
+
92
+ def _build_cache(self, seq_len: int):
93
+ t = torch.arange(seq_len, dtype=self.inv_freq.dtype, device=self.inv_freq.device)
94
+ freqs = torch.outer(t, self.inv_freq)
95
+ emb = torch.cat([freqs, freqs], dim=-1)
96
+ self.register_buffer('cos_cached', emb.cos(), persistent=False)
97
+ self.register_buffer('sin_cached', emb.sin(), persistent=False)
98
+
99
+ def forward(self, q: torch.Tensor, k: torch.Tensor, offset: int = 0):
100
+ seq_len = q.shape[2]
101
+ cos = self.cos_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0)
102
+ sin = self.sin_cached[offset:offset + seq_len].unsqueeze(0).unsqueeze(0)
103
+ return self._apply_rotary(q, cos, sin), self._apply_rotary(k, cos, sin)
104
+
105
+ @staticmethod
106
+ def _apply_rotary(x, cos, sin):
107
+ d = x.shape[-1]
108
+ x1, x2 = x[..., :d // 2], x[..., d // 2:]
109
+ return x * cos + torch.cat([-x2, x1], dim=-1) * sin
110
+
111
+
112
+ class KVProjection(nn.Module):
113
+ def __init__(self, d_model: int, n_heads: int = 12):
114
+ super().__init__()
115
+ self.d_model = d_model
116
+ self.n_heads = n_heads
117
+ self.head_dim = d_model // n_heads
118
+ self.k_proj = nn.Linear(d_model, d_model, bias=False)
119
+ self.v_proj = nn.Linear(d_model, d_model, bias=False)
120
+
121
+ def forward(self, h: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
122
+ return self.k_proj(h), self.v_proj(h)
123
+
124
+
125
+ # =============================================================================
126
+ # Entropy gate
127
+ # =============================================================================
128
+
129
+ class EntropyGate(nn.Module):
130
+ """
131
+ Hard binary gate driven by output-level prediction entropy.
132
+
133
+ Threshold is an EMA of the batch entropy median (no gradient). Two offset
134
+ modes: 'fixed' adds a constant; 'adaptive' adds offset_k * EMA(std), giving
135
+ a scale-independent fire rate as the entropy distribution width changes
136
+ across model scale and training stage.
137
+
138
+ entropy = -sum(p * log(p)) / log(vocab_size)
139
+ threshold = EMA(median) + offset # fixed
140
+ threshold = EMA(median) + offset_k * EMA(std) # adaptive
141
+ gate = (entropy > threshold) # hard binary
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ vocab_size: int,
147
+ init_threshold: float = 0.3,
148
+ ema_momentum: float = 0.99,
149
+ offset: float = 0.03,
150
+ offset_mode: str = 'adaptive',
151
+ offset_k: float = 0.2,
152
+ ):
153
+ super().__init__()
154
+ self.max_entropy = math.log(vocab_size)
155
+ self.ema_momentum = ema_momentum
156
+ self.offset = offset
157
+ self.offset_mode = offset_mode
158
+ self.offset_k = offset_k
159
+ self.register_buffer('running_threshold', torch.tensor(init_threshold))
160
+ self.register_buffer('running_std', torch.tensor(0.1))
161
+
162
+ def forward(self, logits: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
163
+ # Detached: entropy drives gating decisions only. CE loss trains the
164
+ # backbone via the final logits path, never through the gate.
165
+ logits_f32 = logits.detach().float()
166
+ probs = F.softmax(logits_f32, dim=-1)
167
+ raw_entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1)
168
+ entropy = raw_entropy / self.max_entropy
169
+
170
+ if self.training:
171
+ entropy_detached = entropy.detach()
172
+ batch_median = entropy_detached.median()
173
+ self.running_threshold.mul_(self.ema_momentum).add_(
174
+ batch_median, alpha=1.0 - self.ema_momentum
175
+ )
176
+ if self.offset_mode == 'adaptive':
177
+ batch_std = entropy_detached.std()
178
+ self.running_std.mul_(self.ema_momentum).add_(
179
+ batch_std, alpha=1.0 - self.ema_momentum
180
+ )
181
+
182
+ if self.offset_mode == 'adaptive':
183
+ threshold = self.running_threshold + self.offset_k * self.running_std
184
+ else:
185
+ threshold = self.running_threshold + self.offset
186
+
187
+ gate = (entropy > threshold).float()
188
+ return gate, entropy
189
+
190
+
191
+ # =============================================================================
192
+ # AMOR block
193
+ # =============================================================================
194
+
195
+ class AMORBlock(nn.Module):
196
+ """
197
+ Entropy-gated attention block.
198
+
199
+ gate = EntropyGate(lm_head(h_normed))
200
+ attn_out = Attention(h_normed) * gate
201
+ output = h_normed + alpha * attn_out
202
+
203
+ Attention modes:
204
+ 'full_with_mask': dense attention over all positions, output masked by
205
+ the gate. Used for training (GPU-friendly).
206
+ 'true_sparse': Q and the attention computation only run for firing
207
+ positions. Output is bit-identical given the same
208
+ weights/inputs. Used for inference.
209
+
210
+ Zero-initialized out_proj + per-channel alpha (sigmoid(raw_alpha), clamped
211
+ floor of ~0.27) handle output magnitude scaling. RoPE on Q, K.
212
+ """
213
+
214
+ def __init__(
215
+ self,
216
+ d_model: int,
217
+ vocab_size: int,
218
+ n_heads: int = 12,
219
+ init_alpha: float = 0.0,
220
+ init_threshold: float = 0.3,
221
+ gate_offset: float = 0.03,
222
+ offset_mode: str = 'adaptive',
223
+ offset_k: float = 0.2,
224
+ attention_mode: str = 'full_with_mask',
225
+ max_seq_len: int = 4096,
226
+ ):
227
+ super().__init__()
228
+ self.d_model = d_model
229
+ self.n_heads = n_heads
230
+ self.head_dim = d_model // n_heads
231
+ self.attention_mode = attention_mode
232
+
233
+ self.gate = EntropyGate(
234
+ vocab_size,
235
+ init_threshold=init_threshold,
236
+ offset=gate_offset,
237
+ offset_mode=offset_mode,
238
+ offset_k=offset_k,
239
+ )
240
+ self.rope = RotaryPositionEmbedding(self.head_dim, max_seq_len)
241
+ self.q_proj = nn.Linear(d_model, d_model, bias=False)
242
+ self.kv_proj = KVProjection(d_model, n_heads)
243
+ self.out_proj = nn.Linear(d_model, d_model, bias=False)
244
+ nn.init.zeros_(self.out_proj.weight)
245
+ if self.out_proj.bias is not None:
246
+ nn.init.zeros_(self.out_proj.bias)
247
+
248
+ # Per-channel raw alpha; sigmoid + lower clamp at -1 (≈0.27 floor) to
249
+ # prevent collapse. Channels learn their own residual mixing weights.
250
+ self.raw_alpha = nn.Parameter(torch.full((d_model,), float(init_alpha)))
251
+
252
+ def get_alpha(self) -> torch.Tensor:
253
+ return torch.sigmoid(torch.clamp(self.raw_alpha, min=-1.0))
254
+
255
+ def _attention_full_with_mask(self, h_norm: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
256
+ batch, seq, _ = h_norm.shape
257
+ Q = self.q_proj(h_norm).view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2)
258
+ K, V = self.kv_proj(h_norm)
259
+ K = K.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2)
260
+ V = V.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2)
261
+ Q, K = self.rope(Q, K)
262
+ attn_out = F.scaled_dot_product_attention(Q, K, V, is_causal=True)
263
+ attn_out = attn_out.transpose(1, 2).contiguous().view(batch, seq, self.d_model)
264
+ attn_out = self.out_proj(attn_out)
265
+ return attn_out * gate.unsqueeze(-1)
266
+
267
+ def _attention_true_sparse(self, h_norm: torch.Tensor, gate: torch.Tensor) -> torch.Tensor:
268
+ batch, seq, _ = h_norm.shape
269
+ device = h_norm.device
270
+
271
+ K, V = self.kv_proj(h_norm)
272
+ K = K.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2)
273
+ V = V.view(batch, seq, self.n_heads, self.head_dim).transpose(1, 2)
274
+ cos_all = self.rope.cos_cached[:seq].unsqueeze(0).unsqueeze(0)
275
+ sin_all = self.rope.sin_cached[:seq].unsqueeze(0).unsqueeze(0)
276
+ K = RotaryPositionEmbedding._apply_rotary(K, cos_all, sin_all)
277
+
278
+ active_mask = gate > 0.5
279
+ if not active_mask.any():
280
+ return torch.zeros(batch, seq, self.d_model, device=device, dtype=h_norm.dtype)
281
+
282
+ active_indices = active_mask.nonzero(as_tuple=False)
283
+ num_active = active_indices.shape[0]
284
+ batch_indices = active_indices[:, 0]
285
+ seq_indices = active_indices[:, 1]
286
+
287
+ h_active = h_norm[active_mask]
288
+ Q_active = self.q_proj(h_active).view(num_active, self.n_heads, self.head_dim)
289
+ cos_q = self.rope.cos_cached[seq_indices].unsqueeze(1)
290
+ sin_q = self.rope.sin_cached[seq_indices].unsqueeze(1)
291
+ Q_active = RotaryPositionEmbedding._apply_rotary(Q_active, cos_q, sin_q)
292
+
293
+ K_selected = K[batch_indices]
294
+ V_selected = V[batch_indices]
295
+ Q_active = Q_active.unsqueeze(2)
296
+ scores = torch.matmul(Q_active, K_selected.transpose(-2, -1)) / math.sqrt(self.head_dim)
297
+
298
+ positions = seq_indices.view(num_active, 1, 1, 1)
299
+ key_positions = torch.arange(seq, device=device).view(1, 1, 1, seq)
300
+ scores = scores.masked_fill(key_positions > positions, float('-inf'))
301
+ attn_weights = torch.nan_to_num(F.softmax(scores, dim=-1), 0.0)
302
+
303
+ out_active = torch.matmul(attn_weights, V_selected).squeeze(2)
304
+ out_active = out_active.contiguous().view(num_active, self.d_model)
305
+ out_active = self.out_proj(out_active)
306
+
307
+ attn_out = torch.zeros(batch, seq, self.d_model, device=device, dtype=h_norm.dtype)
308
+ attn_out[active_mask] = out_active
309
+ return attn_out
310
+
311
+ def forward(
312
+ self,
313
+ h: torch.Tensor,
314
+ backbone_logits: torch.Tensor,
315
+ ) -> Tuple[torch.Tensor, Dict[str, Any]]:
316
+ gate, entropy = self.gate(backbone_logits)
317
+ if self.attention_mode == 'true_sparse':
318
+ attn_out = self._attention_true_sparse(h, gate)
319
+ else:
320
+ attn_out = self._attention_full_with_mask(h, gate)
321
+ alpha = self.get_alpha()
322
+ h_out = h + alpha * attn_out
323
+ info = {
324
+ 'gate': gate,
325
+ 'entropy': entropy,
326
+ 'alpha': alpha.mean(),
327
+ 'alpha_std': alpha.std(),
328
+ 'fire_rate': gate.mean(),
329
+ }
330
+ return h_out, info
331
+
332
+
333
+ # =============================================================================
334
+ # GDN backbone block (used when backbone='gdn')
335
+ # =============================================================================
336
+
337
+ class GDNBlock(nn.Module):
338
+ """Pre-norm Gated DeltaNet + pre-norm SwiGLU MLP. Requires `fla` library."""
339
+
340
+ def __init__(self, d_model: int, d_ff: int, head_dim: int = 64, expand_v: float = 2.0):
341
+ super().__init__()
342
+ if FLAGatedDeltaNet is None:
343
+ raise ImportError(
344
+ "flash-linear-attention is required for backbone='gdn'. "
345
+ "Install with: pip install flash-linear-attention"
346
+ )
347
+ self.d_model = d_model
348
+ n_heads = int(0.75 * d_model / head_dim)
349
+ assert n_heads * head_dim == int(0.75 * d_model), \
350
+ f"0.75 * d_model={d_model} must be divisible by head_dim={head_dim}"
351
+
352
+ self.norm_ssm = RMSNorm(d_model)
353
+ self.gdn = FLAGatedDeltaNet(
354
+ hidden_size=d_model,
355
+ num_heads=n_heads,
356
+ head_dim=head_dim,
357
+ expand_v=expand_v,
358
+ mode='chunk',
359
+ )
360
+ self.norm_mlp = RMSNorm(d_model)
361
+ self.mlp = SwiGLUMLP(d_model, d_ff)
362
+
363
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
364
+ h = self.norm_ssm(x)
365
+ h = self.gdn(h)[0]
366
+ x = x + h
367
+ x = x + self.mlp(self.norm_mlp(x))
368
+ return x
369
+
370
+
371
+ # =============================================================================
372
+ # Unified AMOR model
373
+ # =============================================================================
374
+
375
+ class AMOR(nn.Module):
376
+ """
377
+ Unified AMOR model. Six published variants are flag combinations:
378
+
379
+ backbone='mamba2', n_amor_blocks=3, residual_mode='classic' # AMOR
380
+ backbone='mamba2', n_amor_blocks=1, residual_mode='classic' # AMOR-classic
381
+ backbone='mamba2', n_amor_blocks=3, residual_mode='h' # AMOR-h
382
+ backbone='gdn', n_amor_blocks=3, residual_mode='classic' # AMOR-GDN
383
+ backbone='gdn', n_amor_blocks=1, residual_mode='classic' # AMOR-classic-GDN
384
+ backbone='gdn', n_amor_blocks=3, residual_mode='h' # AMOR-GDN-h
385
+
386
+ Architecture:
387
+ Embedding
388
+ -> [Backbone Mixer + SwiGLU MLP] x n_layer
389
+ -> norm_f # applied to residual iff residual_mode='classic'
390
+ -> [pre_norm -> AMOR Block -> residual fusion] x n_amor_blocks
391
+ -> final_norm
392
+ -> LM Head (tied to embedding)
393
+
394
+ residual_mode='classic':
395
+ norm_f is applied once to the residual stream after the backbone. Block 0
396
+ therefore has no extra pre-norm and its residual wraps the norm_f'd
397
+ stream; blocks 1+ each carry their own pre-norm and their residual wraps
398
+ the un-normed h.
399
+
400
+ residual_mode='h':
401
+ norm_f is reused inline as block 0's pre-norm; the residual stream stays
402
+ un-normed from the end of the backbone through every AMOR block. All
403
+ blocks are symmetric: pre-norm -> attention -> residual wraps un-normed h.
404
+ Only final_norm sees the residual, immediately before lm_head.
405
+
406
+ Default dimensions match a ~180M-param scale (d_model=768, n_layer=12,
407
+ d_ff=1216, head_dim=64).
408
+ """
409
+
410
+ def __init__(
411
+ self,
412
+ # Architecture flags
413
+ backbone: str = 'mamba2',
414
+ n_amor_blocks: int = 3,
415
+ residual_mode: str = 'classic',
416
+ # Common dims
417
+ vocab_size: int = 128256,
418
+ d_model: int = 768,
419
+ n_layer: int = 12,
420
+ d_ff: int = 1216,
421
+ head_dim: int = 64,
422
+ # Mamba-2 backbone params
423
+ d_state: int = 128,
424
+ d_conv: int = 4,
425
+ expand: int = 2,
426
+ n_groups: int = 1,
427
+ # GDN backbone params
428
+ expand_v: float = 2.0,
429
+ # AMOR block params
430
+ n_heads: int = 12,
431
+ init_alpha: float = 0.0,
432
+ init_threshold: float = 0.3,
433
+ gate_offset: float = 0.03,
434
+ offset_mode: str = 'adaptive',
435
+ offset_k: float = 0.2,
436
+ attention_mode: str = 'full_with_mask',
437
+ max_seq_len: int = 4096,
438
+ ):
439
+ super().__init__()
440
+ if backbone not in ('mamba2', 'gdn'):
441
+ raise ValueError(f"backbone must be 'mamba2' or 'gdn', got {backbone!r}")
442
+ if residual_mode not in ('classic', 'h'):
443
+ raise ValueError(f"residual_mode must be 'classic' or 'h', got {residual_mode!r}")
444
+ if n_amor_blocks < 1:
445
+ raise ValueError(f"n_amor_blocks must be >= 1, got {n_amor_blocks}")
446
+
447
+ self.backbone = backbone
448
+ self.residual_mode = residual_mode
449
+ self.n_amor_blocks = n_amor_blocks
450
+ self.vocab_size = vocab_size
451
+ self.d_model = d_model
452
+ self.n_layer = n_layer
453
+ self.d_ff = d_ff
454
+
455
+ self._build_backbone(
456
+ d_model=d_model, n_layer=n_layer, d_ff=d_ff, vocab_size=vocab_size,
457
+ head_dim=head_dim, d_state=d_state, d_conv=d_conv, expand=expand,
458
+ n_groups=n_groups, expand_v=expand_v,
459
+ )
460
+
461
+ self.lm_head = nn.Linear(d_model, vocab_size, bias=False)
462
+ self.lm_head.weight = self.embed.weight
463
+
464
+ self.amor_blocks = nn.ModuleList()
465
+ self.amor_pre_norms = nn.ModuleList()
466
+ for i in range(n_amor_blocks):
467
+ self.amor_blocks.append(AMORBlock(
468
+ d_model=d_model,
469
+ vocab_size=vocab_size,
470
+ n_heads=n_heads,
471
+ init_alpha=init_alpha,
472
+ init_threshold=init_threshold,
473
+ gate_offset=gate_offset,
474
+ offset_mode=offset_mode,
475
+ offset_k=offset_k,
476
+ attention_mode=attention_mode,
477
+ max_seq_len=max_seq_len,
478
+ ))
479
+ # Block 0's pre-norm slot is None for both modes:
480
+ # 'classic' -> input is already norm_f'd, no pre-norm needed.
481
+ # 'h' -> norm_f is reused inline as block 0's pre-norm.
482
+ if i == 0:
483
+ self.amor_pre_norms.append(None)
484
+ else:
485
+ self.amor_pre_norms.append(RMSNorm(d_model))
486
+
487
+ self.final_norm = RMSNorm(d_model)
488
+ self._init_weights()
489
+
490
+ def _build_backbone(
491
+ self, d_model, n_layer, d_ff, vocab_size, head_dim,
492
+ d_state, d_conv, expand, n_groups, expand_v,
493
+ ):
494
+ if self.backbone == 'mamba2':
495
+ if Mamba2ForCausalLM is None:
496
+ raise ImportError(
497
+ "transformers (with Mamba2 support) is required for backbone='mamba2'. "
498
+ "Install with: pip install 'transformers>=4.43' mamba-ssm causal-conv1d"
499
+ )
500
+ ssm_n_heads = (d_model * expand) // head_dim
501
+ self.mamba_config = Mamba2Config(
502
+ vocab_size=vocab_size,
503
+ hidden_size=d_model,
504
+ num_hidden_layers=n_layer,
505
+ state_size=d_state,
506
+ conv_kernel=d_conv,
507
+ expand=expand,
508
+ head_dim=head_dim,
509
+ num_heads=ssm_n_heads,
510
+ n_groups=n_groups,
511
+ )
512
+ mamba_model = Mamba2ForCausalLM(self.mamba_config)
513
+ self.embed = mamba_model.backbone.embeddings
514
+ self.ssd_layers = mamba_model.backbone.layers
515
+ self.norm_f = mamba_model.backbone.norm_f
516
+ self.mlp_norms = nn.ModuleList([RMSNorm(d_model) for _ in range(n_layer)])
517
+ self.mlps = nn.ModuleList([SwiGLUMLP(d_model, d_ff) for _ in range(n_layer)])
518
+ del mamba_model
519
+ else:
520
+ self.embed = nn.Embedding(vocab_size, d_model)
521
+ self.layers = nn.ModuleList([
522
+ GDNBlock(d_model=d_model, d_ff=d_ff, head_dim=head_dim, expand_v=expand_v)
523
+ for _ in range(n_layer)
524
+ ])
525
+ self.norm_f = RMSNorm(d_model)
526
+
527
+ def _init_weights(self):
528
+ std = 0.02
529
+ nn.init.normal_(self.embed.weight, mean=0.0, std=std)
530
+ if self.backbone == 'mamba2':
531
+ for layer in self.ssd_layers:
532
+ for m in layer.modules():
533
+ if isinstance(m, nn.Linear):
534
+ nn.init.normal_(m.weight, mean=0.0, std=std)
535
+ for mlp in self.mlps:
536
+ for m in mlp.modules():
537
+ if isinstance(m, nn.Linear):
538
+ nn.init.normal_(m.weight, mean=0.0, std=std)
539
+ else:
540
+ for layer in self.layers:
541
+ for m in layer.gdn.modules():
542
+ if isinstance(m, nn.Linear):
543
+ nn.init.normal_(m.weight, mean=0.0, std=std)
544
+ for m in layer.mlp.modules():
545
+ if isinstance(m, nn.Linear):
546
+ nn.init.normal_(m.weight, mean=0.0, std=std)
547
+ for amor_block in self.amor_blocks:
548
+ nn.init.normal_(amor_block.q_proj.weight, mean=0.0, std=std)
549
+ nn.init.normal_(amor_block.kv_proj.k_proj.weight, mean=0.0, std=std)
550
+ nn.init.normal_(amor_block.kv_proj.v_proj.weight, mean=0.0, std=std)
551
+ # out_proj stays zero-initialized.
552
+
553
+ def _run_backbone(self, input_ids: torch.Tensor) -> torch.Tensor:
554
+ h = self.embed(input_ids)
555
+ if self.backbone == 'mamba2':
556
+ for ssd_layer, mlp_norm, mlp in zip(self.ssd_layers, self.mlp_norms, self.mlps):
557
+ h = ssd_layer(h)
558
+ h = h + mlp(mlp_norm(h))
559
+ else:
560
+ for layer in self.layers:
561
+ h = layer(h)
562
+ return h
563
+
564
+ def forward(
565
+ self,
566
+ input_ids: torch.Tensor,
567
+ return_details: bool = False,
568
+ ):
569
+ h = self._run_backbone(input_ids)
570
+
571
+ if self.residual_mode == 'classic':
572
+ # norm_f flows into the residual; block 0 reads the norm_f'd stream
573
+ # directly, blocks 1+ apply their own pre-norm.
574
+ h = self.norm_f(h)
575
+
576
+ all_info = {}
577
+ for i, (amor_block, pre_norm) in enumerate(zip(self.amor_blocks, self.amor_pre_norms)):
578
+ if pre_norm is not None:
579
+ h_normed = pre_norm(h)
580
+ elif self.residual_mode == 'h':
581
+ # Block 0 in 'h' mode: norm_f is the inline pre-norm; residual
582
+ # stays un-normed.
583
+ h_normed = self.norm_f(h)
584
+ else:
585
+ # Block 0 in 'classic' mode: input is already norm_f'd.
586
+ h_normed = h
587
+
588
+ logits_for_gate = self.lm_head(h_normed)
589
+ h_out, info = amor_block(h_normed, logits_for_gate)
590
+ del logits_for_gate
591
+
592
+ if self.residual_mode == 'h':
593
+ # All blocks symmetric: residual wraps un-normed h.
594
+ h = h + (h_out - h_normed)
595
+ else:
596
+ if pre_norm is not None:
597
+ # Residual wraps un-normed h (h was not pre-normed for this block).
598
+ h = h + (h_out - h_normed)
599
+ else:
600
+ # Block 0: residual wraps the norm_f'd stream (h == h_normed here).
601
+ h = h_out
602
+
603
+ if return_details:
604
+ all_info[f'block_{i}'] = info
605
+
606
+ h = self.final_norm(h)
607
+ logits = self.lm_head(h)
608
+
609
+ if return_details:
610
+ fire_rates = [v['fire_rate'] for v in all_info.values() if 'fire_rate' in v]
611
+ alphas = [v['alpha'] for v in all_info.values() if 'alpha' in v]
612
+ details = {
613
+ 'per_block': all_info,
614
+ 'fire_rate': sum(fire_rates) / len(fire_rates) if fire_rates else 0.0,
615
+ 'alpha': sum(alphas) / len(alphas) if alphas else 0.0,
616
+ }
617
+ return logits, details
618
+ return logits
amor_decode.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Decode wrapper for AMOR (autoregressive generation with caching).
3
+
4
+ Same flag dispatch as the model:
5
+ backbone='mamba2' -> Mamba2Cache (recurrent state per layer, O(1) memory)
6
+ backbone='gdn' -> fla Cache (recurrent state + conv state, O(1))
7
+ residual_mode -> determines block 0's pre-norm placement and residual
8
+
9
+ Per-AMOR-block KV caches grow O(N) (one cache per AMOR block, not per backbone
10
+ layer). At each decode step the entropy gate decides whether to compute Q and
11
+ attention; K and V are projected and appended unconditionally so that the cache
12
+ stays causal regardless of past gate decisions.
13
+ """
14
+
15
+ import torch
16
+ import torch.nn.functional as F
17
+
18
+ from amor import AMOR, RotaryPositionEmbedding
19
+
20
+ try:
21
+ from transformers.models.mamba2.modeling_mamba2 import Mamba2Cache
22
+ except ImportError:
23
+ Mamba2Cache = None
24
+
25
+ try:
26
+ from fla.models.utils import Cache as FLACache
27
+ except ImportError:
28
+ try:
29
+ from fla.models.utils import LegacyFLACache as FLACache
30
+ except ImportError:
31
+ FLACache = None
32
+
33
+
34
+ GATE_CHUNK = 4096
35
+
36
+
37
+ def _chunked_gate(lm_head, h, gate_fn):
38
+ """Equivalent to gate_fn(lm_head(h).detach()) but in chunks of GATE_CHUNK
39
+ along the sequence axis, to avoid materializing [B, L, V] at long contexts.
40
+ Bit-identical in eval mode (per-position entropy, frozen threshold)."""
41
+ if h.shape[1] <= GATE_CHUNK:
42
+ return gate_fn(lm_head(h).detach())
43
+ gates, ents = [], []
44
+ for s in range(0, h.shape[1], GATE_CHUNK):
45
+ c = lm_head(h[:, s:s + GATE_CHUNK]).detach()
46
+ g, e = gate_fn(c)
47
+ gates.append(g)
48
+ ents.append(e)
49
+ del c
50
+ return torch.cat(gates, dim=1), torch.cat(ents, dim=1)
51
+
52
+
53
+ class AMORDecodeWrapper:
54
+ """
55
+ Wraps a trained AMOR model for autoregressive decode with true_sparse gating.
56
+
57
+ Use:
58
+ wrapper = AMORDecodeWrapper(model)
59
+ logits, cache = wrapper.prefill(input_ids) # full prompt
60
+ for step in range(num_new_tokens):
61
+ logits, cache, info = wrapper.decode_step(
62
+ next_token_id, cache, step_idx=prompt_len + step,
63
+ )
64
+ """
65
+
66
+ def __init__(self, model: AMOR):
67
+ self.model = model
68
+ self.model.eval()
69
+ self.device = next(model.parameters()).device
70
+ self.dtype = next(model.parameters()).dtype
71
+
72
+ if model.backbone == 'mamba2':
73
+ if Mamba2Cache is None:
74
+ raise ImportError("transformers (with Mamba2Cache) is required for backbone='mamba2'.")
75
+ self.config = model.mamba_config
76
+ else:
77
+ if FLACache is None:
78
+ raise ImportError("flash-linear-attention is required for backbone='gdn'.")
79
+ for i, layer in enumerate(model.layers):
80
+ layer.gdn.layer_idx = i
81
+
82
+ # ---- Backbone helpers (prefill / step) -----------------------------------
83
+
84
+ def _backbone_prefill(self, input_ids: torch.Tensor):
85
+ batch, seq_len = input_ids.shape
86
+ h = self.model.embed(input_ids)
87
+
88
+ if self.model.backbone == 'mamba2':
89
+ backbone_cache = Mamba2Cache(
90
+ self.config, batch_size=batch, dtype=self.dtype, device=self.device,
91
+ )
92
+ cache_position = torch.arange(0, seq_len, device=self.device)
93
+ for ssd_layer, mlp_norm, mlp in zip(
94
+ self.model.ssd_layers, self.model.mlp_norms, self.model.mlps,
95
+ ):
96
+ h = ssd_layer(h, cache_params=backbone_cache, cache_position=cache_position)
97
+ h = h + mlp(mlp_norm(h))
98
+ else:
99
+ backbone_cache = FLACache()
100
+ for layer in self.model.layers:
101
+ h_normed = layer.norm_ssm(h)
102
+ gdn_out, _, backbone_cache = layer.gdn(
103
+ h_normed, past_key_values=backbone_cache, use_cache=True,
104
+ )
105
+ h = h + gdn_out
106
+ h = h + layer.mlp(layer.norm_mlp(h))
107
+
108
+ return h, backbone_cache
109
+
110
+ def _backbone_step(self, token_id: torch.Tensor, backbone_cache, step_idx: int):
111
+ h = self.model.embed(token_id)
112
+
113
+ if self.model.backbone == 'mamba2':
114
+ cache_position = torch.tensor([step_idx], device=self.device)
115
+ for ssd_layer, mlp_norm, mlp in zip(
116
+ self.model.ssd_layers, self.model.mlp_norms, self.model.mlps,
117
+ ):
118
+ h = ssd_layer(h, cache_params=backbone_cache, cache_position=cache_position)
119
+ h = h + mlp(mlp_norm(h))
120
+ else:
121
+ for layer in self.model.layers:
122
+ h_normed = layer.norm_ssm(h)
123
+ gdn_out, _, backbone_cache = layer.gdn(
124
+ h_normed, past_key_values=backbone_cache, use_cache=True,
125
+ )
126
+ h = h + gdn_out
127
+ h = h + layer.mlp(layer.norm_mlp(h))
128
+
129
+ return h, backbone_cache
130
+
131
+ # ---- Pre-norm helper (residual_mode dispatch) ----------------------------
132
+
133
+ def _block0_normed(self, h):
134
+ """Block 0's pre-norm input. None pre_norm slot has two meanings:
135
+ 'classic' -> h is already norm_f'd (pass-through)
136
+ 'h' -> norm_f applied inline to un-normed h
137
+ """
138
+ return self.model.norm_f(h) if self.model.residual_mode == 'h' else h
139
+
140
+ def _residual_step(self, h_pre, h_normed, attn_contribution, pre_norm):
141
+ """Apply one block's residual update. attn_contribution = alpha * attn_out
142
+ (already gated). Pre-norm slot None means block 0; behavior depends on
143
+ residual_mode."""
144
+ if self.model.residual_mode == 'h':
145
+ # All blocks symmetric: residual wraps un-normed h.
146
+ return h_pre + attn_contribution
147
+ if pre_norm is not None:
148
+ # Blocks 1+ in 'classic': residual wraps un-normed h.
149
+ return h_pre + attn_contribution
150
+ # Block 0 in 'classic': h_pre and h_normed are the same norm_f'd tensor;
151
+ # residual wraps that.
152
+ return h_normed + attn_contribution
153
+
154
+ # ---- Public API ----------------------------------------------------------
155
+
156
+ @torch.no_grad()
157
+ def prefill(self, input_ids: torch.Tensor):
158
+ batch, seq_len = input_ids.shape
159
+
160
+ h, backbone_cache = self._backbone_prefill(input_ids)
161
+ if self.model.residual_mode == 'classic':
162
+ h = self.model.norm_f(h)
163
+
164
+ cos_all = None
165
+ amor_kv_caches = []
166
+
167
+ for amor_block, pre_norm in zip(self.model.amor_blocks, self.model.amor_pre_norms):
168
+ if pre_norm is not None:
169
+ h_normed = pre_norm(h)
170
+ else:
171
+ h_normed = self._block0_normed(h)
172
+
173
+ gate, _ = _chunked_gate(self.model.lm_head, h_normed, amor_block.gate)
174
+
175
+ ab = amor_block
176
+ n_heads, head_dim = ab.n_heads, ab.head_dim
177
+
178
+ K, V = ab.kv_proj(h_normed)
179
+ K = K.view(batch, seq_len, n_heads, head_dim).transpose(1, 2)
180
+ V = V.view(batch, seq_len, n_heads, head_dim).transpose(1, 2)
181
+
182
+ if cos_all is None:
183
+ cos_all = ab.rope.cos_cached[:seq_len].unsqueeze(0).unsqueeze(0)
184
+ sin_all = ab.rope.sin_cached[:seq_len].unsqueeze(0).unsqueeze(0)
185
+ K = RotaryPositionEmbedding._apply_rotary(K, cos_all, sin_all)
186
+ amor_kv_caches.append((K, V))
187
+
188
+ Q = ab.q_proj(h_normed).view(batch, seq_len, n_heads, head_dim).transpose(1, 2)
189
+ Q = RotaryPositionEmbedding._apply_rotary(Q, cos_all, sin_all)
190
+ attn_out = F.scaled_dot_product_attention(Q, K, V, is_causal=True)
191
+ attn_out = attn_out.transpose(1, 2).contiguous().view(batch, seq_len, ab.d_model)
192
+ attn_out = ab.out_proj(attn_out) * gate.unsqueeze(-1)
193
+ alpha = ab.get_alpha()
194
+
195
+ h = self._residual_step(h, h_normed, alpha * attn_out, pre_norm)
196
+
197
+ h = self.model.final_norm(h)
198
+ logits = self.model.lm_head(h)
199
+
200
+ cache = {
201
+ 'backbone_cache': backbone_cache,
202
+ 'amor_kv_caches': amor_kv_caches,
203
+ }
204
+ return logits, cache
205
+
206
+ @torch.no_grad()
207
+ def decode_step(self, token_id: torch.Tensor, cache: dict, step_idx: int):
208
+ batch = token_id.shape[0]
209
+ backbone_cache = cache['backbone_cache']
210
+ amor_kv_caches = cache['amor_kv_caches']
211
+
212
+ h, backbone_cache = self._backbone_step(token_id, backbone_cache, step_idx)
213
+ if self.model.residual_mode == 'classic':
214
+ h = self.model.norm_f(h)
215
+
216
+ fire_info = []
217
+ new_amor_kv_caches = []
218
+
219
+ for i, (amor_block, pre_norm) in enumerate(
220
+ zip(self.model.amor_blocks, self.model.amor_pre_norms)
221
+ ):
222
+ past_K, past_V = amor_kv_caches[i]
223
+ ab = amor_block
224
+ n_heads, head_dim = ab.n_heads, ab.head_dim
225
+
226
+ if pre_norm is not None:
227
+ h_normed = pre_norm(h)
228
+ else:
229
+ h_normed = self._block0_normed(h)
230
+
231
+ gate_logits = self.model.lm_head(h_normed).detach()
232
+ gate, entropy = ab.gate(gate_logits)
233
+ del gate_logits
234
+ fired = gate.item() > 0.5
235
+
236
+ K_new, V_new = ab.kv_proj(h_normed)
237
+ K_new = K_new.view(batch, 1, n_heads, head_dim).transpose(1, 2)
238
+ V_new = V_new.view(batch, 1, n_heads, head_dim).transpose(1, 2)
239
+ cos = ab.rope.cos_cached[step_idx:step_idx + 1].unsqueeze(0).unsqueeze(0)
240
+ sin = ab.rope.sin_cached[step_idx:step_idx + 1].unsqueeze(0).unsqueeze(0)
241
+ K_new = RotaryPositionEmbedding._apply_rotary(K_new, cos, sin)
242
+
243
+ K_full = torch.cat([past_K, K_new], dim=2)
244
+ V_full = torch.cat([past_V, V_new], dim=2)
245
+ new_amor_kv_caches.append((K_full, V_full))
246
+
247
+ if fired:
248
+ Q = ab.q_proj(h_normed).view(batch, 1, n_heads, head_dim).transpose(1, 2)
249
+ Q = RotaryPositionEmbedding._apply_rotary(Q, cos, sin)
250
+ attn_out = F.scaled_dot_product_attention(Q, K_full, V_full, is_causal=False)
251
+ attn_out = attn_out.transpose(1, 2).contiguous().view(batch, 1, ab.d_model)
252
+ attn_out = ab.out_proj(attn_out)
253
+ alpha = ab.get_alpha()
254
+ h = self._residual_step(h, h_normed, alpha * attn_out, pre_norm)
255
+ else:
256
+ # Gate didn't fire (true_sparse). For 'classic' mode block 0,
257
+ # h was already norm_f'd above, so no shim is needed. For block 0
258
+ # in 'h' mode, the residual wraps un-normed h, so we leave h as-is.
259
+ pass
260
+
261
+ fire_info.append({'block': i, 'fired': fired, 'entropy': entropy.item()})
262
+
263
+ h = self.model.final_norm(h)
264
+ logits = self.model.lm_head(h)
265
+
266
+ cache['backbone_cache'] = backbone_cache
267
+ cache['amor_kv_caches'] = new_amor_kv_caches
268
+ return logits, cache, fire_info
config.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architecture": "AMOR",
3
+ "format": "custom-pytorch-amor-v1",
4
+ "model_kwargs": {
5
+ "d_model": 1024,
6
+ "n_layer": 24,
7
+ "d_ff": 1984,
8
+ "head_dim": 64,
9
+ "d_state": 128,
10
+ "d_conv": 4,
11
+ "expand": 2,
12
+ "n_groups": 1,
13
+ "backbone": "mamba2",
14
+ "n_amor_blocks": 3,
15
+ "residual_mode": "classic",
16
+ "vocab_size": 128256,
17
+ "n_heads": 16,
18
+ "attention_mode": "full_with_mask",
19
+ "max_seq_len": 4096,
20
+ "init_alpha": 0.0,
21
+ "init_threshold": 0.3,
22
+ "gate_offset": 0.03,
23
+ "offset_mode": "adaptive",
24
+ "offset_k": 0.2
25
+ },
26
+ "tokenizer": "teknium/Llama-3.1-AlternateTokenizer",
27
+ "tokenizer_revision": "5590cc63198b0f111f4289daf006da0a12044173",
28
+ "training_context_length": 3072,
29
+ "tied_weights": {
30
+ "lm_head.weight": "embed.weight"
31
+ },
32
+ "note": "Use load_model.py; not compatible with Transformers AutoModel or GGUF.",
33
+ "tokenizer_revision_provenance": "Pinned at release validation; training-time revision not recorded."
34
+ }
export_metadata.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repo_id": "FlyinGodzilla/AMOR-Mamba2-440M",
3
+ "source_filename": "amor_440m_step_0018255.pt",
4
+ "source_sha256": "9a2e1000be0ab7c2bc5003e5a8f6ca4bf5e357828346f4aa19860fffa6c901df",
5
+ "step": 18255,
6
+ "tokens_seen": 8973017088,
7
+ "n_params": 448650496,
8
+ "tensor_dtypes": {
9
+ "torch.float32": 338
10
+ },
11
+ "gate_buffers": {
12
+ "0": {
13
+ "running_threshold": 0.8732627034187317,
14
+ "running_std": 0.046764787286520004
15
+ },
16
+ "1": {
17
+ "running_threshold": 0.8569177389144897,
18
+ "running_std": 0.05602722615003586
19
+ },
20
+ "2": {
21
+ "running_threshold": 0.8578681349754333,
22
+ "running_std": 0.05078088119626045
23
+ }
24
+ },
25
+ "optimizer_included": false,
26
+ "precision_changed": false,
27
+ "validation": {
28
+ "exported_tensors_equal": true,
29
+ "strict_model_load": true,
30
+ "generation": true,
31
+ "device": "cpu",
32
+ "dtype": "float32",
33
+ "torch": "2.6.0",
34
+ "transformers": "4.51.3",
35
+ "tokenizer_revision": "5590cc63198b0f111f4289daf006da0a12044173",
36
+ "prompt": "The Eiffel Tower is",
37
+ "generated_token_ids": [
38
+ 279,
39
+ 82717,
40
+ 4857,
41
+ 304
42
+ ],
43
+ "prefill_max_abs_diff": 0.0,
44
+ "decode_max_abs_diff": 2.288818359375e-05,
45
+ "seconds": 51.611721992492676
46
+ },
47
+ "implementation": "paper_backup/amor_release; verify against checkpoint before publishing"
48
+ }
load_model.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load this custom AMOR release; not a Transformers AutoModel repository."""
2
+ import json
3
+ from pathlib import Path
4
+ import torch
5
+ from safetensors.torch import load_file
6
+ from amor import AMOR
7
+
8
+ def load_model(directory=None, device="cpu", dtype=torch.float32):
9
+ directory = Path(directory) if directory else Path(__file__).resolve().parent
10
+ config = json.loads((directory / "config.json").read_text())
11
+ model = AMOR(**config["model_kwargs"])
12
+ state = load_file(str(directory / "model.safetensors"), device="cpu")
13
+ # The shared embedding/head matrix is stored only once.
14
+ state["lm_head.weight"] = state["embed.weight"]
15
+ model.load_state_dict(state, strict=True)
16
+ if model.lm_head.weight is not model.embed.weight:
17
+ raise RuntimeError("Embedding/head weight tying was lost")
18
+ return model.to(device=device, dtype=dtype).eval()
manifest.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "LICENSE": {
3
+ "bytes": 1074,
4
+ "sha256": "1d0dde41ece0f9620937f1a81a67e295ace0eae180c009d862006e8f3b288458"
5
+ },
6
+ "README.md": {
7
+ "bytes": 2483,
8
+ "sha256": "e84dafeea0db5b4076c33ab7f97d151f041b918c9bfa449bc40a00c2195ac7ce"
9
+ },
10
+ "amor.py": {
11
+ "bytes": 25015,
12
+ "sha256": "eae95ffa74334e8121101ff9ffa3ded544f527da99473e8cfba80d25d9e1ff22"
13
+ },
14
+ "amor_decode.py": {
15
+ "bytes": 10793,
16
+ "sha256": "9c3f27f8fe58d6a9b138c9303e732a1af89ed970cafbceb956a4047b1fb4adb7"
17
+ },
18
+ "config.json": {
19
+ "bytes": 970,
20
+ "sha256": "f9067b6ed01611d6cc894eb72ffb33aea6809725072d68c8bb471d0565298b7e"
21
+ },
22
+ "export_metadata.json": {
23
+ "bytes": 1321,
24
+ "sha256": "ae3ef5a2b40feea4b7314019b6914d083e98f4baffe03dea553495b93e40fee4"
25
+ },
26
+ "load_model.py": {
27
+ "bytes": 857,
28
+ "sha256": "5bda80ee503b816c88cea06501de4674dd6877e1c27b005f3cf8d1d0ac6c5ced"
29
+ },
30
+ "model.safetensors": {
31
+ "bytes": 1794636104,
32
+ "sha256": "3248a9439b813c8c15219509079562380d10a82fcadf88c78c79cb9694fa0339"
33
+ },
34
+ "requirements.txt": {
35
+ "bytes": 160,
36
+ "sha256": "2ce7324857363b92ccc94ebd6dc4fcca99f690b44955aff7ba715def57c57c5a"
37
+ },
38
+ "training_config.json": {
39
+ "bytes": 752,
40
+ "sha256": "046b88bd3df1a4f86b817ee2cac640d5494de81b95ebf5f9437f49a20f879cf4"
41
+ },
42
+ "training_summary.json": {
43
+ "bytes": 551,
44
+ "sha256": "bde4fba2a156bee6fc9b38eb0b68e45ca24b5da367c08f3a0c2c5ec1cddc3142"
45
+ }
46
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3248a9439b813c8c15219509079562380d10a82fcadf88c78c79cb9694fa0339
3
+ size 1794636104
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ torch==2.6.0
2
+ transformers==4.51.3
3
+ safetensors>=0.4
4
+ einops>=0.7
5
+ # CUDA dependencies: install the ones needed by this backbone.
6
+ mamba-ssm>=2.0
7
+ causal-conv1d>=1.4
training_config.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "amor",
3
+ "scale": "440m",
4
+ "backbone_config": {
5
+ "d_model": 1024,
6
+ "n_layer": 24,
7
+ "d_ff": 1984,
8
+ "d_state": 128,
9
+ "expand": 2,
10
+ "head_dim": 64,
11
+ "d_conv": 4,
12
+ "n_groups": 1,
13
+ "peak_lr": 0.0003
14
+ },
15
+ "seq_len": 3072,
16
+ "batch_size": 4,
17
+ "grad_accum": 40,
18
+ "world_size": 1,
19
+ "total_tokens": 8973009920,
20
+ "total_steps": 18255,
21
+ "warmup_steps": 2000,
22
+ "peak_lr": 0.0003,
23
+ "min_lr": 1e-05,
24
+ "chinchilla_multiplier": 20,
25
+ "seed": 42,
26
+ "vocab_size": 128256,
27
+ "tokenizer": "teknium/Llama-3.1-AlternateTokenizer",
28
+ "n_params": 448650496,
29
+ "amor": {
30
+ "lr_alpha": 0.003,
31
+ "init_alpha": 0.0,
32
+ "init_threshold": 0.3,
33
+ "gate_offset": 0.03,
34
+ "offset_mode": "adaptive",
35
+ "offset_k": 0.2
36
+ }
37
+ }
training_summary.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "amor",
3
+ "scale": "440m",
4
+ "final_val_loss": 2.9757252883911134,
5
+ "final_val_ppl": 19.60383652966566,
6
+ "best_val_loss": 2.9758635330200196,
7
+ "best_val_ppl": 19.606546842110593,
8
+ "total_steps": 18255,
9
+ "tokens_trained": 8973017088,
10
+ "training_hours": 14.797530925538805,
11
+ "n_params": 448650496,
12
+ "flops_per_token": 1634942976.0,
13
+ "world_size": 1,
14
+ "linear_flops": 778505772072960,
15
+ "attention_flops": 18916646584320,
16
+ "ssm_flops": 6184752906240,
17
+ "final_fire_rate": 0.4057179203764526,
18
+ "final_alpha": 0.7287433220977273
19
+ }