zotowata commited on
Commit
ac4f4fa
·
unverified ·
1 Parent(s): d0ee226

Upload src/model.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/model.py +1603 -0
src/model.py ADDED
@@ -0,0 +1,1603 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PC-SHO-DLM: Predictive-Coding Diffusion Language Models
3
+ with Precision-Conditioned Second-Order Settling
4
+
5
+ Reference implementation of the core architecture.
6
+ """
7
+
8
+ import math
9
+ from dataclasses import dataclass
10
+ from typing import Optional, Tuple
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+
16
+
17
+ # =============================================================================
18
+ # Configuration
19
+ # =============================================================================
20
+
21
+ @dataclass
22
+ class PCSHOConfig:
23
+ """Configuration for PC-SHO-DLM."""
24
+ vocab_size: int = 32000
25
+ max_seq_len: int = 1024
26
+ d_model: int = 512
27
+ n_heads: int = 8
28
+ n_layers: int = 12
29
+ d_ff: int = 2048
30
+ dropout: float = 0.1
31
+
32
+ # Diffusion
33
+ n_diffusion_steps: int = 1000
34
+ mask_token_id: int = 0 # [MASK] token id
35
+
36
+ # Predictive coding settling
37
+ n_settling_steps: int = 8
38
+ anchor_rho: float = 0.01
39
+ denoising_lambda: float = 5.0 # For honest entropy-based settling (~30-50% of energy)
40
+
41
+ # Second-order dynamics
42
+ eta_base: float = 0.1
43
+ c_eta: float = 1.0
44
+ gamma_min: float = 0.1
45
+ gamma_max: float = 0.99
46
+ mass: float = 1.0
47
+
48
+ # State continuation
49
+ velocity_decay: float = 0.9
50
+
51
+ # Adaptive settling (soft-gate)
52
+ settling_threshold: float = 0.5
53
+ settling_temperature: float = 0.1 # Sharpness of soft gate sigmoid
54
+ entropy_coeff: float = 1.0
55
+ error_coeff: float = 0.1
56
+
57
+ # Feedback predictor
58
+ feedback_rank: int = 64 # Low-rank feedback, 0 = tied weights
59
+
60
+ # Online learning during inference
61
+ online_learn_lr: float = 1e-5
62
+ online_learn_grad_clip: float = 0.1
63
+ online_learn_min_energy_ratio: float = 0.95 # only update if energy decreased by 5%+
64
+
65
+ # Temporal hierarchy (per-layer scaling of SHO dynamics)
66
+ mass_scale: float = 0.5 # mass increases with depth: m_l = mass * (1 + scale * l/L)
67
+ gamma_scale: float = 0.3 # damping decreases with depth
68
+ eta_scale: float = 0.3 # step size decreases with depth
69
+
70
+ # Homeostatic plasticity (adaptive anchoring per layer)
71
+ homeostatic_target: float = 0.0 # 0 = auto-detect from first batch
72
+ homeostatic_rate: float = 0.0001 # very slow adaptation
73
+ homeostatic_rho_min: float = 0.005
74
+ homeostatic_rho_max: float = 0.02 # never exceed 2x the base anchor_rho
75
+
76
+ # Lateral inhibition — DISABLED (breaks settling convergence via random sampling)
77
+ lateral_inhibition: float = 0.0
78
+ settling_budget_fraction: float = 0.5
79
+
80
+
81
+ # =============================================================================
82
+ # Precision Head
83
+ # =============================================================================
84
+
85
+ class PrecisionHead(nn.Module):
86
+ """Learned tokenwise/channelwise precision for a single layer.
87
+
88
+ Predicts diagonal precision from hidden states and timestep.
89
+ Uses factored (token x channel) parameterization.
90
+ """
91
+
92
+ def __init__(self, d_model: int, n_diffusion_steps: int, eps: float = 1e-6):
93
+ super().__init__()
94
+ self.eps = eps
95
+ # Token-level precision
96
+ self.tok_proj = nn.Sequential(
97
+ nn.Linear(d_model, d_model // 4),
98
+ nn.GELU(),
99
+ nn.Linear(d_model // 4, 1),
100
+ )
101
+ # Channel-level precision (shared across tokens)
102
+ self.ch_proj = nn.Sequential(
103
+ nn.Linear(d_model, d_model // 4),
104
+ nn.GELU(),
105
+ nn.Linear(d_model // 4, d_model),
106
+ )
107
+ # Timestep modulation
108
+ self.time_embed = nn.Embedding(n_diffusion_steps + 1, d_model)
109
+
110
+ def forward(
111
+ self, h: torch.Tensor, t: torch.Tensor
112
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
113
+ """
114
+ Args:
115
+ h: (batch, seq_len, d_model)
116
+ t: (batch,) integer timesteps
117
+
118
+ Returns:
119
+ tok_precision: (batch, seq_len, 1)
120
+ ch_precision: (batch, 1, d_model)
121
+ """
122
+ t_emb = self.time_embed(t).unsqueeze(1) # (B, 1, D)
123
+ h_mod = h + t_emb
124
+
125
+ # Aggregate over sequence for channel precision
126
+ h_mean = h_mod.mean(dim=1, keepdim=True) # (B, 1, D)
127
+
128
+ tok_precision = F.softplus(self.tok_proj(h_mod)).clamp(max=10.0) + self.eps # (B, S, 1)
129
+ ch_precision = F.softplus(self.ch_proj(h_mean)).clamp(max=10.0) + self.eps # (B, 1, D)
130
+
131
+ return tok_precision, ch_precision
132
+
133
+
134
+ # =============================================================================
135
+ # Bidirectional Transformer Block (Forward / Bottom-Up)
136
+ # =============================================================================
137
+
138
+ class BidirectionalTransformerBlock(nn.Module):
139
+ """Standard bidirectional transformer block (bottom-up pathway)."""
140
+
141
+ def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
142
+ super().__init__()
143
+ self.self_attn = nn.MultiheadAttention(
144
+ d_model, n_heads, dropout=dropout, batch_first=True
145
+ )
146
+ self.ff = nn.Sequential(
147
+ nn.Linear(d_model, d_ff),
148
+ nn.GELU(),
149
+ nn.Linear(d_ff, d_model),
150
+ nn.Dropout(dropout),
151
+ )
152
+ self.norm1 = nn.LayerNorm(d_model)
153
+ self.norm2 = nn.LayerNorm(d_model)
154
+ self.dropout = nn.Dropout(dropout)
155
+
156
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
157
+ # Self-attention (bidirectional, no causal mask)
158
+ residual = x
159
+ x = self.norm1(x)
160
+ attn_out, _ = self.self_attn(x, x, x)
161
+ x = residual + self.dropout(attn_out)
162
+
163
+ # Feedforward
164
+ residual = x
165
+ x = self.norm2(x)
166
+ x = residual + self.ff(x)
167
+
168
+ return x
169
+
170
+
171
+ # =============================================================================
172
+ # Feedback Predictor (Top-Down Pathway)
173
+ # =============================================================================
174
+
175
+ class FeedbackPredictor(nn.Module):
176
+ """Lightweight top-down predictor g_l: predicts h_l from h_{l+1}.
177
+
178
+ Uses low-rank projection by default for parameter efficiency.
179
+ """
180
+
181
+ def __init__(self, d_model: int, rank: int = 64):
182
+ super().__init__()
183
+ if rank > 0 and rank < d_model:
184
+ self.proj = nn.Sequential(
185
+ nn.LayerNorm(d_model),
186
+ nn.Linear(d_model, rank, bias=False),
187
+ nn.GELU(),
188
+ nn.Linear(rank, d_model, bias=False),
189
+ )
190
+ else:
191
+ # Full-rank feedback
192
+ self.proj = nn.Sequential(
193
+ nn.LayerNorm(d_model),
194
+ nn.Linear(d_model, d_model),
195
+ nn.GELU(),
196
+ nn.Linear(d_model, d_model),
197
+ )
198
+
199
+ def forward(self, h_above: torch.Tensor) -> torch.Tensor:
200
+ return self.proj(h_above)
201
+
202
+
203
+ # =============================================================================
204
+ # Lightweight Settling Block (Linear Attention for Inner Loop)
205
+ # =============================================================================
206
+
207
+ class LinearAttention(nn.Module):
208
+ """O(n*d^2) linear attention with recurrent SSM dual form.
209
+
210
+ Supports two modes:
211
+ - Batched: O(n*d^2) over full sequence (training)
212
+ - Recurrent: O(d^2) per token with fixed-size state (inference)
213
+
214
+ The recurrent state S = K^T @ V is the Mamba-2 duality:
215
+ same math as linear attention but O(1) memory per new token.
216
+ This eliminates KV-cache entirely — 393KB total for unlimited context.
217
+
218
+ Per-head precision implements attention as active inference.
219
+ """
220
+
221
+ def __init__(self, d_model: int, n_heads: int, dropout: float = 0.0):
222
+ super().__init__()
223
+ self.n_heads = n_heads
224
+ self.d_head = d_model // n_heads
225
+ self.q_proj = nn.Linear(d_model, d_model)
226
+ self.k_proj = nn.Linear(d_model, d_model)
227
+ self.v_proj = nn.Linear(d_model, d_model)
228
+ self.o_proj = nn.Linear(d_model, d_model)
229
+ # Per-head precision (attention as active inference)
230
+ self.head_precision = nn.Linear(d_model, n_heads)
231
+ # Recurrent state: S[h] = (d_head, d_head), z[h] = (d_head,) per head
232
+ self._recurrent_state = None # (B, H, d, d)
233
+ self._recurrent_norm = None # (B, H, d)
234
+
235
+ def _elu_feature(self, x):
236
+ return F.elu(x, alpha=1.0) + 1.0
237
+
238
+ def reset_recurrent_state(self):
239
+ """Reset for new sequence."""
240
+ self._recurrent_state = None
241
+ self._recurrent_norm = None
242
+
243
+ def forward(self, x: torch.Tensor, recurrent: bool = False) -> torch.Tensor:
244
+ """Forward pass.
245
+
246
+ Args:
247
+ x: (B, S, D) input
248
+ recurrent: if True, use O(1) recurrent mode (incremental state update)
249
+ """
250
+ B, S, D = x.shape
251
+ H, d = self.n_heads, self.d_head
252
+
253
+ Q = self._elu_feature(self.q_proj(x).view(B, S, H, d).transpose(1, 2))
254
+ K = self._elu_feature(self.k_proj(x).view(B, S, H, d).transpose(1, 2))
255
+ V = self.v_proj(x).view(B, S, H, d).transpose(1, 2)
256
+
257
+ # Per-head precision: active inference gating
258
+ pi = F.softplus(self.head_precision(x.mean(dim=1))) # (B, H)
259
+ Q = Q * pi.unsqueeze(-1).unsqueeze(-1)
260
+
261
+ if recurrent:
262
+ # === RECURRENT MODE: O(d^2) per token, replaces KV-cache ===
263
+ # State: S = sum_t k_t @ v_t^T, shape (B, H, d, d)
264
+ # Output: o_t = q_t @ S_t / (q_t @ z_t)
265
+ device = x.device
266
+ if self._recurrent_state is None:
267
+ self._recurrent_state = torch.zeros(B, H, d, d, device=device)
268
+ self._recurrent_norm = torch.zeros(B, H, d, device=device)
269
+
270
+ outputs = []
271
+ for t in range(S):
272
+ k_t = K[:, :, t, :] # (B, H, d)
273
+ v_t = V[:, :, t, :]
274
+ q_t = Q[:, :, t, :]
275
+
276
+ # Update recurrent state: S += k @ v^T
277
+ self._recurrent_state = self._recurrent_state + torch.einsum("bhd,bhe->bhde", k_t, v_t)
278
+ self._recurrent_norm = self._recurrent_norm + k_t
279
+
280
+ # Query: o = q @ S / (q @ z)
281
+ o_t = torch.einsum("bhd,bhde->bhe", q_t, self._recurrent_state)
282
+ z_t = torch.einsum("bhd,bhd->bh", q_t, self._recurrent_norm).unsqueeze(-1) + 1e-6
283
+ outputs.append(o_t / z_t)
284
+
285
+ out = torch.stack(outputs, dim=2).transpose(1, 2).reshape(B, S, D)
286
+ else:
287
+ # === BATCHED MODE: O(n * d^2) over full sequence ===
288
+ KV = torch.einsum("bhsd,bhse->bhde", K, V)
289
+ QKV = torch.einsum("bhsd,bhde->bhse", Q, KV)
290
+ normalizer = torch.einsum("bhsd,bhd->bhs", Q, K.sum(dim=2)).unsqueeze(-1) + 1e-6
291
+ out = (QKV / normalizer).transpose(1, 2).reshape(B, S, D)
292
+
293
+ # Cache the final recurrent state for potential continuation
294
+ self._recurrent_state = KV.detach()
295
+ self._recurrent_norm = K.sum(dim=2).detach()
296
+
297
+ return self.o_proj(out)
298
+
299
+
300
+ class LightweightSettlingBlock(nn.Module):
301
+ """Settling block with linear attention — used in the K inner iterations.
302
+
303
+ Same structure as BidirectionalTransformerBlock but O(n) instead of O(n^2).
304
+ """
305
+
306
+ def __init__(self, d_model: int, n_heads: int, d_ff: int, dropout: float = 0.1):
307
+ super().__init__()
308
+ self.attn = LinearAttention(d_model, n_heads, dropout)
309
+ self.ff = nn.Sequential(
310
+ nn.Linear(d_model, d_ff),
311
+ nn.GELU(),
312
+ nn.Linear(d_ff, d_model),
313
+ nn.Dropout(dropout),
314
+ )
315
+ self.norm1 = nn.LayerNorm(d_model)
316
+ self.norm2 = nn.LayerNorm(d_model)
317
+ self.dropout = nn.Dropout(dropout)
318
+
319
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
320
+ residual = x
321
+ x = self.norm1(x)
322
+ x = residual + self.dropout(self.attn(x))
323
+ residual = x
324
+ x = self.norm2(x)
325
+ x = residual + self.ff(x)
326
+ return x
327
+
328
+
329
+ # =============================================================================
330
+ # Masking Diffusion Schedule
331
+ # =============================================================================
332
+
333
+ class MaskDiffusionSchedule(nn.Module):
334
+ """Absorbing-mask corruption schedule for discrete diffusion."""
335
+
336
+ def __init__(self, n_steps: int):
337
+ super().__init__()
338
+ self.n_steps = n_steps
339
+ # Cosine masking schedule
340
+ alphas = torch.cos(
341
+ (torch.arange(n_steps + 1) / n_steps) * (math.pi / 2)
342
+ ) ** 2
343
+ # alpha_bar: probability of NOT being masked
344
+ # At t=0: alpha_bar ~ 1 (nothing masked)
345
+ # At t=T: alpha_bar ~ 0 (everything masked)
346
+ self.register_buffer("alpha_bar", alphas)
347
+
348
+ def mask_rate(self, t: torch.Tensor) -> torch.Tensor:
349
+ """Probability of a token being masked at timestep t."""
350
+ return 1.0 - self.alpha_bar[t]
351
+
352
+ def corrupt(
353
+ self,
354
+ x_0: torch.Tensor,
355
+ t: torch.Tensor,
356
+ mask_token_id: int,
357
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
358
+ """Apply forward corruption: mask tokens with probability alpha_bar_t.
359
+
360
+ Args:
361
+ x_0: (batch, seq_len) clean token ids
362
+ t: (batch,) timesteps
363
+ mask_token_id: id of [MASK] token
364
+
365
+ Returns:
366
+ x_t: corrupted sequence
367
+ mask: boolean mask of corrupted positions
368
+ """
369
+ mask_prob = self.mask_rate(t).unsqueeze(-1) # (B, 1)
370
+ mask = torch.rand_like(x_0.float()) < mask_prob # (B, S)
371
+ x_t = x_0.clone()
372
+ x_t[mask] = mask_token_id
373
+ return x_t, mask
374
+
375
+
376
+ # =============================================================================
377
+ # PC-SHO-DLM Core Model
378
+ # =============================================================================
379
+
380
+ class PCSHODLM(nn.Module):
381
+ """PC-SHO-DLM: Predictive-Coding Diffusion Language Model
382
+ with Precision-Conditioned Second-Order Settling.
383
+ """
384
+
385
+ def __init__(self, config: PCSHOConfig):
386
+ super().__init__()
387
+ self.config = config
388
+ self._inference_updater = None # lazy init for online learning
389
+ self._honest_settling = True # no peeking at x_0 during settling
390
+
391
+ # Embeddings
392
+ self.token_embed = nn.Embedding(config.vocab_size, config.d_model)
393
+ self.pos_embed = nn.Embedding(config.max_seq_len, config.d_model)
394
+ self.time_embed = nn.Embedding(config.n_diffusion_steps + 1, config.d_model)
395
+
396
+ # Bottom-up transformer blocks
397
+ self.forward_blocks = nn.ModuleList([
398
+ BidirectionalTransformerBlock(
399
+ config.d_model, config.n_heads, config.d_ff, config.dropout
400
+ )
401
+ for _ in range(config.n_layers)
402
+ ])
403
+
404
+ # Top-down feedback predictors
405
+ self.feedback_blocks = nn.ModuleList([
406
+ FeedbackPredictor(config.d_model, config.feedback_rank)
407
+ for _ in range(config.n_layers - 1) # No feedback from above top layer
408
+ ])
409
+
410
+ # Precision heads (one per layer for both up and down)
411
+ self.precision_up = nn.ModuleList([
412
+ PrecisionHead(config.d_model, config.n_diffusion_steps)
413
+ for _ in range(config.n_layers)
414
+ ])
415
+ self.precision_down = nn.ModuleList([
416
+ PrecisionHead(config.d_model, config.n_diffusion_steps)
417
+ for _ in range(config.n_layers - 1)
418
+ ])
419
+
420
+ # Readout head
421
+ self.readout_norm = nn.LayerNorm(config.d_model)
422
+ self.readout = nn.Linear(config.d_model, config.vocab_size, bias=False)
423
+
424
+ # Diffusion schedule
425
+ self.schedule = MaskDiffusionSchedule(config.n_diffusion_steps)
426
+
427
+ # Homeostatic plasticity: adaptive per-layer anchoring
428
+ self.register_buffer('layer_rho', torch.full((config.n_layers,), config.anchor_rho))
429
+ self.register_buffer('activity_ema', torch.ones(config.n_layers))
430
+
431
+ # Lightweight settling blocks (linear attention for inner loop)
432
+ self.settling_blocks = nn.ModuleList([
433
+ LightweightSettlingBlock(config.d_model, config.n_heads, config.d_ff, config.dropout)
434
+ for _ in range(config.n_layers)
435
+ ])
436
+
437
+ @property
438
+ def n_active_layers(self) -> int:
439
+ """Number of active layers (supports dynamic insertion/removal)."""
440
+ return len(self.forward_blocks)
441
+
442
+ def embed_input(
443
+ self, x_t: torch.Tensor, t: torch.Tensor
444
+ ) -> torch.Tensor:
445
+ """Create input embeddings from corrupted tokens and timestep."""
446
+ B, S = x_t.shape
447
+ positions = torch.arange(S, device=x_t.device).unsqueeze(0)
448
+
449
+ h = (
450
+ self.token_embed(x_t)
451
+ + self.pos_embed(positions)
452
+ + self.time_embed(t).unsqueeze(1)
453
+ )
454
+ return h
455
+
456
+ def amortized_forward_pass(
457
+ self, h_0: torch.Tensor
458
+ ) -> list[torch.Tensor]:
459
+ """Single feedforward pass for initialization."""
460
+ h_init = [h_0]
461
+ h = h_0
462
+ for block in self.forward_blocks:
463
+ h = block(h)
464
+ h_init.append(h)
465
+ return h_init # len = n_layers + 1
466
+
467
+ def compute_predictions(
468
+ self,
469
+ h: list[torch.Tensor],
470
+ ) -> Tuple[list[torch.Tensor], list[torch.Tensor]]:
471
+ """Compute bottom-up and top-down predictions.
472
+
473
+ Args:
474
+ h: list of hidden states [h_0, h_1, ..., h_L]
475
+
476
+ Returns:
477
+ mu_up: bottom-up proposals [mu_1^up, ..., mu_L^up]
478
+ mu_down: top-down predictions [mu_0^down, ..., mu_{L-1}^down]
479
+ """
480
+ # Bottom-up: mu_l^up = f_l(h_{l-1})
481
+ # Always use forward_blocks — they carry trained weights.
482
+ # settling_blocks (linear attention) are available for explicit long-context mode
483
+ # but should NOT replace forward_blocks during standard settling.
484
+ mu_up = []
485
+ for l, block in enumerate(self.forward_blocks):
486
+ mu_up.append(block(h[l]))
487
+
488
+ # Top-down: mu_l^down = g_l(h_{l+1})
489
+ mu_down = []
490
+ for l, fb_block in enumerate(self.feedback_blocks):
491
+ mu_down.append(fb_block(h[l + 2])) # g_l predicts h_{l+1} from h_{l+2}
492
+
493
+ return mu_up, mu_down
494
+
495
+ def compute_energy_gradient(
496
+ self,
497
+ h: list[torch.Tensor],
498
+ h_init: list[torch.Tensor],
499
+ x_0: torch.Tensor,
500
+ mask: torch.Tensor,
501
+ t: torch.Tensor,
502
+ ) -> Tuple[list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], float]:
503
+ """Compute gradient of E_t w.r.t. each h_l.
504
+
505
+ Returns:
506
+ grad_h: gradient for each layer [grad_h_1, ..., grad_h_L]
507
+ eps_up: bottom-up errors
508
+ eps_down: top-down errors
509
+ energy: scalar energy value
510
+ """
511
+ config = self.config
512
+ L = config.n_layers
513
+
514
+ # Compute predictions
515
+ mu_up, mu_down = self.compute_predictions(h)
516
+
517
+ # Compute errors
518
+ eps_up = [h[l + 1] - mu_up[l] for l in range(L)]
519
+ eps_down = [h[l + 1] - mu_down[l] for l in range(L - 1)]
520
+
521
+ # Compute precisions
522
+ gamma_precs = [] # bottom-up precisions
523
+ lambda_precs = [] # top-down precisions
524
+
525
+ for l in range(L):
526
+ tok_p, ch_p = self.precision_up[l](h[l + 1], t)
527
+ gamma_precs.append((tok_p, ch_p))
528
+
529
+ for l in range(L - 1):
530
+ tok_p, ch_p = self.precision_down[l](h[l + 1], t)
531
+ lambda_precs.append((tok_p, ch_p))
532
+
533
+ # Energy computation — per-token normalization.
534
+ # All terms use mean over D (channels), sum over B×S (batch×sequence).
535
+ # This makes prediction errors and denoising on the SAME per-token scale,
536
+ # so denoising_lambda directly controls their relative importance.
537
+ # Gradients remain large enough (sum over B×S) for effective settling.
538
+ energy = 0.0
539
+
540
+ # Bottom-up consistency: Σ_{B,S} mean_D(0.5 * ||eps||_P^2)
541
+ for l in range(L):
542
+ tok_p, ch_p = gamma_precs[l]
543
+ weighted_err = eps_up[l] * tok_p * ch_p # (B, S, D)
544
+ energy += 0.5 * (weighted_err * eps_up[l]).mean(dim=-1).sum()
545
+
546
+ # Top-down consistency
547
+ for l in range(L - 1):
548
+ tok_p, ch_p = lambda_precs[l]
549
+ weighted_err = eps_down[l] * tok_p * ch_p
550
+ energy += 0.5 * (weighted_err * eps_down[l]).mean(dim=-1).sum()
551
+
552
+ # Denoising loss at top layer — sum over masked tokens (already per-token).
553
+ # HONEST SETTLING: entropy minimization pushes toward confident predictions
554
+ # without peeking at x_0. Cross-entropy with x_0 is used in param updates only.
555
+ logits = self.readout(self.readout_norm(h[L])) # (B, S, V)
556
+ mask_logits = logits[mask]
557
+
558
+ if mask_logits.numel() > 0:
559
+ if self._honest_settling:
560
+ probs = F.softmax(mask_logits, dim=-1)
561
+ entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1) # per-token entropy
562
+ denoising_loss = entropy.sum() # sum over masked tokens
563
+ else:
564
+ mask_targets = x_0[mask]
565
+ denoising_loss = F.cross_entropy(mask_logits, mask_targets, reduction="sum")
566
+
567
+ t_frac = t.float().mean() / config.n_diffusion_steps
568
+ lambda_t = config.denoising_lambda * (1.0 + (1.0 - t_frac))
569
+ energy += lambda_t * denoising_loss
570
+
571
+ # Anchoring: per-token normalization
572
+ for l in range(1, L + 1):
573
+ rho_l = self.layer_rho[min(l - 1, len(self.layer_rho) - 1)]
574
+ energy += rho_l * ((h[l] - h_init[l]) ** 2).mean(dim=-1).sum()
575
+
576
+ # Lateral inhibition
577
+ if config.lateral_inhibition > 0:
578
+ for l in range(1, L + 1):
579
+ h_norm = F.normalize(h[l], dim=-1)
580
+ S_dim = h_norm.shape[1]
581
+ if S_dim > 16:
582
+ idx = torch.randint(0, S_dim, (16,), device=h_norm.device)
583
+ sim = torch.einsum("bsd,btd->bst", h_norm[:, idx], h_norm[:, idx])
584
+ mask_diag = 1.0 - torch.eye(16, device=sim.device).unsqueeze(0)
585
+ energy += config.lateral_inhibition * (F.relu(sim - 0.5) * mask_diag).sum()
586
+
587
+ # Compute gradients via autograd (local operation)
588
+ h_params = [h[l + 1] for l in range(L)]
589
+ grads = torch.autograd.grad(
590
+ energy, h_params, create_graph=False, retain_graph=False
591
+ )
592
+ grad_h = list(grads)
593
+
594
+ return grad_h, eps_up, eps_down, energy.item()
595
+
596
+ def compute_aggregate_precision(
597
+ self,
598
+ h: list[torch.Tensor],
599
+ t: torch.Tensor,
600
+ active_mask: Optional[torch.Tensor] = None,
601
+ ) -> list[float]:
602
+ """Compute aggregate precision per layer for controller."""
603
+ agg_precs = []
604
+ for l in range(self.config.n_layers):
605
+ tok_p, ch_p = self.precision_up[l](h[l + 1], t)
606
+ # Mean precision over active tokens
607
+ if active_mask is not None:
608
+ mean_p = (tok_p.squeeze(-1) * active_mask.float()).sum() / (
609
+ active_mask.float().sum() + 1e-8
610
+ )
611
+ else:
612
+ mean_p = tok_p.mean()
613
+ agg_precs.append(mean_p.item())
614
+ return agg_precs
615
+
616
+ def settling_step(
617
+ self,
618
+ h: list[torch.Tensor],
619
+ v: list[torch.Tensor],
620
+ h_init: list[torch.Tensor],
621
+ x_0: torch.Tensor,
622
+ mask: torch.Tensor,
623
+ t: torch.Tensor,
624
+ active_tokens: Optional[torch.Tensor] = None,
625
+ ) -> Tuple[list[torch.Tensor], list[torch.Tensor], float]:
626
+ """One step of precision-conditioned second-order settling.
627
+
628
+ Args:
629
+ h: current hidden states [h_0, h_1, ..., h_L]
630
+ v: current velocities [v_1, ..., v_L]
631
+ h_init: initialization states
632
+ x_0: clean tokens (for denoising loss)
633
+ mask: masked positions
634
+ t: timesteps
635
+ active_tokens: (B, S) boolean mask of tokens still settling
636
+
637
+ Returns:
638
+ h_new: updated hidden states
639
+ v_new: updated velocities
640
+ energy: current energy value
641
+ """
642
+ config = self.config
643
+ L = config.n_layers
644
+
645
+ # Enable grad for h (layers 1..L) - use enable_grad to work inside no_grad contexts
646
+ with torch.enable_grad():
647
+ for l in range(1, L + 1):
648
+ h[l] = h[l].detach().requires_grad_(True)
649
+
650
+ # Compute energy gradient
651
+ grad_h, eps_up, eps_down, energy = self.compute_energy_gradient(
652
+ h, h_init, x_0, mask, t
653
+ )
654
+
655
+ # Get aggregate precisions for controller
656
+ with torch.no_grad():
657
+ agg_precs = self.compute_aggregate_precision(h, t, active_tokens)
658
+
659
+ h_new = [h[0]] # h_0 is fixed (input embedding)
660
+ v_new = []
661
+
662
+ for l in range(L):
663
+ prec = agg_precs[l] # scalar aggregate precision per layer
664
+
665
+ # TEMPORAL HIERARCHY: per-layer scaling of SHO dynamics
666
+ layer_frac = l / max(L - 1, 1)
667
+ m_l = config.mass * (1.0 + config.mass_scale * layer_frac)
668
+ eta_scale = 1.0 / (1.0 + config.eta_scale * layer_frac)
669
+
670
+ # Scalar precision-conditioned controller (proven in v1)
671
+ eta = config.eta_base * eta_scale / (1.0 + config.c_eta * prec)
672
+ gamma_raw = 2.0 * math.sqrt(m_l * prec)
673
+ gamma = max(config.gamma_min, min(config.gamma_max, gamma_raw))
674
+
675
+ # Second-order update
676
+ v_l_new = (1.0 - gamma) * v[l] - eta * grad_h[l].detach()
677
+ h_l_new = h[l + 1].detach() + v_l_new
678
+
679
+ # Adaptive settling: soft-gate converged tokens (preserves differentiability)
680
+ if active_tokens is not None:
681
+ # Soft gate: sigmoid of (uncertainty - threshold) / temperature
682
+ # active_tokens here is the raw uncertainty tensor
683
+ gate = active_tokens.unsqueeze(-1) # (B, S, 1)
684
+ h_l_new = gate * h_l_new + (1.0 - gate) * h[l + 1].detach()
685
+ v_l_new = gate * v_l_new + (1.0 - gate) * v[l]
686
+
687
+ v_new.append(v_l_new)
688
+ h_new.append(h_l_new)
689
+
690
+ return h_new, v_new, energy
691
+
692
+ def compute_token_uncertainty(
693
+ self, h: list[torch.Tensor], eps_up: Optional[list] = None
694
+ ) -> torch.Tensor:
695
+ """Compute per-token uncertainty for adaptive settling.
696
+
697
+ Args:
698
+ h: hidden states [h_0, ..., h_L]
699
+
700
+ Returns:
701
+ uncertainty: (batch, seq_len) uncertainty scores
702
+ """
703
+ config = self.config
704
+ logits = self.readout(self.readout_norm(h[-1]))
705
+ probs = F.softmax(logits, dim=-1)
706
+
707
+ # Entropy of predicted distribution
708
+ entropy = -(probs * (probs + 1e-10).log()).sum(dim=-1) # (B, S)
709
+
710
+ # Prediction error magnitude (if available)
711
+ if eps_up is not None:
712
+ err_magnitude = sum(
713
+ e.detach().norm(dim=-1) for e in eps_up
714
+ ) * config.error_coeff
715
+ else:
716
+ err_magnitude = 0.0
717
+
718
+ uncertainty = config.entropy_coeff * entropy + err_magnitude
719
+ return uncertainty
720
+
721
+ def settle(
722
+ self,
723
+ h_init: list[torch.Tensor],
724
+ x_0: torch.Tensor,
725
+ mask: torch.Tensor,
726
+ t: torch.Tensor,
727
+ prev_h: Optional[list[torch.Tensor]] = None,
728
+ prev_v: Optional[list[torch.Tensor]] = None,
729
+ return_errors: bool = False,
730
+ ):
731
+ """Run K steps of second-order predictive-coding settling.
732
+
733
+ Args:
734
+ h_init: feedforward initialization
735
+ x_0: clean tokens
736
+ mask: masked positions
737
+ t: timesteps
738
+ prev_h: warm-start hidden states from previous diffusion step
739
+ prev_v: warm-start velocities from previous diffusion step
740
+
741
+ Returns:
742
+ h: settled hidden states
743
+ v: final velocities
744
+ energies: energy trace over settling steps
745
+ """
746
+ config = self.config
747
+ L = config.n_layers
748
+
749
+ # Initialize: warm start or from scratch
750
+ if prev_h is not None and prev_v is not None:
751
+ h = [h_init[0]] + [ph.detach() for ph in prev_h[1:]]
752
+ v = [config.velocity_decay * pv.detach() for pv in prev_v]
753
+ else:
754
+ h = [hi.detach() for hi in h_init]
755
+ v = [torch.zeros_like(h_init[l + 1]) for l in range(L)]
756
+
757
+ energies = []
758
+ self._in_settling = True # flag for lightweight blocks
759
+
760
+ for k in range(config.n_settling_steps):
761
+ # Compute soft gate for adaptive settling (preserves differentiability)
762
+ if k > 0:
763
+ with torch.no_grad():
764
+ uncertainty = self.compute_token_uncertainty(h)
765
+ # Soft sigmoid gate: 1 = fully active, 0 = frozen
766
+ active_tokens = torch.sigmoid(
767
+ (uncertainty - config.settling_threshold) / config.settling_temperature
768
+ )
769
+ else:
770
+ active_tokens = None
771
+
772
+ h, v, energy = self.settling_step(
773
+ h, v, h_init, x_0, mask, t, active_tokens
774
+ )
775
+ energies.append(energy)
776
+
777
+ self._in_settling = False # done settling
778
+
779
+ # HOMEOSTATIC PLASTICITY: adapt per-layer anchoring based on activity
780
+ if self.training and config.homeostatic_rate > 0:
781
+ with torch.no_grad():
782
+ for l in range(min(L, len(self.layer_rho))):
783
+ activity = h[l + 1].detach().norm(dim=-1).mean().item()
784
+ self.activity_ema[l] = 0.99 * self.activity_ema[l] + 0.01 * activity
785
+ # Auto-detect target: use first observed norm as baseline
786
+ target = config.homeostatic_target if config.homeostatic_target > 0 else self.activity_ema[l]
787
+ error = self.activity_ema[l] - target
788
+ self.layer_rho[l] = (self.layer_rho[l] + config.homeostatic_rate * error).clamp(
789
+ config.homeostatic_rho_min, config.homeostatic_rho_max
790
+ )
791
+
792
+ # Stationarity residual — expensive, only compute periodically
793
+ stationarity_residual = None
794
+
795
+ # Optionally compute final prediction errors (for online learning)
796
+ final_errors = None
797
+ if return_errors:
798
+ with torch.no_grad():
799
+ mu_up, mu_down = self.compute_predictions(h)
800
+ eps_up = [h[l + 1] - mu_up[l] for l in range(L)]
801
+ eps_down = [h[l + 1] - mu_down[l] for l in range(L - 1)]
802
+ final_errors = (eps_up, eps_down)
803
+
804
+ return h, v, energies, stationarity_residual, final_errors
805
+
806
+ def forward(
807
+ self,
808
+ x_0: torch.Tensor,
809
+ t: Optional[torch.Tensor] = None,
810
+ ) -> dict:
811
+ """Training forward pass.
812
+
813
+ Uses the amortized forward pass (with full gradient flow) for the
814
+ training loss. Settling runs under no_grad for energy monitoring only.
815
+
816
+ This ensures all forward blocks receive gradient signal from the
817
+ denoising loss, not just inter-layer consistency.
818
+ """
819
+ B, S = x_0.shape
820
+ device = x_0.device
821
+
822
+ # Sample timestep
823
+ if t is None:
824
+ t = torch.randint(1, self.config.n_diffusion_steps + 1, (B,), device=device)
825
+
826
+ # Corrupt
827
+ x_t, mask = self.schedule.corrupt(x_0, t, self.config.mask_token_id)
828
+
829
+ # Embed
830
+ h_0 = self.embed_input(x_t, t)
831
+
832
+ # Amortized forward pass (gradients flow through all forward blocks)
833
+ h_init = self.amortized_forward_pass(h_0)
834
+
835
+ # Decode from amortized pass — all layers get gradient signal
836
+ logits = self.readout(self.readout_norm(h_init[-1]))
837
+
838
+ # Compute denoising loss
839
+ mask_logits = logits[mask]
840
+ mask_targets = x_0[mask]
841
+
842
+ if mask_logits.numel() > 0:
843
+ loss = F.cross_entropy(mask_logits, mask_targets)
844
+ else:
845
+ loss = torch.tensor(0.0, device=device)
846
+
847
+ # Skip settling during training — it's K× extra compute for monitoring only.
848
+ # Settling is used at inference time (generate method).
849
+ return {
850
+ "loss": loss,
851
+ "logits": logits,
852
+ "energies": [],
853
+ "mask": mask,
854
+ "h_settled": None,
855
+ "v_final": None,
856
+ "stationarity_residual": None,
857
+ }
858
+
859
+ def generate(
860
+ self,
861
+ seq_len: int,
862
+ batch_size: int = 1,
863
+ device: str = "cpu",
864
+ online_learn: bool = False,
865
+ ) -> torch.Tensor:
866
+ """Generate sequences via reverse diffusion with state continuation.
867
+
868
+ Args:
869
+ seq_len: length of sequences to generate
870
+ batch_size: number of sequences
871
+ online_learn: if True, apply local weight updates during generation
872
+ (the model learns from its own denoising in real time)
873
+
874
+ Returns:
875
+ x_0: (batch, seq_len) generated token ids
876
+ """
877
+ config = self.config
878
+ T = config.n_diffusion_steps
879
+ mask_id = config.mask_token_id
880
+
881
+ # Initialize online learning if requested
882
+ if online_learn:
883
+ if self._inference_updater is None:
884
+ self._inference_updater = InferenceUpdater(
885
+ self, lr=config.online_learn_lr, grad_clip=config.online_learn_grad_clip
886
+ )
887
+ self._inference_updater.snapshot()
888
+
889
+ # Use no_grad unless online learning needs gradients
890
+ ctx = torch.enable_grad() if online_learn else torch.no_grad()
891
+
892
+ with ctx:
893
+ # Start fully masked
894
+ x = torch.full(
895
+ (batch_size, seq_len), mask_id, dtype=torch.long, device=device
896
+ )
897
+
898
+ prev_h = None
899
+ prev_v = None
900
+
901
+ for t_val in range(T, 0, -1):
902
+ t = torch.full((batch_size,), t_val, dtype=torch.long, device=device)
903
+
904
+ # Only process still-masked positions
905
+ mask = (x == mask_id)
906
+ if not mask.any():
907
+ break
908
+
909
+ # Embed
910
+ h_0 = self.embed_input(x, t)
911
+
912
+ # Amortized pass
913
+ h_init = self.amortized_forward_pass(h_0)
914
+
915
+ # Settle with warm start (return errors if online learning)
916
+ h_settled, v_final, energies, _, errors = self.settle(
917
+ h_init, x, mask, t,
918
+ prev_h=prev_h, prev_v=prev_v,
919
+ return_errors=online_learn,
920
+ )
921
+
922
+ # Online learning: update weights from settling errors
923
+ if online_learn and errors is not None and len(energies) >= 2:
924
+ energy_ratio = energies[-1] / max(energies[0], 1e-8)
925
+ if energy_ratio < config.online_learn_min_energy_ratio:
926
+ eps_up, eps_down = errors
927
+ self._inference_updater.update_from_errors(
928
+ h_settled, eps_up, eps_down, x, mask, t
929
+ )
930
+
931
+ # State continuation
932
+ prev_h = h_settled
933
+ prev_v = v_final
934
+
935
+ # Decode
936
+ logits = self.readout(self.readout_norm(h_settled[-1]))
937
+
938
+ # Determine how many tokens to unmask this step
939
+ n_masked = mask.sum(dim=-1)
940
+ t_prev = t_val - 1
941
+ target_mask_rate = self.schedule.mask_rate(
942
+ torch.tensor([t_prev], device=device)
943
+ ).squeeze()
944
+ n_to_keep_masked = (target_mask_rate * seq_len).long()
945
+
946
+ # For each sequence, unmask highest-confidence masked tokens
947
+ probs = F.softmax(logits, dim=-1)
948
+ confidence = probs.max(dim=-1).values # (B, S)
949
+ confidence[~mask] = float("inf") # Don't re-unmask
950
+
951
+ for b in range(batch_size):
952
+ b_mask_idx = mask[b].nonzero(as_tuple=True)[0]
953
+ if len(b_mask_idx) == 0:
954
+ continue
955
+
956
+ b_conf = confidence[b, b_mask_idx]
957
+ n_to_unmask = max(0, len(b_mask_idx) - n_to_keep_masked.item())
958
+
959
+ if n_to_unmask > 0:
960
+ _, top_idx = b_conf.topk(min(n_to_unmask, len(b_conf)))
961
+ unmask_positions = b_mask_idx[top_idx]
962
+ predicted_tokens = logits[b, unmask_positions].argmax(dim=-1)
963
+ x[b, unmask_positions] = predicted_tokens
964
+
965
+ # Final: unmask any remaining
966
+ still_masked = (x == mask_id)
967
+ if still_masked.any():
968
+ t_final = torch.ones(batch_size, dtype=torch.long, device=device)
969
+ h_0 = self.embed_input(x, t_final)
970
+ h_init = self.amortized_forward_pass(h_0)
971
+ h_settled, _, _, _, _ = self.settle(
972
+ h_init, x, still_masked, t_final,
973
+ prev_h=prev_h, prev_v=prev_v,
974
+ )
975
+ logits = self.readout(self.readout_norm(h_settled[-1]))
976
+ final_tokens = logits[still_masked].argmax(dim=-1)
977
+ x[still_masked] = final_tokens
978
+
979
+ return x
980
+
981
+ def unified_step(
982
+ self,
983
+ h: list[torch.Tensor],
984
+ v: list[torch.Tensor],
985
+ h_init: list[torch.Tensor],
986
+ x_0: torch.Tensor,
987
+ mask: torch.Tensor,
988
+ t: torch.Tensor,
989
+ param_lr: float = 3e-4,
990
+ active_tokens: Optional[torch.Tensor] = None,
991
+ ) -> Tuple[list[torch.Tensor], list[torch.Tensor], float]:
992
+ """One step of UNIFIED inference-and-learning.
993
+
994
+ Updates both hidden states AND parameters simultaneously.
995
+ This is the PC-native mode: settling IS learning.
996
+
997
+ Two-timescale system:
998
+ - Fast: hidden states settle via SHO dynamics (large eta ~0.1)
999
+ - Slow: parameters update via local errors (param_lr ~3e-4)
1000
+
1001
+ The ratio eta/param_lr ≈ 300 satisfies Borkar (2008) two-timescale
1002
+ separation, while param_lr is large enough for practical learning.
1003
+ """
1004
+ config = self.config
1005
+ L = self.n_active_layers
1006
+
1007
+ # Track energies for adaptive two-timescale ratio
1008
+ if not hasattr(self, '_unified_energies'):
1009
+ self._unified_energies = []
1010
+
1011
+ # Enable grad for h
1012
+ with torch.enable_grad():
1013
+ for l in range(1, L + 1):
1014
+ h[l] = h[l].detach().requires_grad_(True)
1015
+
1016
+ # Compute energy gradient w.r.t. hidden states
1017
+ grad_h, eps_up, eps_down, energy = self.compute_energy_gradient(
1018
+ h, h_init, x_0, mask, t
1019
+ )
1020
+
1021
+ self._unified_energies.append(energy)
1022
+
1023
+ # Get precisions for controller
1024
+ with torch.no_grad():
1025
+ agg_precs = self.compute_aggregate_precision(h, t, active_tokens)
1026
+
1027
+ # === FAST PROCESS: update hidden states (settling) ===
1028
+ h_new = [h[0]]
1029
+ v_new = []
1030
+
1031
+ for l in range(L):
1032
+ prec = agg_precs[l]
1033
+ eta = config.eta_base / (1.0 + config.c_eta * prec)
1034
+ gamma_raw = 2.0 * math.sqrt(config.mass * prec)
1035
+ gamma = max(config.gamma_min, min(config.gamma_max, gamma_raw))
1036
+
1037
+ v_l_new = (1.0 - gamma) * v[l] - eta * grad_h[l].detach()
1038
+ h_l_new = h[l + 1].detach() + v_l_new
1039
+
1040
+ if active_tokens is not None:
1041
+ gate = active_tokens.unsqueeze(-1)
1042
+ h_l_new = gate * h_l_new + (1.0 - gate) * h[l + 1].detach()
1043
+ v_l_new = gate * v_l_new + (1.0 - gate) * v[l]
1044
+
1045
+ v_new.append(v_l_new)
1046
+ h_new.append(h_l_new)
1047
+
1048
+ # === SLOW PROCESS: update parameters (learning) ===
1049
+ # param_lr is the external learning rate (e.g. 3e-4)
1050
+ # Two-timescale ratio: eta_base/param_lr ≈ 300 (hidden states settle ~300x faster)
1051
+ scaled_lr = param_lr
1052
+
1053
+ for l in range(L):
1054
+ h_below = h[l].detach()
1055
+ h_current = h[l + 1].detach()
1056
+
1057
+ # Bottom-up: local gradient for forward block
1058
+ mu_up = self.forward_blocks[l](h_below)
1059
+ with torch.no_grad():
1060
+ tok_p, ch_p = self.precision_up[l](h_current, t)
1061
+ eps = h_current - mu_up
1062
+ local_loss = 0.5 * (eps * tok_p * ch_p * eps).mean(dim=-1).sum()
1063
+ local_loss.backward()
1064
+
1065
+ with torch.no_grad():
1066
+ nn.utils.clip_grad_norm_(self.forward_blocks[l].parameters(), 1.0)
1067
+ for p in self.forward_blocks[l].parameters():
1068
+ if p.grad is not None:
1069
+ p.data -= scaled_lr * p.grad
1070
+ p.grad.zero_()
1071
+
1072
+ # Top-down feedback
1073
+ for l in range(L - 1):
1074
+ h_above = h[l + 2].detach()
1075
+ h_current = h[l + 1].detach()
1076
+ mu_down = self.feedback_blocks[l](h_above)
1077
+ with torch.no_grad():
1078
+ tok_p, ch_p = self.precision_down[l](h_current, t)
1079
+ eps = h_current - mu_down
1080
+ local_loss = 0.5 * (eps * tok_p * ch_p * eps).mean(dim=-1).sum()
1081
+ local_loss.backward()
1082
+ with torch.no_grad():
1083
+ nn.utils.clip_grad_norm_(self.feedback_blocks[l].parameters(), 1.0)
1084
+ for p in self.feedback_blocks[l].parameters():
1085
+ if p.grad is not None:
1086
+ p.data -= scaled_lr * p.grad
1087
+ p.grad.zero_()
1088
+
1089
+ # Task-directed update: readout loss propagates through all blocks.
1090
+ # This gives every component (forward blocks, readout, embeddings)
1091
+ # direct task signal — the cross-entropy gradient tells each layer
1092
+ # what features to produce for correct token prediction.
1093
+ # Combined with the local consistency updates above, each forward block
1094
+ # receives TWO gradients: (1) local prediction error and (2) task loss.
1095
+ x_t_input = x_0.clone()
1096
+ x_t_input[mask] = config.mask_token_id
1097
+ h_0_fresh = self.embed_input(x_t_input, t)
1098
+ h_through = h_0_fresh
1099
+ for block in self.forward_blocks:
1100
+ h_through = block(h_through)
1101
+ logits = self.readout(self.readout_norm(h_through))
1102
+ mask_logits = logits[mask]
1103
+ mask_targets = x_0[mask]
1104
+ if mask_logits.numel() > 0:
1105
+ readout_loss = F.cross_entropy(mask_logits, mask_targets)
1106
+ readout_loss.backward()
1107
+ # Update ALL trainable components with task signal
1108
+ all_params = list(self.parameters())
1109
+ with torch.no_grad():
1110
+ nn.utils.clip_grad_norm_(all_params, 1.0)
1111
+ for p in all_params:
1112
+ if p.grad is not None:
1113
+ p.data -= scaled_lr * p.grad
1114
+ p.grad.zero_()
1115
+
1116
+ # Precision heads: calibrate using settled error statistics
1117
+ prec_params = list(self.precision_up.parameters()) + list(self.precision_down.parameters())
1118
+ prec_loss = 0.0
1119
+ for l in range(L):
1120
+ tok_p, ch_p = self.precision_up[l](h[l + 1].detach(), t)
1121
+ eps = (h[l + 1].detach() - self.forward_blocks[l](h[l].detach()).detach())
1122
+ # Gaussian NLL: 0.5 * P * eps^2 - 0.5 * log(P)
1123
+ prec_loss = prec_loss + 0.5 * (eps * tok_p * ch_p * eps).mean(dim=-1).sum()
1124
+ prec_loss = prec_loss - 0.5 * (tok_p.log().mean(dim=-1).sum() + ch_p.log().mean(dim=-1).sum())
1125
+ if isinstance(prec_loss, torch.Tensor) and prec_loss.requires_grad:
1126
+ prec_loss.backward()
1127
+ with torch.no_grad():
1128
+ nn.utils.clip_grad_norm_(prec_params, 1.0)
1129
+ for p in prec_params:
1130
+ if p.grad is not None:
1131
+ p.data -= scaled_lr * 0.1 * p.grad # slower for precision
1132
+ p.grad.zero_()
1133
+
1134
+ return h_new, v_new, energy
1135
+
1136
+ def unified_settle(
1137
+ self,
1138
+ h_init: list[torch.Tensor],
1139
+ x_0: torch.Tensor,
1140
+ mask: torch.Tensor,
1141
+ t: torch.Tensor,
1142
+ param_lr: float = 3e-4,
1143
+ prev_h: Optional[list[torch.Tensor]] = None,
1144
+ prev_v: Optional[list[torch.Tensor]] = None,
1145
+ ) -> Tuple[list[torch.Tensor], list[torch.Tensor], list[float]]:
1146
+ """Run K unified steps (settling + learning simultaneously)."""
1147
+ config = self.config
1148
+ L = self.n_active_layers
1149
+ self._unified_energies = [] # reset
1150
+
1151
+ if prev_h is not None and prev_v is not None:
1152
+ h = [h_init[0]] + [ph.detach() for ph in prev_h[1:]]
1153
+ v = [config.velocity_decay * pv.detach() for pv in prev_v]
1154
+ else:
1155
+ h = [hi.detach() for hi in h_init]
1156
+ v = [torch.zeros_like(h_init[l + 1]) for l in range(L)]
1157
+
1158
+ energies = []
1159
+ for k in range(config.n_settling_steps):
1160
+ if k > 0:
1161
+ with torch.no_grad():
1162
+ uncertainty = self.compute_token_uncertainty(h)
1163
+ active_tokens = torch.sigmoid(
1164
+ (uncertainty - config.settling_threshold) / config.settling_temperature
1165
+ )
1166
+ else:
1167
+ active_tokens = None
1168
+
1169
+ h, v, energy = self.unified_step(
1170
+ h, v, h_init, x_0, mask, t, param_lr, active_tokens
1171
+ )
1172
+ energies.append(energy)
1173
+
1174
+ return h, v, energies
1175
+
1176
+
1177
+ # =============================================================================
1178
+ # Local Parameter Update (for training without global backprop)
1179
+ # =============================================================================
1180
+
1181
+ class LocalParameterUpdater:
1182
+ """Implements local parameter updates for PC-SHO-DLM.
1183
+
1184
+ After settling, updates each layer's parameters using only
1185
+ local prediction errors and local Jacobians.
1186
+ This is the 'globally backprop-free' training procedure.
1187
+ """
1188
+
1189
+ def __init__(
1190
+ self,
1191
+ model: PCSHODLM,
1192
+ lr_forward: float = 1e-4,
1193
+ lr_feedback: float = 1e-4,
1194
+ lr_readout: float = 1e-4,
1195
+ lr_precision: float = 1e-4,
1196
+ ):
1197
+ self.model = model
1198
+ self.lr_forward = lr_forward
1199
+ self.lr_feedback = lr_feedback
1200
+ self.lr_readout = lr_readout
1201
+ self.lr_precision = lr_precision
1202
+
1203
+ # Separate optimizers for each component
1204
+ self.forward_optimizers = [
1205
+ torch.optim.AdamW(block.parameters(), lr=lr_forward)
1206
+ for block in model.forward_blocks
1207
+ ]
1208
+ self.feedback_optimizers = [
1209
+ torch.optim.AdamW(block.parameters(), lr=lr_feedback)
1210
+ for block in model.feedback_blocks
1211
+ ]
1212
+ self.readout_optimizer = torch.optim.AdamW(
1213
+ list(model.readout.parameters()) + list(model.readout_norm.parameters()),
1214
+ lr=lr_readout,
1215
+ )
1216
+ self.precision_optimizer = torch.optim.AdamW(
1217
+ list(model.precision_up.parameters())
1218
+ + list(model.precision_down.parameters()),
1219
+ lr=lr_precision,
1220
+ )
1221
+ self.embed_optimizer = torch.optim.AdamW(
1222
+ list(model.token_embed.parameters())
1223
+ + list(model.pos_embed.parameters())
1224
+ + list(model.time_embed.parameters()),
1225
+ lr=lr_forward,
1226
+ )
1227
+
1228
+ def local_update(
1229
+ self,
1230
+ h_settled: list[torch.Tensor],
1231
+ x_0: torch.Tensor,
1232
+ mask: torch.Tensor,
1233
+ t: torch.Tensor,
1234
+ ):
1235
+ """Perform local parameter updates using settled prediction errors.
1236
+
1237
+ This is the key: we only need local Jacobians, not global backprop.
1238
+ """
1239
+ model = self.model
1240
+ config = model.config
1241
+ L = config.n_layers
1242
+
1243
+ # Re-enable gradients for parameters
1244
+ # Compute fresh predictions from settled states
1245
+ for l in range(L):
1246
+ h_below = h_settled[l].detach()
1247
+ h_current = h_settled[l + 1].detach()
1248
+
1249
+ # Bottom-up: local loss for forward block
1250
+ self.forward_optimizers[l].zero_grad()
1251
+ mu_up = model.forward_blocks[l](h_below)
1252
+
1253
+ # Get precision
1254
+ with torch.no_grad():
1255
+ tok_p, ch_p = model.precision_up[l](h_current, t)
1256
+
1257
+ # Precision-weighted error
1258
+ eps_up = h_current - mu_up
1259
+ local_loss_up = 0.5 * (eps_up * tok_p * ch_p * eps_up).mean(dim=-1).sum()
1260
+ local_loss_up.backward()
1261
+ torch.nn.utils.clip_grad_norm_(model.forward_blocks[l].parameters(), 1.0)
1262
+ self.forward_optimizers[l].step()
1263
+
1264
+ # Top-down: local loss for feedback blocks
1265
+ for l in range(L - 1):
1266
+ self.feedback_optimizers[l].zero_grad()
1267
+ h_above = h_settled[l + 2].detach()
1268
+ h_current = h_settled[l + 1].detach()
1269
+
1270
+ mu_down = model.feedback_blocks[l](h_above)
1271
+
1272
+ with torch.no_grad():
1273
+ tok_p, ch_p = model.precision_down[l](h_current, t)
1274
+
1275
+ eps_down = h_current - mu_down
1276
+ local_loss_down = 0.5 * (eps_down * tok_p * ch_p * eps_down).mean(dim=-1).sum()
1277
+ local_loss_down.backward()
1278
+ torch.nn.utils.clip_grad_norm_(model.feedback_blocks[l].parameters(), 1.0)
1279
+ self.feedback_optimizers[l].step()
1280
+
1281
+ # Readout: denoising loss
1282
+ self.readout_optimizer.zero_grad()
1283
+ logits = model.readout(model.readout_norm(h_settled[L].detach()))
1284
+ mask_logits = logits[mask]
1285
+ mask_targets = x_0[mask]
1286
+ if mask_logits.numel() > 0:
1287
+ readout_loss = F.cross_entropy(mask_logits, mask_targets)
1288
+ readout_loss.backward()
1289
+ self.readout_optimizer.step()
1290
+
1291
+ def step(self, batch: dict):
1292
+ """Complete training step: corrupt, settle, locally update."""
1293
+ model = self.model
1294
+ x_0 = batch["input_ids"]
1295
+ B, S = x_0.shape
1296
+ device = x_0.device
1297
+
1298
+ # Sample timestep
1299
+ t = torch.randint(1, model.config.n_diffusion_steps + 1, (B,), device=device)
1300
+
1301
+ # Corrupt
1302
+ x_t, mask = model.schedule.corrupt(x_0, t, model.config.mask_token_id)
1303
+
1304
+ # Embed
1305
+ self.embed_optimizer.zero_grad()
1306
+ h_0 = model.embed_input(x_t, t)
1307
+
1308
+ # Amortized pass
1309
+ h_init = model.amortized_forward_pass(h_0)
1310
+
1311
+ # Settle (no global gradients needed)
1312
+ with torch.no_grad():
1313
+ h_settled, v_final, energies, stationarity, _ = model.settle(h_init, x_0, mask, t)
1314
+
1315
+ # Local updates
1316
+ self.local_update(h_settled, x_0, mask, t)
1317
+
1318
+ # Update embeddings (token, position, timestep) via readout loss
1319
+ self.embed_optimizer.zero_grad()
1320
+ with torch.enable_grad():
1321
+ h_0_fresh = model.embed_input(x_t, t)
1322
+ h_init_fresh = model.amortized_forward_pass(h_0_fresh)
1323
+ logits_e = model.readout(model.readout_norm(h_init_fresh[model.config.n_layers]))
1324
+ ml_e, mt_e = logits_e[mask], x_0[mask]
1325
+ if ml_e.numel() > 0:
1326
+ embed_loss = F.cross_entropy(ml_e, mt_e)
1327
+ embed_loss.backward()
1328
+ torch.nn.utils.clip_grad_norm_(
1329
+ list(model.token_embed.parameters()) + list(model.pos_embed.parameters()) + list(model.time_embed.parameters()), 1.0
1330
+ )
1331
+ self.embed_optimizer.step()
1332
+
1333
+ # Update precision heads (can use the energy as signal)
1334
+ self.precision_optimizer.zero_grad()
1335
+ # Re-compute energy with precision gradients enabled
1336
+ for l in range(1, model.config.n_layers + 1):
1337
+ h_settled[l] = h_settled[l].detach()
1338
+
1339
+ # Small precision-calibration step
1340
+ mu_up_list, mu_down_list = model.compute_predictions(h_settled)
1341
+ prec_loss = 0.0
1342
+ for l in range(model.config.n_layers):
1343
+ tok_p, ch_p = model.precision_up[l](h_settled[l + 1].detach(), t)
1344
+ eps = (h_settled[l + 1].detach() - mu_up_list[l].detach())
1345
+ # Log-likelihood of the error under the precision model
1346
+ prec_loss += 0.5 * (eps * tok_p * ch_p * eps).mean()
1347
+ prec_loss -= 0.5 * (tok_p.log().mean() + ch_p.log().mean())
1348
+
1349
+ if isinstance(prec_loss, torch.Tensor) and prec_loss.requires_grad:
1350
+ prec_loss.backward()
1351
+ self.precision_optimizer.step()
1352
+
1353
+ # Compute readout loss for monitoring (not used for training)
1354
+ with torch.no_grad():
1355
+ logits = model.readout(model.readout_norm(h_settled[model.config.n_layers]))
1356
+ mask_logits = logits[mask]
1357
+ mask_targets = x_0[mask]
1358
+ if mask_logits.numel() > 0:
1359
+ readout_loss = F.cross_entropy(mask_logits, mask_targets).item()
1360
+ else:
1361
+ readout_loss = 0.0
1362
+
1363
+ return {
1364
+ "loss": readout_loss,
1365
+ "energies": energies,
1366
+ "stationarity": stationarity if stationarity else 0.0,
1367
+ "n_masked": mask.sum().item(),
1368
+ }
1369
+
1370
+
1371
+ # =============================================================================
1372
+ # Online Learning During Inference
1373
+ # =============================================================================
1374
+
1375
+ class InferenceUpdater:
1376
+ """Lightweight local updater for online learning during generation.
1377
+
1378
+ Uses prediction errors already computed during settling — no additional
1379
+ forward/backward passes needed. This is the key PC advantage: the settling
1380
+ process computes everything required for weight updates.
1381
+
1382
+ Uses SGD with momentum (not AdamW) to minimize memory overhead at inference.
1383
+ """
1384
+
1385
+ def __init__(self, model: PCSHODLM, lr: float = 1e-5, grad_clip: float = 0.1):
1386
+ self.model = model
1387
+ self.lr = lr
1388
+ self.grad_clip = grad_clip
1389
+ self._snapshot = None
1390
+
1391
+ # Per-layer SGD optimizers (lightweight)
1392
+ self.forward_opts = [
1393
+ torch.optim.SGD(block.parameters(), lr=lr, momentum=0.9)
1394
+ for block in model.forward_blocks
1395
+ ]
1396
+ self.feedback_opts = [
1397
+ torch.optim.SGD(block.parameters(), lr=lr, momentum=0.9)
1398
+ for block in model.feedback_blocks
1399
+ ]
1400
+ self.readout_opt = torch.optim.SGD(
1401
+ list(model.readout.parameters()) + list(model.readout_norm.parameters()),
1402
+ lr=lr, momentum=0.9,
1403
+ )
1404
+
1405
+ def snapshot(self):
1406
+ """Save parameter snapshot for potential rollback."""
1407
+ self._snapshot = {
1408
+ name: p.data.clone() for name, p in self.model.named_parameters()
1409
+ }
1410
+
1411
+ def rollback(self):
1412
+ """Restore parameters from snapshot."""
1413
+ if self._snapshot is None:
1414
+ return
1415
+ for name, p in self.model.named_parameters():
1416
+ if name in self._snapshot:
1417
+ p.data.copy_(self._snapshot[name])
1418
+
1419
+ def update_from_errors(
1420
+ self,
1421
+ h_settled: list[torch.Tensor],
1422
+ eps_up: list[torch.Tensor],
1423
+ eps_down: list[torch.Tensor],
1424
+ x_0: torch.Tensor,
1425
+ mask: torch.Tensor,
1426
+ t: torch.Tensor,
1427
+ ) -> float:
1428
+ """Apply local weight updates using pre-computed prediction errors.
1429
+
1430
+ Returns total update norm for monitoring.
1431
+ """
1432
+ model = self.model
1433
+ L = model.n_active_layers
1434
+ total_norm = 0.0
1435
+
1436
+ # Bottom-up blocks: minimize precision-weighted eps_up
1437
+ for l in range(L):
1438
+ self.forward_opts[l].zero_grad()
1439
+ h_below = h_settled[l].detach()
1440
+ mu_up = model.forward_blocks[l](h_below)
1441
+
1442
+ with torch.no_grad():
1443
+ tok_p, ch_p = model.precision_up[l](h_settled[l + 1].detach(), t)
1444
+
1445
+ eps = h_settled[l + 1].detach() - mu_up
1446
+ loss = 0.5 * (eps * tok_p * ch_p * eps).sum() / x_0.shape[0]
1447
+ loss.backward()
1448
+ nn.utils.clip_grad_norm_(model.forward_blocks[l].parameters(), self.grad_clip)
1449
+ self.forward_opts[l].step()
1450
+ total_norm += loss.item()
1451
+
1452
+ # Top-down blocks
1453
+ for l in range(L - 1):
1454
+ self.feedback_opts[l].zero_grad()
1455
+ h_above = h_settled[l + 2].detach()
1456
+ mu_down = model.feedback_blocks[l](h_above)
1457
+
1458
+ with torch.no_grad():
1459
+ tok_p, ch_p = model.precision_down[l](h_settled[l + 1].detach(), t)
1460
+
1461
+ eps = h_settled[l + 1].detach() - mu_down
1462
+ loss = 0.5 * (eps * tok_p * ch_p * eps).sum() / x_0.shape[0]
1463
+ loss.backward()
1464
+ nn.utils.clip_grad_norm_(model.feedback_blocks[l].parameters(), self.grad_clip)
1465
+ self.feedback_opts[l].step()
1466
+
1467
+ # Readout
1468
+ self.readout_opt.zero_grad()
1469
+ logits = model.readout(model.readout_norm(h_settled[L].detach()))
1470
+ mask_logits = logits[mask]
1471
+ mask_targets = x_0[mask]
1472
+ if mask_logits.numel() > 0:
1473
+ rl = F.cross_entropy(mask_logits, mask_targets)
1474
+ rl.backward()
1475
+ nn.utils.clip_grad_norm_(
1476
+ list(model.readout.parameters()) + list(model.readout_norm.parameters()),
1477
+ self.grad_clip,
1478
+ )
1479
+ self.readout_opt.step()
1480
+
1481
+ return total_norm
1482
+
1483
+
1484
+ # =============================================================================
1485
+ # Dynamic Layer Manager
1486
+ # =============================================================================
1487
+
1488
+ class DynamicLayerManager:
1489
+ """Manages runtime insertion and removal of layers.
1490
+
1491
+ Exploits PC locality: a new layer only needs to become consistent
1492
+ with its immediate neighbors. Distant layers are unaffected.
1493
+ """
1494
+
1495
+ def __init__(self, model: PCSHODLM):
1496
+ self.model = model
1497
+ self.history: list[dict] = []
1498
+
1499
+ def insert_layer(self, position: int, init_strategy: str = "identity") -> None:
1500
+ """Insert a new layer at the given position.
1501
+
1502
+ Args:
1503
+ position: index in the layer stack (0-indexed)
1504
+ init_strategy: "identity" (near-identity init) or "random"
1505
+ """
1506
+ model = self.model
1507
+ config = model.config
1508
+ device = next(model.parameters()).device
1509
+
1510
+ # Create new modules
1511
+ new_forward = BidirectionalTransformerBlock(
1512
+ config.d_model, config.n_heads, config.d_ff, config.dropout
1513
+ ).to(device)
1514
+ new_feedback = FeedbackPredictor(config.d_model, config.feedback_rank).to(device)
1515
+ new_prec_up = PrecisionHead(config.d_model, config.n_diffusion_steps).to(device)
1516
+ new_prec_down = PrecisionHead(config.d_model, config.n_diffusion_steps).to(device)
1517
+
1518
+ # Identity initialization: zero out the residual path so output ≈ input
1519
+ if init_strategy == "identity":
1520
+ with torch.no_grad():
1521
+ # Zero the final linear in the feedforward and attention output
1522
+ for name, param in new_forward.named_parameters():
1523
+ if "ff.3" in name or "ff.2" in name: # last linear in FF
1524
+ param.zero_()
1525
+
1526
+ # Insert into module lists
1527
+ model.forward_blocks.insert(position, new_forward)
1528
+ model.precision_up.insert(position, new_prec_up)
1529
+
1530
+ # Feedback and precision_down have n_layers-1 entries
1531
+ fb_pos = min(position, len(model.feedback_blocks))
1532
+ model.feedback_blocks.insert(fb_pos, new_feedback)
1533
+ model.precision_down.insert(min(position, len(model.precision_down)), new_prec_down)
1534
+
1535
+ # Update config
1536
+ config.n_layers += 1
1537
+
1538
+ self.history.append({
1539
+ "action": "insert",
1540
+ "position": position,
1541
+ "strategy": init_strategy,
1542
+ "new_n_layers": config.n_layers,
1543
+ })
1544
+
1545
+ def remove_layer(self, position: int) -> None:
1546
+ """Remove a layer at the given position."""
1547
+ model = self.model
1548
+ config = model.config
1549
+
1550
+ if config.n_layers <= 2:
1551
+ raise ValueError("Cannot remove layer: minimum 2 layers required")
1552
+
1553
+ del model.forward_blocks[position]
1554
+ del model.precision_up[position]
1555
+
1556
+ fb_pos = min(position, len(model.feedback_blocks) - 1)
1557
+ del model.feedback_blocks[fb_pos]
1558
+ del model.precision_down[min(position, len(model.precision_down) - 1)]
1559
+
1560
+ config.n_layers -= 1
1561
+
1562
+ self.history.append({
1563
+ "action": "remove",
1564
+ "position": position,
1565
+ "new_n_layers": config.n_layers,
1566
+ })
1567
+
1568
+
1569
+ # =============================================================================
1570
+ # Utility: Model Size
1571
+ # =============================================================================
1572
+
1573
+ def count_parameters(model: nn.Module) -> int:
1574
+ return sum(p.numel() for p in model.parameters() if p.requires_grad)
1575
+
1576
+
1577
+ if __name__ == "__main__":
1578
+ # Quick sanity check
1579
+ config = PCSHOConfig(
1580
+ vocab_size=1000,
1581
+ max_seq_len=128,
1582
+ d_model=256,
1583
+ n_heads=4,
1584
+ n_layers=4,
1585
+ d_ff=512,
1586
+ n_diffusion_steps=100,
1587
+ n_settling_steps=4,
1588
+ )
1589
+
1590
+ model = PCSHODLM(config)
1591
+ print(f"PC-SHO-DLM parameters: {count_parameters(model):,}")
1592
+
1593
+ # Test forward pass
1594
+ x_0 = torch.randint(1, 1000, (2, 64))
1595
+ output = model(x_0)
1596
+ print(f"Loss: {output['loss'].item():.4f}")
1597
+ print(f"Energy trace: {output['energies']}")
1598
+ print(f"Logits shape: {output['logits'].shape}")
1599
+
1600
+ # Test generation
1601
+ generated = model.generate(seq_len=32, batch_size=2)
1602
+ print(f"Generated shape: {generated.shape}")
1603
+ print(f"Generated sample: {generated[0, :10].tolist()}")