KitsuVp commited on
Commit
3e72e5e
·
verified ·
1 Parent(s): a0a6ac4

Update modeling_neollm.py

Browse files
Files changed (1) hide show
  1. modeling_neollm.py +903 -79
modeling_neollm.py CHANGED
@@ -20,7 +20,17 @@ Attention stack (orthogonal, all active simultaneously when enabled):
20
  moving-average bias that prevents collapse. Reduces first-token bias,
21
  increases attention entropy, and is complementary to Gated Attention
22
  (Bae et al. 2026, Table 2: Affine-Scaled + Gated > either alone).
23
- Only active in eager attention mode (flash kernels lack weight access).
 
 
 
 
 
 
 
 
 
 
24
 
25
  References:
26
  FANformer: "FANformer: Improving Large Language Models Through Effective
@@ -42,7 +52,8 @@ References:
42
  """
43
 
44
  import math
45
- from typing import Callable, Optional, Union, Tuple
 
46
 
47
  import torch
48
  import torch.nn.functional as F
@@ -60,14 +71,277 @@ from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_u
60
  from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
61
  from transformers.processing_utils import Unpack
62
  from transformers.utils import TransformersKwargs, logging
63
- from .configuration_neollm import NeoLLMConfig
64
 
65
  from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
66
 
67
  logger = logging.get_logger(__name__)
68
 
69
 
70
- # ==================== LEARNABLE MULTIPLIERS ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  class ScalarMultiplier(nn.Module):
73
  """
@@ -454,6 +728,7 @@ class LeviathanGenerator(nn.Module):
454
  self,
455
  token_ids: torch.Tensor,
456
  return_internals: bool = False,
 
457
  ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, torch.Tensor]]:
458
  """
459
  Generate embeddings from discrete token indices.
@@ -478,6 +753,9 @@ class LeviathanGenerator(nn.Module):
478
  token_ids: (batch, seq_len) or (seq_len,)
479
  return_internals: if True, also return z_tilde and B_vals for
480
  reuse by JTok-M surfaces in every decoder layer.
 
 
 
481
  Returns:
482
  embeddings [*token_ids.shape, hidden_size],
483
  or (embeddings, z_tilde [N, d_seed], B_vals [N, d_seed, n_knots])
@@ -494,12 +772,19 @@ class LeviathanGenerator(nn.Module):
494
  for r in range(self.k):
495
  z = z + self.codebooks[r][coords_flat[:, r]]
496
 
 
 
 
497
  # ── JTok-M shared path ────────────────────────────────────────────
498
  # Produces z_tilde and B_vals consumed by every decoder layer's
499
  # JTok-M module. This path is unchanged and uses fixed scalar scale.
500
  z_tilde = torch.sigmoid(self.seed_norm(self.seed_proj(z))) # [N, d_seed]
501
  B_vals = self._bspline_basis(z_tilde.clamp(0.0, 1.0)) # [N, d_seed, n_knots]
502
 
 
 
 
 
503
  # ── Per-head generator path (fully vectorized, 6 kernels) ────────
504
  # All 8 heads are processed simultaneously. No Python loop.
505
  # Maximum intermediate tensor [N, M, d_seed, n_knots] appears once.
@@ -509,6 +794,9 @@ class LeviathanGenerator(nn.Module):
509
  z_all = F.linear(z.to(target_dtype), self.head_proj_weight)
510
  z_all = z_all.view(N, self.num_modes, self.d_seed) # [N, M, d_seed]
511
 
 
 
 
512
  # Kernel 2: per-head LayerNorm + sigmoid(x/2)
513
  # Manual LN over last dim with independent weight/bias per head.
514
  # Mathematically identical to 8 separate nn.LayerNorm(d_seed).
@@ -520,6 +808,9 @@ class LeviathanGenerator(nn.Module):
520
  + self.head_norm_bias.unsqueeze(0)
521
  z_all = torch.sigmoid(z_all / 2.0) # [N, M, d_seed]
522
 
 
 
 
523
  # Kernel 3: vectorized B-spline basis for all heads
524
  # head_scale [M, d_seed] is used inside _bspline_basis_all_heads
525
  B_all = self._bspline_basis_all_heads(
@@ -529,6 +820,9 @@ class LeviathanGenerator(nn.Module):
529
  # Kernel 4: vectorized KHRONOS tensor product for all heads
530
  modes_all = self._khronos_all_heads(B_all) # [N, M, krank]
531
 
 
 
 
532
  # Kernel 5: project all heads to hidden_size and sum
533
  # einsum: token n, head m, krank k → hidden d (summed over m)
534
  # head_out_weight [M, krank, hidden_size]
@@ -541,6 +835,9 @@ class LeviathanGenerator(nn.Module):
541
  # No W_res — confirmed absent in the authors' implementation
542
  e = e.reshape(*orig_shape, self.hidden_size)
543
 
 
 
 
544
  if return_internals:
545
  return e, z_tilde, B_vals
546
  return e
@@ -627,6 +924,7 @@ class LeviathanJTokM(nn.Module):
627
  B_vals: torch.Tensor,
628
  z_tilde: torch.Tensor,
629
  target_dtype: torch.dtype,
 
630
  ) -> torch.Tensor:
631
  """
632
  Evaluate all n_e surfaces vectorized over the full token batch.
@@ -638,9 +936,10 @@ class LeviathanJTokM(nn.Module):
638
  All shapes are static → torch.compile compatible.
639
 
640
  Args:
641
- B_vals: [N, d_seed, n_knots] float32
642
- z_tilde: [N, d_seed]
643
  target_dtype: model dtype
 
644
  Returns:
645
  surfaces: [N, n_e, D]
646
  """
@@ -669,7 +968,12 @@ class LeviathanJTokM(nn.Module):
669
  z = z_tilde.to(target_dtype)
670
  out_res = torch.einsum("nd,idc->nic", z, self.W_res.to(target_dtype))
671
 
672
- return out_modes + out_res # [N, n_e, D]
 
 
 
 
 
673
 
674
  # ── Router ────────────────────────────────────────────────────────────
675
 
@@ -680,6 +984,7 @@ class LeviathanJTokM(nn.Module):
680
  self,
681
  h_tilde: torch.Tensor,
682
  surfaces: torch.Tensor,
 
683
  ) -> Tuple[torch.Tensor, torch.Tensor]:
684
  """
685
  Context-dependent routing over h_tilde (hidden state after attention).
@@ -698,6 +1003,7 @@ class LeviathanJTokM(nn.Module):
698
  Args:
699
  h_tilde: [N, D] — hidden state after attention (before MLP)
700
  surfaces: [N, n_e, D]
 
701
  Returns:
702
  mixed: [N, D]
703
  aux_stats: (p_sum [n_e], f_sum [n_e], N) for loss accumulation
@@ -720,6 +1026,12 @@ class LeviathanJTokM(nn.Module):
720
  selected = surfaces.gather(dim=1, index=idx_exp) # [N, K, D]
721
  mixed = (w.unsqueeze(-1) * selected).sum(dim=1) # [N, D]
722
 
 
 
 
 
 
 
723
  # Load-balancing statistics for aux loss (Appendix B, Yang et al. 2026)
724
  # p_i = mean routing probability over batch
725
  # f_i = fraction of tokens actually routed to i
@@ -741,31 +1053,41 @@ class LeviathanJTokM(nn.Module):
741
  h_tilde: torch.Tensor,
742
  z_tilde: torch.Tensor,
743
  B_vals: torch.Tensor,
 
744
  ) -> Tuple[torch.Tensor, Tuple]:
745
  """
746
  Compute additive JTok-M residual for one decoder layer.
747
 
748
  Args:
749
- h_tilde: [N, D] hidden state after attention (before MLP)
750
- z_tilde: [N, d_seed] latent coordinate from generator
751
- B_vals: [N, d_seed, n_k] B-spline basis (computed once, reused)
 
 
752
  Returns:
753
- delta_r: [N, D] additive residual (already scaled)
754
  aux_stats: tuple for accumulating load-balance loss
755
  """
756
  target_dtype = h_tilde.dtype
757
 
758
  # All n_e surfaces in one vectorized pass
759
- surfaces = self._eval_surfaces(B_vals, z_tilde, target_dtype) # [N, n_e, D]
760
 
761
  # Context-dependent routing
762
- mixed, aux_stats = self._route_and_mix(h_tilde, surfaces) # [N, D]
763
 
764
  # Normalise direction, apply scaler, scale with 1/√(2ℓ)
765
  # Norm_ε decouples direction from magnitude (JTok Appendix D.2)
766
  mixed_norm = mixed / (mixed.norm(dim=-1, keepdim=True) + self.norm_eps)
767
  delta_r = self.lns_scale * self.scaler * mixed_norm # [N, D]
768
 
 
 
 
 
 
 
 
769
  return delta_r, aux_stats
770
 
771
 
@@ -825,10 +1147,20 @@ class FANLayer(nn.Module):
825
  if self.input_linear.bias is not None:
826
  nn.init.zeros_(self.input_linear.bias)
827
 
828
- def forward(self, x: torch.Tensor) -> torch.Tensor:
 
 
 
 
829
  pg = self.input_linear(x)
830
  p, g = torch.split(pg, [self.p_output_dim, self.g_output_dim], dim=-1)
831
- return torch.cat([torch.cos(p), torch.sin(p), g], dim=-1)
 
 
 
 
 
 
832
 
833
 
834
  class LNS(nn.Module):
@@ -853,8 +1185,17 @@ class GPAS(nn.Module):
853
  self.d_model = d_model
854
  self.alpha = nn.Parameter(torch.zeros(1))
855
 
856
- def forward(self, x: torch.Tensor) -> torch.Tensor:
857
- return x - F.silu(self.alpha) * x.detach()
 
 
 
 
 
 
 
 
 
858
 
859
 
860
  class SeeDNorm(nn.Module):
@@ -883,7 +1224,11 @@ class SeeDNorm(nn.Module):
883
  def _rms_norm(self, x: torch.Tensor) -> torch.Tensor:
884
  return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
885
 
886
- def forward(self, x: torch.Tensor) -> torch.Tensor:
 
 
 
 
887
  x_for_dynamic = F.dropout(x, p=self.dropout_input)
888
  rescale_factor = torch.tanh(
889
  torch.sum(x_for_dynamic * self.beta, dim=-1, keepdim=True)
@@ -891,7 +1236,13 @@ class SeeDNorm(nn.Module):
891
  dynamic_scale = rescale_factor * self.alpha + self.gamma
892
  x_normalized = self._rms_norm(x.float())
893
  x_normalized = F.dropout(x_normalized, p=self.dropout_hidden)
894
- return (x_normalized * dynamic_scale.float()).type_as(x)
 
 
 
 
 
 
895
 
896
  def extra_repr(self) -> str:
897
  return (f"dim={self.dim}, eps={self.eps}, "
@@ -1107,6 +1458,7 @@ def affine_scaled_eager_attention_forward(
1107
  alpha: torch.Tensor,
1108
  beta: torch.Tensor,
1109
  dropout: float = 0.0,
 
1110
  **kwargs: Unpack[TransformersKwargs],
1111
  ):
1112
  """
@@ -1129,8 +1481,10 @@ def affine_scaled_eager_attention_forward(
1129
  Reference: Bae et al. (2026), Affine-Scaled Attention, Eq. 6–8.
1130
 
1131
  Args:
1132
- alpha: [batch, num_heads, seq_q, 1] — input-dependent scale per query
1133
- beta: [batch, num_heads, seq_q, 1] — input-dependent bias per query
 
 
1134
  """
1135
  key_states = repeat_kv(key, module.num_key_value_groups)
1136
  value_states = repeat_kv(value, module.num_key_value_groups)
@@ -1139,19 +1493,137 @@ def affine_scaled_eager_attention_forward(
1139
  if attention_mask is not None:
1140
  attn_weights = attn_weights + attention_mask[:, :, :, : key_states.shape[-2]]
1141
 
1142
- attn_weights = nn.functional.softmax(
1143
  attn_weights, dim=-1, dtype=torch.float32
1144
  ).to(query.dtype)
1145
 
 
 
 
1146
  # Affine reweighting: relaxes the unit-sum constraint of softmax.
1147
  # α ∈ [0, 1] scales down the softmax distribution (input-adaptively per head).
1148
  # β offsets to prevent collapse when α deviates from its running mean.
1149
  # Shapes: α, β are [B, H, S_q, 1], attn_weights is [B, H, S_q, S_k] → broadcast.
1150
- attn_weights = alpha * attn_weights + beta
1151
 
1152
- attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
1153
- attn_output = torch.matmul(attn_weights, value_states).transpose(1, 2).contiguous()
1154
- return attn_output, attn_weights
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1155
 
1156
 
1157
  class NeoLLMAttention(nn.Module):
@@ -1336,19 +1808,49 @@ class NeoLLMAttention(nn.Module):
1336
  self.direction_vecs = None
1337
  self.direction_router = None
1338
 
1339
- def _apply_momentum_attention(self, q, k):
 
 
 
 
 
1340
  if not self.use_momentum_attention or self.momentum_gamma == 0.0:
1341
  return q, k
1342
- return q + self.momentum_gamma * causal_first_difference(q), \
1343
- k + self.momentum_gamma * causal_first_difference(k)
1344
-
1345
- def _apply_mea_head_mixing(self, k, v):
 
 
 
 
 
 
 
 
 
 
 
 
 
1346
  if not self.use_mea_attention:
1347
  return k, v
1348
- return head_linear_compose(k, self.mea_key_mix).contiguous(), \
1349
- head_linear_compose(v, self.mea_value_mix).contiguous()
1350
-
1351
- def _apply_lucid_preconditioner(self, k, v, attention_mask):
 
 
 
 
 
 
 
 
 
 
 
 
1352
  if not self.use_lucid_attention:
1353
  return v.contiguous()
1354
  key_rn = rms_key_unit_norm(k, eps=self.lucid_attention_eps)
@@ -1360,15 +1862,20 @@ class NeoLLMAttention(nn.Module):
1360
  eye = torch.eye(prec.shape[-1], device=prec.device, dtype=prec.dtype).view(
1361
  1, 1, prec.shape[-1], prec.shape[-1]
1362
  )
1363
- prec = prec + eye * (1.0 - prec.diagonal(dim1=-2, dim2=-1).unsqueeze(-1))
1364
- return torch.linalg.solve_triangular(
1365
  prec, v.float(), upper=False, unitriangular=True
1366
  ).to(v.dtype).contiguous()
 
 
 
 
1367
 
1368
  def _apply_directional_routing(
1369
  self,
1370
  attn_out: torch.Tensor,
1371
  hidden_states: torch.Tensor,
 
1372
  ) -> torch.Tensor:
1373
  """
1374
  Directional suppression at position C (post-XSA, pre-reshape).
@@ -1377,6 +1884,7 @@ class NeoLLMAttention(nn.Module):
1377
  attn_out: [B, S, H, d_head] — output after XSA and SeeDNorm.
1378
  hidden_states: [B, S, hidden_size] — pre-FAN residual stream,
1379
  used as router input (same as paper's x_i).
 
1380
  Returns:
1381
  [B, S, H, d_head] with selected directional components suppressed.
1382
  """
@@ -1416,8 +1924,17 @@ class NeoLLMAttention(nn.Module):
1416
  # Σ_k weighted_{h,k} · d_{h,k}:
1417
  # weighted [B, S, H, K] × d [H, K, D] → [B, S, H, D]
1418
  suppression = torch.einsum("bshk,hkd->bshd", weighted, d)
 
 
 
 
 
 
 
 
 
1419
 
1420
- return attn_out - suppression
1421
 
1422
  def forward(
1423
  self,
@@ -1425,11 +1942,13 @@ class NeoLLMAttention(nn.Module):
1425
  position_embeddings: tuple[torch.Tensor, torch.Tensor],
1426
  attention_mask: Optional[torch.Tensor] = None,
1427
  first_layer_fan: Optional[torch.Tensor] = None,
 
1428
  **kwargs: Unpack[FlashAttentionKwargs],
1429
  ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
1430
  input_shape = hidden_states.shape[:-1]
1431
 
1432
- h_fan = self.fan_layer(hidden_states)
 
1433
  if first_layer_fan is not None:
1434
  h_fan = self.lambda_1 * first_layer_fan + self.lambda_2 * h_fan
1435
  current_layer_fan = h_fan.clone()
@@ -1443,27 +1962,42 @@ class NeoLLMAttention(nn.Module):
1443
  )
1444
  gate = gate.reshape(*input_shape, -1)
1445
 
 
 
 
 
1446
  q = self.q_norm(q_raw.view(query_shape)).transpose(1, 2)
1447
  k = self.k_norm(self.k_proj(h_fan).view(kv_shape)).transpose(1, 2)
1448
  v = self.v_proj(h_fan).view(kv_shape).transpose(1, 2)
1449
 
 
 
 
 
 
1450
  cos, sin = position_embeddings
1451
  q, k = apply_rotary_pos_emb(q, k, cos, sin)
1452
- q, k = self._apply_momentum_attention(q, k)
1453
- k, v = self._apply_mea_head_mixing(k, v)
1454
- v = self._apply_lucid_preconditioner(k, v, attention_mask)
 
 
 
 
 
1455
 
1456
  # Capture v_ref for XSA after MEA mixing and LUCID preconditioning.
1457
  # This is the vector that actually participated in SDPA aggregation.
1458
  v_ref = v if self.use_xsa else None
1459
 
1460
  # ── Affine-Scaled Attention ───────────────────────────────────────
 
 
 
 
1461
  alpha = None
1462
  beta = None
1463
- use_affine = (
1464
- self.use_affine_scaled_attention
1465
- and self.config._attn_implementation == "eager"
1466
- )
1467
  if use_affine:
1468
  alpha = linear_clipping(self.alpha_proj(hidden_states)) # [B, S, H]
1469
  alpha = alpha.permute(0, 2, 1).unsqueeze(-1) # [B, H, S, 1]
@@ -1476,14 +2010,31 @@ class NeoLLMAttention(nn.Module):
1476
  self.affine_momentum * self.alpha_ma
1477
  + (1.0 - self.affine_momentum) * batch_mean
1478
  )
 
 
 
 
1479
 
1480
  if use_affine:
1481
- attn_out, attn_weights = affine_scaled_eager_attention_forward(
1482
- self, q, k, v, attention_mask,
1483
- scaling=self.scaling, alpha=alpha, beta=beta,
1484
- dropout=0.0 if not self.training else self.attention_dropout,
1485
- **kwargs,
1486
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
1487
  else:
1488
  attn_fn = eager_attention_forward
1489
  if self.config._attn_implementation != "eager":
@@ -1493,10 +2044,19 @@ class NeoLLMAttention(nn.Module):
1493
  dropout=0.0 if not self.training else self.attention_dropout,
1494
  scaling=self.scaling, **kwargs,
1495
  )
 
 
 
 
 
 
 
1496
 
1497
  attn_out = attn_out.reshape(*input_shape, -1, self.head_dim)
1498
  if self.use_mea_attention:
1499
  attn_out = self.mea_output_norm(attn_out)
 
 
1500
 
1501
  # ── Exclusive Self Attention (position B, pre-routing) ────────────
1502
  # Removes auto-position component before directional routing so that
@@ -1507,7 +2067,11 @@ class NeoLLMAttention(nn.Module):
1507
  v_ref_t = v_ref_t.to(attn_out.dtype)
1508
  proj = (attn_out * v_ref_t).sum(dim=-1, keepdim=True)
1509
  norm_sq = (v_ref_t * v_ref_t).sum(dim=-1, keepdim=True).clamp(min=self.xsa_eps)
1510
- attn_out = attn_out - (proj / norm_sq) * v_ref_t
 
 
 
 
1511
 
1512
  # ── Directional Routing (position C, post-XSA, pre-reshape) ──────
1513
  # Suppresses cross-domain interference directions from the head output.
@@ -1515,14 +2079,26 @@ class NeoLLMAttention(nn.Module):
1515
  # When use_xsa=False: directions span full head-space (no XSA pre-clean).
1516
  # When use_directional_routing=False: this block is skipped entirely.
1517
  if self.use_directional_routing:
1518
- attn_out = self._apply_directional_routing(attn_out, hidden_states)
 
 
1519
 
1520
  # ── Reshape → o_proj → Gated Attention gate → dropout ────────────
1521
- attn_out = attn_out.reshape(*input_shape, -1).contiguous()
1522
- attn_out = self.o_proj(attn_out * torch.sigmoid(gate))
1523
- attn_out = self.dropout(attn_out)
 
 
 
 
 
 
 
1524
 
1525
- return attn_out, attn_weights, current_layer_fan
 
 
 
1526
  class PolyNorm(nn.Module):
1527
  def __init__(
1528
  self,
@@ -1561,7 +2137,11 @@ class PolyNorm(nn.Module):
1561
  out = branch - alpha.to(branch.dtype) * proj_coeff * ref
1562
  return self._norm(out)
1563
 
1564
- def forward(self, x):
 
 
 
 
1565
  # Caché de potencias: x_sq reutilizado en x1 y x2; x_cu = x·x_sq evita pow(3)
1566
  x_sq = x.pow(2)
1567
  x_cu = x * x_sq
@@ -1571,9 +2151,20 @@ class PolyNorm(nn.Module):
1571
  x2 = x_sq * (x_sq * x_sq).mean(-1, keepdim=True).add(self.eps).rsqrt()
1572
  x3 = x_cu * (x_cu * x_cu).mean(-1, keepdim=True).add(self.eps).rsqrt()
1573
 
 
 
 
 
 
1574
  # Fuerzas exclusivas aprendibles
1575
  alpha2, alpha3 = torch.sigmoid(self.exclusive_logits).unbind()
1576
 
 
 
 
 
 
 
1577
  # Precalcular ref (x1) en fp32 y su norma al cuadrado — compartido por x2 y x3
1578
  x1_f = x1.float()
1579
  ref_norm_sq = x1_f.pow(2).sum(-1, keepdim=True).clamp_min(self.proj_eps)
@@ -1582,13 +2173,22 @@ class PolyNorm(nn.Module):
1582
  x2 = self._exclusive(x2, x1, alpha2, x1_f, ref_norm_sq)
1583
  x3 = self._exclusive(x3, x1, alpha3, x1_f, ref_norm_sq)
1584
 
1585
- return (
 
 
 
 
1586
  self.weight[0] * x3
1587
  + self.weight[1] * x2
1588
  + self.weight[2] * x1
1589
  + self.bias
1590
  )
1591
 
 
 
 
 
 
1592
 
1593
  class NeoLLMMLP(nn.Module):
1594
  """MLP with FANformer integration and Learnable Multipliers."""
@@ -1614,11 +2214,34 @@ class NeoLLMMLP(nn.Module):
1614
  self.act_fn = PolyNorm(exclusive_init=0.05)
1615
  self.dropout = nn.Dropout(config.dropout_rate)
1616
 
1617
- def forward(self, x):
1618
- x_fan = self.fan_layer(x)
1619
- return self.down_proj(
1620
- self.dropout(self.act_fn(self.gate_proj(x_fan)) * self.up_proj(x_fan))
1621
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1622
 
1623
 
1624
  class NeoLLMDecoderLayer(GradientCheckpointingLayer):
@@ -1681,6 +2304,8 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
1681
  sources: list,
1682
  partial: torch.Tensor,
1683
  query: torch.Tensor,
 
 
1684
  ) -> torch.Tensor:
1685
  """
1686
  Depth-wise softmax attention over preceding layer outputs.
@@ -1722,9 +2347,14 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
1722
  B_vals: Optional[torch.Tensor] = None,
1723
  attn_res_sources: Optional[list] = None,
1724
  attn_res_partial: Optional[torch.Tensor] = None,
 
1725
  output_attentions: Optional[bool] = False,
1726
  **kwargs: Unpack[FlashAttentionKwargs],
1727
  ) -> Tuple:
 
 
 
 
1728
  # ── Attention Residuals: compute pre-attention input ──────────────
1729
  # When active, the input to the attention sublayer is no longer the
1730
  # raw hidden_states (accumulated residual) but a softmax-weighted
@@ -1732,41 +2362,68 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
1732
  # attn_res_partial carries the intra-block standard residual that
1733
  # connects the attention and MLP sublayers within this layer.
1734
  # When inactive, flow is identical to the original.
 
1735
  if self.use_attn_res and attn_res_sources is not None and attn_res_partial is not None:
1736
- h_attn = self._attn_res(attn_res_sources, attn_res_partial,
1737
- self.attn_res_query_attn)
 
 
1738
  residual_attn = attn_res_partial
1739
  else:
1740
  h_attn = hidden_states
1741
  residual_attn = hidden_states
1742
 
1743
  # ── Attention block ───────────────────────────────────────────────
1744
- h_attn = self.lns_attn(self.input_layernorm(h_attn))
 
 
 
 
1745
 
1746
  hidden_states, attn_weights, self.current_layer_fan = self.self_attn(
1747
- hidden_states=h_attn,
1748
  attention_mask=attention_mask,
1749
  position_embeddings=position_embeddings,
1750
  first_layer_fan=first_layer_fan,
 
1751
  **kwargs,
1752
  )
1753
- h_tilde = self.gpas_attn(residual_attn + hidden_states)
 
 
 
 
 
 
 
 
1754
 
1755
  # ── Attention Residuals: compute pre-MLP input ────────────────────
1756
  # After attention, the partial sum is updated with h_tilde.
1757
  # The pre-MLP AttnRes attends over the same sources but with h_tilde
1758
  # as the current partial — capturing the within-layer attention output.
1759
  if self.use_attn_res and attn_res_sources is not None:
1760
- h_mlp = self._attn_res(attn_res_sources, h_tilde,
1761
- self.attn_res_query_mlp)
 
 
1762
  residual_mlp = h_tilde
1763
  else:
1764
  h_mlp = h_tilde
1765
  residual_mlp = h_tilde
1766
 
1767
  # ── MLP block ─────────────────────────────────────────────────────
1768
- h_mlp = self.lns_mlp(self.post_attention_layernorm(h_mlp))
1769
- delta_m = self.mlp(h_mlp)
 
 
 
 
 
 
 
 
 
1770
 
1771
  # ── JTok-M injection (additive alongside MLP residual) ────────────
1772
  aux_stats = None
@@ -1776,12 +2433,18 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
1776
  z_flat = z_tilde.reshape(-1, z_tilde.shape[-1])
1777
  B_flat = B_vals.reshape(-1, B_vals.shape[-2], B_vals.shape[-1])
1778
 
1779
- delta_r, aux_stats = self.jtokm(h_flat, z_flat, B_flat)
 
1780
  delta_r = delta_r.reshape(orig_shape)
1781
 
1782
- hidden_states = self.gpas_mlp(residual_mlp + delta_m + delta_r)
 
1783
  else:
1784
- hidden_states = self.gpas_mlp(residual_mlp + delta_m)
 
 
 
 
1785
 
1786
  outputs = (hidden_states,)
1787
  if output_attentions:
@@ -1968,6 +2631,30 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
1968
  else:
1969
  self.embed_tokens = value
1970
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1971
  def forward(
1972
  self,
1973
  input_ids: Optional[torch.LongTensor] = None,
@@ -1977,6 +2664,7 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
1977
  output_hidden_states: Optional[bool] = None,
1978
  output_attentions: Optional[bool] = None,
1979
  return_dict: Optional[bool] = None,
 
1980
  **kwargs: Unpack[TransformersKwargs],
1981
  ) -> Tuple:
1982
  output_hidden_states = (
@@ -1996,12 +2684,18 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
1996
  z_tilde = None
1997
  B_vals = None
1998
 
 
 
 
 
 
 
1999
  if inputs_embeds is None:
2000
  if self.config.use_token_generator:
2001
  if self.config.use_jtokm:
2002
  # Return internals for reuse by JTok-M surfaces
2003
  inputs_embeds, z_tilde, B_vals = self.token_generator(
2004
- input_ids, return_internals=True
2005
  )
2006
  # Reshape to [batch, seq, d_seed] and [batch, seq, d_seed, n_knots]
2007
  z_tilde = z_tilde.reshape(*input_ids.shape, self.config.generator_d_seed)
@@ -2011,10 +2705,13 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
2011
  self.config.generator_num_knots,
2012
  )
2013
  else:
2014
- inputs_embeds = self.token_generator(input_ids)
2015
  else:
2016
  inputs_embeds = self.embed_tokens(input_ids)
2017
 
 
 
 
2018
  if position_ids is None:
2019
  position_ids = torch.arange(
2020
  0, inputs_embeds.shape[1], device=inputs_embeds.device
@@ -2061,6 +2758,10 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
2061
  else 1 # Full AttnRes: every layer is its own "block"
2062
  )
2063
 
 
 
 
 
2064
  for layer_idx, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]):
2065
  if output_hidden_states:
2066
  all_hidden_states = all_hidden_states + (hidden_states,)
@@ -2077,6 +2778,13 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
2077
  attn_res_sources = attn_res_sources + [attn_res_partial]
2078
  attn_res_partial = hidden_states # start new block from current output
2079
 
 
 
 
 
 
 
 
2080
  layer_outputs = decoder_layer(
2081
  hidden_states,
2082
  position_embeddings=position_embeddings,
@@ -2086,6 +2794,7 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
2086
  B_vals=B_vals,
2087
  attn_res_sources=attn_res_sources,
2088
  attn_res_partial=attn_res_partial if use_attn_res else None,
 
2089
  output_attentions=output_attentions,
2090
  **kwargs,
2091
  )
@@ -2111,6 +2820,14 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
2111
  if output_hidden_states:
2112
  all_hidden_states = all_hidden_states + (hidden_states,)
2113
 
 
 
 
 
 
 
 
 
2114
  if not return_dict:
2115
  return tuple(
2116
  v for v in [hidden_states, None, all_hidden_states, all_attentions]
@@ -2153,6 +2870,30 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
2153
  total_loss = CE_loss + L_aux
2154
 
2155
  where L_aux = λ · n_e · (1/L) · Σ_ℓ Σ_i p_i^ℓ · f_i^ℓ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2156
  """
2157
 
2158
  _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
@@ -2166,8 +2907,70 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
2166
  if config.use_token_generator:
2167
  self._tied_weights_keys = {}
2168
 
 
 
 
 
 
 
 
2169
  self.post_init()
2170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2171
  def get_input_embeddings(self):
2172
  return self.model.get_input_embeddings()
2173
 
@@ -2217,6 +3020,9 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
2217
  return_dict: Optional[bool] = None,
2218
  **kwargs: Unpack[TransformersKwargs],
2219
  ) -> CausalLMOutputWithPast:
 
 
 
2220
  model_out = self.model(
2221
  input_ids=input_ids,
2222
  attention_mask=attention_mask,
@@ -2224,6 +3030,7 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
2224
  inputs_embeds=inputs_embeds,
2225
  output_hidden_states=output_hidden_states,
2226
  return_dict=return_dict,
 
2227
  **kwargs,
2228
  )
2229
 
@@ -2262,6 +3069,11 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
2262
  )
2263
  logits = self.lm_head(hidden_states[:, slice_indices, :])
2264
 
 
 
 
 
 
2265
  return CausalLMOutputWithPast(
2266
  loss=loss,
2267
  logits=logits,
@@ -2286,6 +3098,18 @@ __all__ = [
2286
  "VectorMultiplier",
2287
  "LinearWithMultipliers",
2288
  "MEAHeadSeeDNorm",
 
 
 
 
 
 
 
 
 
 
 
 
2289
  ]
2290
 
2291
  AutoConfig.register("neollm", NeoLLMConfig)
 
20
  moving-average bias that prevents collapse. Reduces first-token bias,
21
  increases attention entropy, and is complementary to Gated Attention
22
  (Bae et al. 2026, Table 2: Affine-Scaled + Gated > either alone).
23
+
24
+ Flash/SDPA path (exact, no approximation):
25
+ Expanding [α·softmax(QKᵀ)+β]V distributively yields two terms:
26
+ α · flash_attn(Q,K,V) — Flash computes this directly
27
+ + β · Σ_{j≤i} V_j — causal prefix-sum of V (V.cumsum)
28
+ The V_cumsum tensor [B,H,S,d_head] is the only extra memory vs a
29
+ standard flash call. Semantically identical to the eager path;
30
+ per-weight softmax tensors (attn_weights_pre/post_affine) are
31
+ unavailable in flash mode and remain None in AnalysisState.
32
+
33
+ Eager path: exact with full weight access, used for interpretability.
34
 
35
  References:
36
  FANformer: "FANformer: Improving Large Language Models Through Effective
 
52
  """
53
 
54
  import math
55
+ from dataclasses import dataclass
56
+ from typing import Callable, List, Optional, Union, Tuple
57
 
58
  import torch
59
  import torch.nn.functional as F
 
71
  from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
72
  from transformers.processing_utils import Unpack
73
  from transformers.utils import TransformersKwargs, logging
74
+ from configuration_neollm import NeoLLMConfig
75
 
76
  from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
77
 
78
  logger = logging.get_logger(__name__)
79
 
80
 
81
+ # ==================== NATIVE ANALYSIS STATE ====================
82
+ # All dataclasses below define the analysis container hierarchy.
83
+ # This infrastructure is ONLY active when:
84
+ # 1. model.enable_analysis() has been called, AND
85
+ # 2. the model is in eval mode (model.eval() / not model.training)
86
+ #
87
+ # During training, analysis_state is always None — zero overhead,
88
+ # zero interference with gradients, zero change to the training flow.
89
+ #
90
+ # Access after any inference call:
91
+ # state = model.last_analysis # AnalysisState | None
92
+ #
93
+ # Every tensor stored here is detached from the computation graph.
94
+ # Fields are None when the corresponding config flag is inactive,
95
+ # so adding or removing a component only requires updating its own
96
+ # dataclass — all other analysis paths remain untouched.
97
+
98
+
99
+ @dataclass
100
+ class FANAnalysis:
101
+ """
102
+ Decomposed output of a FANLayer call.
103
+ FANLayer'(X) = [cos(Wp·X) ‖ sin(Wp·X) ‖ (Wp̄·X + Bp̄)]
104
+ """
105
+ cosine_component: Optional[torch.Tensor] = None # [*, p_output_dim]
106
+ sine_component: Optional[torch.Tensor] = None # [*, p_output_dim]
107
+ linear_component: Optional[torch.Tensor] = None # [*, g_output_dim]
108
+
109
+
110
+ @dataclass
111
+ class SeeDNormAnalysis:
112
+ """
113
+ Internals of a SeeDNorm forward pass.
114
+ SeeDNorm(x) = [σ(x·β^T)·α + γ] ⊙ x/RMS(x)
115
+ """
116
+ rescale_factor: Optional[torch.Tensor] = None # tanh(Σ x·β) — the dynamic gate
117
+ dynamic_scale: Optional[torch.Tensor] = None # rescale_factor·α + γ
118
+ x_normalized: Optional[torch.Tensor] = None # x / RMS(x) before dynamic_scale
119
+ output: Optional[torch.Tensor] = None # final output
120
+
121
+
122
+ @dataclass
123
+ class GPASAnalysis:
124
+ """
125
+ Internals of a GPAS forward pass.
126
+ GPAS(x) = x - silu(α)·x_detached
127
+ """
128
+ silu_alpha: Optional[torch.Tensor] = None # F.silu(self.alpha) — scalar
129
+ subtracted_component: Optional[torch.Tensor] = None # silu(α) · x.detach()
130
+
131
+
132
+ @dataclass
133
+ class PolyNormAnalysis:
134
+ """
135
+ Internals of a PolyNorm forward pass (used as MLP act_fn).
136
+ Branches: x1 (linear), x2 (quadratic), x3 (cubic), each normalized.
137
+ x2 and x3 are partially orthogonalized against x1 via learned α.
138
+ """
139
+ x1: Optional[torch.Tensor] = None # RMS-normalized linear branch
140
+ x2_pre_exclusive: Optional[torch.Tensor] = None # quadratic branch before exclusivity
141
+ x3_pre_exclusive: Optional[torch.Tensor] = None # cubic branch before exclusivity
142
+ x2_post_exclusive: Optional[torch.Tensor] = None # quadratic after exclusivity + renorm
143
+ x3_post_exclusive: Optional[torch.Tensor] = None # cubic after exclusivity + renorm
144
+ alpha2: Optional[torch.Tensor] = None # sigmoid(exclusive_logits[0]) for x2
145
+ alpha3: Optional[torch.Tensor] = None # sigmoid(exclusive_logits[1]) for x3
146
+ weights: Optional[torch.Tensor] = None # self.weight [3] — branch mixing
147
+ bias: Optional[torch.Tensor] = None # self.bias scalar
148
+ output: Optional[torch.Tensor] = None # final PolyNorm output
149
+
150
+
151
+ @dataclass
152
+ class AttentionAnalysis:
153
+ """
154
+ Full attention internals for one NeoLLMAttention forward pass.
155
+ Fields present unconditionally capture the always-active path.
156
+ Fields guarded by config flags are None when that flag is off.
157
+ """
158
+ # ── FANLayer (always active) ──────────────────────────────────────
159
+ fan: Optional[FANAnalysis] = None # FAN components for attention
160
+
161
+ # ── Q/K/V projection (always active) ─────────────────────────────
162
+ q_raw: Optional[torch.Tensor] = None # Q before q_norm [B,S,H,d]
163
+ gate_raw: Optional[torch.Tensor] = None # gate chunk before sigmoid [B,S,H*d]
164
+ gate_sigmoid: Optional[torch.Tensor] = None # sigmoid(gate) — Gated Attention weight
165
+ q_post_norm: Optional[torch.Tensor] = None # Q after SeeDNorm q_norm [B,H,S,d]
166
+ k_post_norm: Optional[torch.Tensor] = None # K after SeeDNorm k_norm [B,H,S,d]
167
+ v_raw: Optional[torch.Tensor] = None # V raw (pre MEA/LUCID) [B,H,S,d]
168
+
169
+ # ── RoPE (always active) ──────────────────────────────────────────
170
+ q_post_rope: Optional[torch.Tensor] = None # Q after RoPE [B,H,S,d]
171
+ k_post_rope: Optional[torch.Tensor] = None # K after RoPE [B,H,S,d]
172
+
173
+ # ── Momentum (conditional on use_momentum_attention) ──────────────
174
+ q_momentum_delta: Optional[torch.Tensor] = None # causal_first_difference(Q)
175
+ k_momentum_delta: Optional[torch.Tensor] = None # causal_first_difference(K)
176
+ q_post_momentum: Optional[torch.Tensor] = None # Q + γ·Δ
177
+ k_post_momentum: Optional[torch.Tensor] = None # K + γ·Δ
178
+
179
+ # ── MEA head mixing (conditional on use_mea_attention) ────────────
180
+ mea_key_mix_matrix: Optional[torch.Tensor] = None # mea_key_mix [H_comp,H_kv]
181
+ mea_value_mix_matrix: Optional[torch.Tensor] = None # mea_value_mix [H_comp,H_kv]
182
+ k_post_mea: Optional[torch.Tensor] = None # K after head mixing
183
+ v_post_mea: Optional[torch.Tensor] = None # V after head mixing
184
+
185
+ # ── LUCID preconditioner (conditional on use_lucid_attention) ─────
186
+ lucid_preconditioner: Optional[torch.Tensor] = None # lower-triangular prec matrix
187
+ v_post_lucid: Optional[torch.Tensor] = None # V after triangular solve
188
+
189
+ # ── Affine-Scaled Attention (conditional on use_affine_scaled_attention) ──
190
+ alpha_per_head: Optional[torch.Tensor] = None # α [B,H,S,1] in [0,1]
191
+ beta_per_head: Optional[torch.Tensor] = None # β [B,H,S,1] moving-avg bias
192
+ alpha_moving_avg: Optional[torch.Tensor] = None # alpha_ma EMA snapshot
193
+ attn_weights_pre_affine: Optional[torch.Tensor] = None # softmax weights before α,β
194
+ attn_weights_post_affine: Optional[torch.Tensor] = None # α·softmax + β
195
+
196
+ # ── Standard attention weights (eager non-affine path) ────────────
197
+ attn_weights: Optional[torch.Tensor] = None # softmax weights (None for flash/sdpa)
198
+
199
+ # ── Post-SDPA (always active) ─────────────────────────────────────
200
+ attn_output_raw: Optional[torch.Tensor] = None # SDPA output [B,S,H,d]
201
+
202
+ # ── MEA output norm (conditional on use_mea_attention) ────────────
203
+ attn_output_post_mea_norm: Optional[torch.Tensor] = None # after MEAHeadSeeDNorm
204
+
205
+ # ── XSA (conditional on use_xsa) ─────────────────────────────────
206
+ xsa_self_position_component: Optional[torch.Tensor] = None # proj subtracted
207
+ attn_output_post_xsa: Optional[torch.Tensor] = None # after XSA removal
208
+
209
+ # ── Directional Routing (conditional on use_directional_routing) ──
210
+ direction_vecs_normalized: Optional[torch.Tensor] = None # unit-norm d [H,K,d]
211
+ dr_router_logits: Optional[torch.Tensor] = None # MLP router output [B,H*K]
212
+ dr_routing_weights: Optional[torch.Tensor] = None # sigmoid(T·logits) [B,H,K]
213
+ dr_projection: Optional[torch.Tensor] = None # (o·d) scalars [B,S,H,K]
214
+ dr_suppression: Optional[torch.Tensor] = None # Σ r·proj·d [B,S,H,d]
215
+ attn_output_post_routing: Optional[torch.Tensor] = None # after DR removal
216
+
217
+ # ── Gate and o_proj (always active) ───────────────────────────────
218
+ attn_output_pre_gate: Optional[torch.Tensor] = None # pre gate multiply [B,S,H,d]
219
+ attn_output_final: Optional[torch.Tensor] = None # after o_proj [B,S,D]
220
+
221
+
222
+ @dataclass
223
+ class MLPAnalysis:
224
+ """
225
+ Internals of a NeoLLMMLP forward pass.
226
+ SwiGLU-like: down_proj(dropout(PolyNorm(gate_proj(fan)) · up_proj(fan)))
227
+ """
228
+ fan: Optional[FANAnalysis] = None # FAN components for MLP
229
+ gate_proj_output: Optional[torch.Tensor] = None # gate_proj(x_fan) [B,S,I]
230
+ up_proj_output: Optional[torch.Tensor] = None # up_proj(x_fan) [B,S,I]
231
+ polynorm: Optional[PolyNormAnalysis] = None # PolyNorm of gate branch
232
+ act_times_up: Optional[torch.Tensor] = None # PolyNorm(gate)·up [B,S,I]
233
+ output: Optional[torch.Tensor] = None # after down_proj [B,S,D]
234
+
235
+
236
+ @dataclass
237
+ class JTokMAnalysis:
238
+ """
239
+ Internals of a LeviathanJTokM forward pass for one decoder layer.
240
+ """
241
+ surfaces: Optional[torch.Tensor] = None # all n_e surface outputs [N,n_e,D]
242
+ router_logits: Optional[torch.Tensor] = None # pre-TopK logits [N,n_e]
243
+ topk_indices: Optional[torch.Tensor] = None # selected surface indices [N,K]
244
+ routing_weights: Optional[torch.Tensor] = None # normalized sigmoid weights [N,K]
245
+ mixed_pre_norm: Optional[torch.Tensor] = None # weighted sum before norm [N,D]
246
+ mixed_normalized: Optional[torch.Tensor] = None # direction-normalized mixed [N,D]
247
+ delta_r: Optional[torch.Tensor] = None # final injection (scaled) [N,D]
248
+ p_sum: Optional[torch.Tensor] = None # routing probability sum [n_e]
249
+ f_sum: Optional[torch.Tensor] = None # load fraction sum [n_e]
250
+ lns_scale: Optional[float] = None # 1/√(2ℓ) scaling factor
251
+
252
+
253
+ @dataclass
254
+ class AttnResAnalysis:
255
+ """
256
+ Depth-wise softmax attention weights from AttnRes sublayer calls.
257
+ Only populated when use_attn_res=True.
258
+ """
259
+ weights_pre_attn: Optional[torch.Tensor] = None # softmax over sources [N+1,B,S]
260
+ weights_pre_mlp: Optional[torch.Tensor] = None # softmax over sources [N+1,B,S]
261
+ sources_count: Optional[int] = None # number of sources including partial
262
+
263
+
264
+ @dataclass
265
+ class LayerAnalysis:
266
+ """
267
+ Complete analysis snapshot for one NeoLLMDecoderLayer forward pass.
268
+ Sub-objects are None when the corresponding config flag is inactive.
269
+ """
270
+ layer_idx: int = 0
271
+
272
+ # Hidden state boundaries
273
+ hidden_states_input: Optional[torch.Tensor] = None # entering this layer
274
+ hidden_states_output: Optional[torch.Tensor] = None # leaving this layer
275
+ h_tilde: Optional[torch.Tensor] = None # post-attn residual, pre-MLP
276
+
277
+ # Attention sublayer
278
+ seednorm_pre_attn: Optional[SeeDNormAnalysis] = None # input_layernorm
279
+ lns_attn_output: Optional[torch.Tensor] = None # after LNS(1/√ℓ)
280
+ attention: Optional[AttentionAnalysis] = None # full attention analysis
281
+ attn_contribution: Optional[torch.Tensor] = None # attn output before residual
282
+ gpas_attn: Optional[GPASAnalysis] = None # GPAS after attn residual
283
+
284
+ # MLP sublayer
285
+ seednorm_post_attn: Optional[SeeDNormAnalysis] = None # post_attention_layernorm
286
+ lns_mlp_output: Optional[torch.Tensor] = None # after LNS(1/√ℓ)
287
+ mlp: Optional[MLPAnalysis] = None # full MLP analysis
288
+ mlp_contribution: Optional[torch.Tensor] = None # MLP output before residual
289
+ gpas_mlp: Optional[GPASAnalysis] = None # GPAS after MLP residual
290
+
291
+ # Optional components (None when inactive)
292
+ jtokm: Optional[JTokMAnalysis] = None # if use_jtokm
293
+ attn_res: Optional[AttnResAnalysis] = None # if use_attn_res
294
+
295
+
296
+ @dataclass
297
+ class GeneratorAnalysis:
298
+ """
299
+ Internals of a LeviathanGenerator forward pass.
300
+ Only populated when use_token_generator=True.
301
+ """
302
+ z_raw: Optional[torch.Tensor] = None # [N,d_seed] codebook sum
303
+ z_tilde: Optional[torch.Tensor] = None # [N,d_seed] JTok-M path output
304
+ B_vals: Optional[torch.Tensor] = None # [N,d_seed,n_knots] B-spline basis
305
+ z_all_pre_norm: Optional[torch.Tensor] = None # [N,M,d_seed] per-head pre-sigmoid
306
+ z_all_post_sigmoid: Optional[torch.Tensor] = None # [N,M,d_seed] per-head post-sigmoid
307
+ modes_all: Optional[torch.Tensor] = None # [N,M,krank] KHRONOS tensor product
308
+ embeddings: Optional[torch.Tensor] = None # [N,hidden_size] final output
309
+
310
+
311
+ @dataclass
312
+ class AnalysisState:
313
+ """
314
+ Root analysis container for one NeoLLMForCausalLM forward pass.
315
+
316
+ Populated when model.enable_analysis() is active AND model.training=False.
317
+ All tensors are detached from the computation graph.
318
+
319
+ Structure:
320
+ .input_ids — original input token ids
321
+ .embeddings — token embeddings entering the decoder stack
322
+ .final_hidden_states — post-norm output entering lm_head
323
+ .generator — LeviathanGenerator internals (use_token_generator only)
324
+ .layers[i] — per-layer LayerAnalysis, indexed by layer_idx
325
+ .jtokm_aux_stats — raw per-layer load-balancing tuples (use_jtokm only)
326
+ .attn_res_sources_final — AttnRes source list at end of forward (use_attn_res only)
327
+ .logits — lm_head output (only when labels is None)
328
+
329
+ Access pattern:
330
+ model.eval()
331
+ model.enable_analysis()
332
+ _ = model(input_ids)
333
+ state = model.last_analysis
334
+ alpha = state.layers[3].attention.alpha_per_head
335
+ """
336
+ input_ids: Optional[torch.Tensor] = None
337
+ embeddings: Optional[torch.Tensor] = None
338
+ final_hidden_states: Optional[torch.Tensor] = None
339
+
340
+ generator: Optional[GeneratorAnalysis] = None
341
+ layers: Optional[List[LayerAnalysis]] = None
342
+ jtokm_aux_stats: Optional[list] = None
343
+ attn_res_sources_final: Optional[list] = None
344
+ logits: Optional[torch.Tensor] = None
345
 
346
  class ScalarMultiplier(nn.Module):
347
  """
 
728
  self,
729
  token_ids: torch.Tensor,
730
  return_internals: bool = False,
731
+ analysis: Optional[GeneratorAnalysis] = None,
732
  ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, torch.Tensor]]:
733
  """
734
  Generate embeddings from discrete token indices.
 
753
  token_ids: (batch, seq_len) or (seq_len,)
754
  return_internals: if True, also return z_tilde and B_vals for
755
  reuse by JTok-M surfaces in every decoder layer.
756
+ analysis: Optional[GeneratorAnalysis] — when not None and model
757
+ is in eval mode with analysis armed, deposits all
758
+ internal tensors (detached). No-op during training.
759
  Returns:
760
  embeddings [*token_ids.shape, hidden_size],
761
  or (embeddings, z_tilde [N, d_seed], B_vals [N, d_seed, n_knots])
 
772
  for r in range(self.k):
773
  z = z + self.codebooks[r][coords_flat[:, r]]
774
 
775
+ if analysis is not None:
776
+ analysis.z_raw = z.detach()
777
+
778
  # ── JTok-M shared path ────────────────────────────────────────────
779
  # Produces z_tilde and B_vals consumed by every decoder layer's
780
  # JTok-M module. This path is unchanged and uses fixed scalar scale.
781
  z_tilde = torch.sigmoid(self.seed_norm(self.seed_proj(z))) # [N, d_seed]
782
  B_vals = self._bspline_basis(z_tilde.clamp(0.0, 1.0)) # [N, d_seed, n_knots]
783
 
784
+ if analysis is not None:
785
+ analysis.z_tilde = z_tilde.detach()
786
+ analysis.B_vals = B_vals.detach()
787
+
788
  # ── Per-head generator path (fully vectorized, 6 kernels) ────────
789
  # All 8 heads are processed simultaneously. No Python loop.
790
  # Maximum intermediate tensor [N, M, d_seed, n_knots] appears once.
 
794
  z_all = F.linear(z.to(target_dtype), self.head_proj_weight)
795
  z_all = z_all.view(N, self.num_modes, self.d_seed) # [N, M, d_seed]
796
 
797
+ if analysis is not None:
798
+ analysis.z_all_pre_norm = z_all.detach()
799
+
800
  # Kernel 2: per-head LayerNorm + sigmoid(x/2)
801
  # Manual LN over last dim with independent weight/bias per head.
802
  # Mathematically identical to 8 separate nn.LayerNorm(d_seed).
 
808
  + self.head_norm_bias.unsqueeze(0)
809
  z_all = torch.sigmoid(z_all / 2.0) # [N, M, d_seed]
810
 
811
+ if analysis is not None:
812
+ analysis.z_all_post_sigmoid = z_all.detach()
813
+
814
  # Kernel 3: vectorized B-spline basis for all heads
815
  # head_scale [M, d_seed] is used inside _bspline_basis_all_heads
816
  B_all = self._bspline_basis_all_heads(
 
820
  # Kernel 4: vectorized KHRONOS tensor product for all heads
821
  modes_all = self._khronos_all_heads(B_all) # [N, M, krank]
822
 
823
+ if analysis is not None:
824
+ analysis.modes_all = modes_all.detach()
825
+
826
  # Kernel 5: project all heads to hidden_size and sum
827
  # einsum: token n, head m, krank k → hidden d (summed over m)
828
  # head_out_weight [M, krank, hidden_size]
 
835
  # No W_res — confirmed absent in the authors' implementation
836
  e = e.reshape(*orig_shape, self.hidden_size)
837
 
838
+ if analysis is not None:
839
+ analysis.embeddings = e.detach()
840
+
841
  if return_internals:
842
  return e, z_tilde, B_vals
843
  return e
 
924
  B_vals: torch.Tensor,
925
  z_tilde: torch.Tensor,
926
  target_dtype: torch.dtype,
927
+ analysis: Optional[JTokMAnalysis] = None,
928
  ) -> torch.Tensor:
929
  """
930
  Evaluate all n_e surfaces vectorized over the full token batch.
 
936
  All shapes are static → torch.compile compatible.
937
 
938
  Args:
939
+ B_vals: [N, d_seed, n_knots] float32
940
+ z_tilde: [N, d_seed]
941
  target_dtype: model dtype
942
+ analysis: Optional[JTokMAnalysis] — deposits surfaces when not None.
943
  Returns:
944
  surfaces: [N, n_e, D]
945
  """
 
968
  z = z_tilde.to(target_dtype)
969
  out_res = torch.einsum("nd,idc->nic", z, self.W_res.to(target_dtype))
970
 
971
+ surfaces = out_modes + out_res # [N, n_e, D]
972
+
973
+ if analysis is not None:
974
+ analysis.surfaces = surfaces.detach()
975
+
976
+ return surfaces
977
 
978
  # ── Router ────────────────────────────────────────────────────────────
979
 
 
984
  self,
985
  h_tilde: torch.Tensor,
986
  surfaces: torch.Tensor,
987
+ analysis: Optional[JTokMAnalysis] = None,
988
  ) -> Tuple[torch.Tensor, torch.Tensor]:
989
  """
990
  Context-dependent routing over h_tilde (hidden state after attention).
 
1003
  Args:
1004
  h_tilde: [N, D] — hidden state after attention (before MLP)
1005
  surfaces: [N, n_e, D]
1006
+ analysis: Optional[JTokMAnalysis] — deposits routing data when not None.
1007
  Returns:
1008
  mixed: [N, D]
1009
  aux_stats: (p_sum [n_e], f_sum [n_e], N) for loss accumulation
 
1026
  selected = surfaces.gather(dim=1, index=idx_exp) # [N, K, D]
1027
  mixed = (w.unsqueeze(-1) * selected).sum(dim=1) # [N, D]
1028
 
1029
+ if analysis is not None:
1030
+ analysis.router_logits = g.detach()
1031
+ analysis.topk_indices = topk_idx.detach()
1032
+ analysis.routing_weights = w.detach()
1033
+ analysis.mixed_pre_norm = mixed.detach()
1034
+
1035
  # Load-balancing statistics for aux loss (Appendix B, Yang et al. 2026)
1036
  # p_i = mean routing probability over batch
1037
  # f_i = fraction of tokens actually routed to i
 
1053
  h_tilde: torch.Tensor,
1054
  z_tilde: torch.Tensor,
1055
  B_vals: torch.Tensor,
1056
+ analysis: Optional[JTokMAnalysis] = None,
1057
  ) -> Tuple[torch.Tensor, Tuple]:
1058
  """
1059
  Compute additive JTok-M residual for one decoder layer.
1060
 
1061
  Args:
1062
+ h_tilde: [N, D] hidden state after attention (before MLP)
1063
+ z_tilde: [N, d_seed] latent coordinate from generator
1064
+ B_vals: [N, d_seed, n_k] B-spline basis (computed once, reused)
1065
+ analysis: Optional[JTokMAnalysis] — deposits all JTok-M internals when
1066
+ not None. No-op during training (analysis is always None then).
1067
  Returns:
1068
+ delta_r: [N, D] additive residual (already scaled)
1069
  aux_stats: tuple for accumulating load-balance loss
1070
  """
1071
  target_dtype = h_tilde.dtype
1072
 
1073
  # All n_e surfaces in one vectorized pass
1074
+ surfaces = self._eval_surfaces(B_vals, z_tilde, target_dtype, analysis=analysis)
1075
 
1076
  # Context-dependent routing
1077
+ mixed, aux_stats = self._route_and_mix(h_tilde, surfaces, analysis=analysis)
1078
 
1079
  # Normalise direction, apply scaler, scale with 1/√(2ℓ)
1080
  # Norm_ε decouples direction from magnitude (JTok Appendix D.2)
1081
  mixed_norm = mixed / (mixed.norm(dim=-1, keepdim=True) + self.norm_eps)
1082
  delta_r = self.lns_scale * self.scaler * mixed_norm # [N, D]
1083
 
1084
+ if analysis is not None:
1085
+ analysis.mixed_normalized = mixed_norm.detach()
1086
+ analysis.delta_r = delta_r.detach()
1087
+ analysis.p_sum = aux_stats[0].detach()
1088
+ analysis.f_sum = aux_stats[1].detach()
1089
+ analysis.lns_scale = self.lns_scale
1090
+
1091
  return delta_r, aux_stats
1092
 
1093
 
 
1147
  if self.input_linear.bias is not None:
1148
  nn.init.zeros_(self.input_linear.bias)
1149
 
1150
+ def forward(
1151
+ self,
1152
+ x: torch.Tensor,
1153
+ analysis: Optional[FANAnalysis] = None,
1154
+ ) -> torch.Tensor:
1155
  pg = self.input_linear(x)
1156
  p, g = torch.split(pg, [self.p_output_dim, self.g_output_dim], dim=-1)
1157
+ cos_p = torch.cos(p)
1158
+ sin_p = torch.sin(p)
1159
+ if analysis is not None:
1160
+ analysis.cosine_component = cos_p.detach()
1161
+ analysis.sine_component = sin_p.detach()
1162
+ analysis.linear_component = g.detach()
1163
+ return torch.cat([cos_p, sin_p, g], dim=-1)
1164
 
1165
 
1166
  class LNS(nn.Module):
 
1185
  self.d_model = d_model
1186
  self.alpha = nn.Parameter(torch.zeros(1))
1187
 
1188
+ def forward(
1189
+ self,
1190
+ x: torch.Tensor,
1191
+ analysis: Optional[GPASAnalysis] = None,
1192
+ ) -> torch.Tensor:
1193
+ silu_alpha = F.silu(self.alpha)
1194
+ subtracted = silu_alpha * x.detach()
1195
+ if analysis is not None:
1196
+ analysis.silu_alpha = silu_alpha.detach()
1197
+ analysis.subtracted_component = subtracted.detach()
1198
+ return x - subtracted
1199
 
1200
 
1201
  class SeeDNorm(nn.Module):
 
1224
  def _rms_norm(self, x: torch.Tensor) -> torch.Tensor:
1225
  return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
1226
 
1227
+ def forward(
1228
+ self,
1229
+ x: torch.Tensor,
1230
+ analysis: Optional[SeeDNormAnalysis] = None,
1231
+ ) -> torch.Tensor:
1232
  x_for_dynamic = F.dropout(x, p=self.dropout_input)
1233
  rescale_factor = torch.tanh(
1234
  torch.sum(x_for_dynamic * self.beta, dim=-1, keepdim=True)
 
1236
  dynamic_scale = rescale_factor * self.alpha + self.gamma
1237
  x_normalized = self._rms_norm(x.float())
1238
  x_normalized = F.dropout(x_normalized, p=self.dropout_hidden)
1239
+ output = (x_normalized * dynamic_scale.float()).type_as(x)
1240
+ if analysis is not None:
1241
+ analysis.rescale_factor = rescale_factor.detach()
1242
+ analysis.dynamic_scale = dynamic_scale.detach()
1243
+ analysis.x_normalized = x_normalized.detach()
1244
+ analysis.output = output.detach()
1245
+ return output
1246
 
1247
  def extra_repr(self) -> str:
1248
  return (f"dim={self.dim}, eps={self.eps}, "
 
1458
  alpha: torch.Tensor,
1459
  beta: torch.Tensor,
1460
  dropout: float = 0.0,
1461
+ attn_analysis: Optional[AttentionAnalysis] = None,
1462
  **kwargs: Unpack[TransformersKwargs],
1463
  ):
1464
  """
 
1481
  Reference: Bae et al. (2026), Affine-Scaled Attention, Eq. 6–8.
1482
 
1483
  Args:
1484
+ alpha: [batch, num_heads, seq_q, 1] — input-dependent scale per query
1485
+ beta: [batch, num_heads, seq_q, 1] — input-dependent bias per query
1486
+ attn_analysis: Optional[AttentionAnalysis] — deposits pre/post-affine weights
1487
+ when not None. No-op during training (always None then).
1488
  """
1489
  key_states = repeat_kv(key, module.num_key_value_groups)
1490
  value_states = repeat_kv(value, module.num_key_value_groups)
 
1493
  if attention_mask is not None:
1494
  attn_weights = attn_weights + attention_mask[:, :, :, : key_states.shape[-2]]
1495
 
1496
+ attn_weights_softmax = nn.functional.softmax(
1497
  attn_weights, dim=-1, dtype=torch.float32
1498
  ).to(query.dtype)
1499
 
1500
+ if attn_analysis is not None:
1501
+ attn_analysis.attn_weights_pre_affine = attn_weights_softmax.detach()
1502
+
1503
  # Affine reweighting: relaxes the unit-sum constraint of softmax.
1504
  # α ∈ [0, 1] scales down the softmax distribution (input-adaptively per head).
1505
  # β offsets to prevent collapse when α deviates from its running mean.
1506
  # Shapes: α, β are [B, H, S_q, 1], attn_weights is [B, H, S_q, S_k] → broadcast.
1507
+ attn_weights_affine = alpha * attn_weights_softmax + beta
1508
 
1509
+ if attn_analysis is not None:
1510
+ attn_analysis.attn_weights_post_affine = attn_weights_affine.detach()
1511
+
1512
+ attn_weights_affine = nn.functional.dropout(
1513
+ attn_weights_affine, p=dropout, training=module.training
1514
+ )
1515
+ attn_output = torch.matmul(attn_weights_affine, value_states).transpose(1, 2).contiguous()
1516
+ return attn_output, attn_weights_affine
1517
+
1518
+
1519
+ def affine_scaled_flash_attention_forward(
1520
+ module: nn.Module,
1521
+ query: torch.Tensor,
1522
+ key: torch.Tensor,
1523
+ value: torch.Tensor,
1524
+ attention_mask: Optional[torch.Tensor],
1525
+ scaling: float,
1526
+ alpha: torch.Tensor,
1527
+ beta: torch.Tensor,
1528
+ dropout: float = 0.0,
1529
+ attn_analysis: Optional[AttentionAnalysis] = None,
1530
+ **kwargs: Unpack[TransformersKwargs],
1531
+ ):
1532
+ """
1533
+ Affine-Scaled Attention — flash/sdpa path.
1534
+
1535
+ Exact mathematical decomposition of [α·softmax(QKᵀ)+β]V using only the
1536
+ public flash/sdpa interface — no kernel modification required.
1537
+
1538
+ Derivation
1539
+ ----------
1540
+ The paper formula expands distributively:
1541
+
1542
+ [α · softmax(QKᵀ/√dk) + β] · V
1543
+ = α · [softmax(QKᵀ/√dk) · V] ← term 1: standard flash output
1544
+ + β · [Σ_{j≤i} V_j] ← term 2: causal prefix-sum of V
1545
+
1546
+ Term 2 follows because β is a scalar per query (broadcast over all S_k),
1547
+ and softmax weights sum to 1 over the causal window:
1548
+ β_i · Σ_j w_{i,j} · V_j = β_i · Σ_{j≤i} V_j (since Σ w_{i,j} = 1)
1549
+
1550
+ Dropout
1551
+ -------
1552
+ The eager path drops entries of the combined weight matrix (α·softmax + β)
1553
+ before multiplying by V. With the flash interface we cannot access that
1554
+ combined matrix, so we apply dropout=0 to the flash kernel and instead
1555
+ apply nn.functional.dropout to the final combined output tensor. This is
1556
+ output dropout rather than weight dropout — a different (but standard)
1557
+ regularisation that achieves the same intent without the intermediate
1558
+ weight matrix. During inference dropout=0 so the paths are identical.
1559
+
1560
+ Padding mask
1561
+ ------------
1562
+ The V_cumsum must not accumulate values from padding positions, since the
1563
+ flash kernel zeros those out internally but a plain cumsum does not.
1564
+ We zero V at padding positions before the cumsum by reading the diagonal
1565
+ of the attention_mask (valid positions have mask value 0, padding has -inf).
1566
+
1567
+ Memory overhead vs standard flash call
1568
+ ---------------------------------------
1569
+ One extra tensor of shape [B, H_q, S, d_head] for V_cumsum.
1570
+ At (B=1, H=8, S=2048, d_head=64, bf16): ≈ 4 MB per call, ≈ 48 MB total
1571
+ across 12 layers. Allocated and freed within each forward call.
1572
+
1573
+ Attention-weight analysis fields (attn_weights_pre_affine,
1574
+ attn_weights_post_affine) remain None — those tensors are never
1575
+ materialised by the flash kernel and cannot be recovered.
1576
+ All other AnalysisState fields (alpha, beta, alpha_ma) are deposited
1577
+ by the caller before this function is invoked.
1578
+
1579
+ Args:
1580
+ alpha: [B, H_q, S_q, 1] — input-dependent scale per query, in [0, 1]
1581
+ beta: [B, H_q, S_q, 1] — moving-average bias per query
1582
+ attn_analysis: deposited by caller; this function does not write to it.
1583
+ """
1584
+ # ── Term 1: standard flash / sdpa output ─────────────────────────────
1585
+ # dropout=0.0: we apply dropout to the combined output below instead.
1586
+ attn_fn = ALL_ATTENTION_FUNCTIONS[module.config._attn_implementation]
1587
+ flash_out, _ = attn_fn(
1588
+ module, query, key, value, attention_mask,
1589
+ dropout=0.0, scaling=scaling, **kwargs,
1590
+ )
1591
+ # flash_out: [B, S, H_q, d_head] — HF wrappers all return this layout
1592
+
1593
+ # ── Term 2: β · causal prefix-sum of V ───────────────────────────────
1594
+ # Expand V from KV heads to query heads for GQA.
1595
+ value_expanded = repeat_kv(value, module.num_key_value_groups) # [B, H_q, S, d_head]
1596
+
1597
+ # Zero out padded positions so they don't accumulate into the prefix sum.
1598
+ # attention_mask is [B, 1, S, S] with 0 at valid positions, -inf at padding.
1599
+ # The diagonal gives the per-position validity: valid → 0, padding → -inf.
1600
+ if attention_mask is not None and attention_mask.ndim == 4:
1601
+ diag = attention_mask.diagonal(dim1=-2, dim2=-1) # [B, 1, S]
1602
+ # valid=True where diag==0 (not -inf), padding=False
1603
+ valid = (diag == 0).to(value_expanded.dtype) # [B, 1, S]
1604
+ valid = valid.unsqueeze(-1) # [B, 1, S, 1]
1605
+ # broadcast over H_q and d_head; zero out V at padding positions
1606
+ value_expanded = value_expanded * valid # [B, H_q, S, d_head]
1607
+
1608
+ # Causal prefix-sum: position i accumulates all valid j ≤ i.
1609
+ v_cumsum = value_expanded.cumsum(dim=2) # [B, H_q, S, d_head]
1610
+ # Transpose to match flash_out layout.
1611
+ v_cumsum_t = v_cumsum.transpose(1, 2).contiguous() # [B, S, H_q, d_head]
1612
+
1613
+ # α, β: [B, H_q, S, 1] → [B, S, H_q, 1] to broadcast over d_head.
1614
+ alpha_t = alpha.permute(0, 2, 1, 3) # [B, S, H_q, 1]
1615
+ beta_t = beta.permute(0, 2, 1, 3) # [B, S, H_q, 1]
1616
+
1617
+ # ── Combine and apply dropout to the full affine output ────���──────────
1618
+ output = alpha_t * flash_out + beta_t * v_cumsum_t # [B, S, H_q, d_head]
1619
+
1620
+ # Apply output dropout on the combined affine result.
1621
+ # This regularises the full [α·flash + β·V_cumsum] output consistently.
1622
+ if dropout > 0.0 and module.training:
1623
+ output = nn.functional.dropout(output, p=dropout, training=True)
1624
+
1625
+ # attn_weights is None — flash never exposes the softmax weight matrix.
1626
+ return output, None
1627
 
1628
 
1629
  class NeoLLMAttention(nn.Module):
 
1808
  self.direction_vecs = None
1809
  self.direction_router = None
1810
 
1811
+ def _apply_momentum_attention(
1812
+ self,
1813
+ q: torch.Tensor,
1814
+ k: torch.Tensor,
1815
+ attn_analysis: Optional[AttentionAnalysis] = None,
1816
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1817
  if not self.use_momentum_attention or self.momentum_gamma == 0.0:
1818
  return q, k
1819
+ dq = causal_first_difference(q)
1820
+ dk = causal_first_difference(k)
1821
+ q_new = q + self.momentum_gamma * dq
1822
+ k_new = k + self.momentum_gamma * dk
1823
+ if attn_analysis is not None:
1824
+ attn_analysis.q_momentum_delta = dq.detach()
1825
+ attn_analysis.k_momentum_delta = dk.detach()
1826
+ attn_analysis.q_post_momentum = q_new.detach()
1827
+ attn_analysis.k_post_momentum = k_new.detach()
1828
+ return q_new, k_new
1829
+
1830
+ def _apply_mea_head_mixing(
1831
+ self,
1832
+ k: torch.Tensor,
1833
+ v: torch.Tensor,
1834
+ attn_analysis: Optional[AttentionAnalysis] = None,
1835
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1836
  if not self.use_mea_attention:
1837
  return k, v
1838
+ k_mixed = head_linear_compose(k, self.mea_key_mix).contiguous()
1839
+ v_mixed = head_linear_compose(v, self.mea_value_mix).contiguous()
1840
+ if attn_analysis is not None:
1841
+ attn_analysis.mea_key_mix_matrix = self.mea_key_mix.detach()
1842
+ attn_analysis.mea_value_mix_matrix = self.mea_value_mix.detach()
1843
+ attn_analysis.k_post_mea = k_mixed.detach()
1844
+ attn_analysis.v_post_mea = v_mixed.detach()
1845
+ return k_mixed, v_mixed
1846
+
1847
+ def _apply_lucid_preconditioner(
1848
+ self,
1849
+ k: torch.Tensor,
1850
+ v: torch.Tensor,
1851
+ attention_mask: Optional[torch.Tensor],
1852
+ attn_analysis: Optional[AttentionAnalysis] = None,
1853
+ ) -> torch.Tensor:
1854
  if not self.use_lucid_attention:
1855
  return v.contiguous()
1856
  key_rn = rms_key_unit_norm(k, eps=self.lucid_attention_eps)
 
1862
  eye = torch.eye(prec.shape[-1], device=prec.device, dtype=prec.dtype).view(
1863
  1, 1, prec.shape[-1], prec.shape[-1]
1864
  )
1865
+ prec = prec + eye * (1.0 - prec.diagonal(dim1=-2, dim2=-1).unsqueeze(-1))
1866
+ result = torch.linalg.solve_triangular(
1867
  prec, v.float(), upper=False, unitriangular=True
1868
  ).to(v.dtype).contiguous()
1869
+ if attn_analysis is not None:
1870
+ attn_analysis.lucid_preconditioner = prec.detach()
1871
+ attn_analysis.v_post_lucid = result.detach()
1872
+ return result
1873
 
1874
  def _apply_directional_routing(
1875
  self,
1876
  attn_out: torch.Tensor,
1877
  hidden_states: torch.Tensor,
1878
+ attn_analysis: Optional[AttentionAnalysis] = None,
1879
  ) -> torch.Tensor:
1880
  """
1881
  Directional suppression at position C (post-XSA, pre-reshape).
 
1884
  attn_out: [B, S, H, d_head] — output after XSA and SeeDNorm.
1885
  hidden_states: [B, S, hidden_size] — pre-FAN residual stream,
1886
  used as router input (same as paper's x_i).
1887
+ attn_analysis: Optional[AttentionAnalysis] — deposits DR internals.
1888
  Returns:
1889
  [B, S, H, d_head] with selected directional components suppressed.
1890
  """
 
1924
  # Σ_k weighted_{h,k} · d_{h,k}:
1925
  # weighted [B, S, H, K] × d [H, K, D] → [B, S, H, D]
1926
  suppression = torch.einsum("bshk,hkd->bshd", weighted, d)
1927
+ result = attn_out - suppression
1928
+
1929
+ if attn_analysis is not None:
1930
+ attn_analysis.direction_vecs_normalized = d.detach()
1931
+ attn_analysis.dr_router_logits = logits.detach()
1932
+ attn_analysis.dr_routing_weights = r.squeeze(1).detach()
1933
+ attn_analysis.dr_projection = proj.detach()
1934
+ attn_analysis.dr_suppression = suppression.detach()
1935
+ attn_analysis.attn_output_post_routing = result.detach()
1936
 
1937
+ return result
1938
 
1939
  def forward(
1940
  self,
 
1942
  position_embeddings: tuple[torch.Tensor, torch.Tensor],
1943
  attention_mask: Optional[torch.Tensor] = None,
1944
  first_layer_fan: Optional[torch.Tensor] = None,
1945
+ attn_analysis: Optional[AttentionAnalysis] = None,
1946
  **kwargs: Unpack[FlashAttentionKwargs],
1947
  ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
1948
  input_shape = hidden_states.shape[:-1]
1949
 
1950
+ fan_a = attn_analysis.fan if attn_analysis is not None else None
1951
+ h_fan = self.fan_layer(hidden_states, analysis=fan_a)
1952
  if first_layer_fan is not None:
1953
  h_fan = self.lambda_1 * first_layer_fan + self.lambda_2 * h_fan
1954
  current_layer_fan = h_fan.clone()
 
1962
  )
1963
  gate = gate.reshape(*input_shape, -1)
1964
 
1965
+ if attn_analysis is not None:
1966
+ attn_analysis.q_raw = q_raw.detach()
1967
+ attn_analysis.gate_raw = gate.detach()
1968
+
1969
  q = self.q_norm(q_raw.view(query_shape)).transpose(1, 2)
1970
  k = self.k_norm(self.k_proj(h_fan).view(kv_shape)).transpose(1, 2)
1971
  v = self.v_proj(h_fan).view(kv_shape).transpose(1, 2)
1972
 
1973
+ if attn_analysis is not None:
1974
+ attn_analysis.q_post_norm = q.detach()
1975
+ attn_analysis.k_post_norm = k.detach()
1976
+ attn_analysis.v_raw = v.detach()
1977
+
1978
  cos, sin = position_embeddings
1979
  q, k = apply_rotary_pos_emb(q, k, cos, sin)
1980
+
1981
+ if attn_analysis is not None:
1982
+ attn_analysis.q_post_rope = q.detach()
1983
+ attn_analysis.k_post_rope = k.detach()
1984
+
1985
+ q, k = self._apply_momentum_attention(q, k, attn_analysis=attn_analysis)
1986
+ k, v = self._apply_mea_head_mixing(k, v, attn_analysis=attn_analysis)
1987
+ v = self._apply_lucid_preconditioner(k, v, attention_mask, attn_analysis=attn_analysis)
1988
 
1989
  # Capture v_ref for XSA after MEA mixing and LUCID preconditioning.
1990
  # This is the vector that actually participated in SDPA aggregation.
1991
  v_ref = v if self.use_xsa else None
1992
 
1993
  # ── Affine-Scaled Attention ───────────────────────────────────────
1994
+ # Active whenever use_affine_scaled_attention=True, regardless of
1995
+ # attention backend. Two code paths — same math, different execution:
1996
+ # eager : full weight access, attn_weights_pre/post_affine captured.
1997
+ # flash/sdpa: α·flash_out + β·V_cumsum, no weight tensors materialised.
1998
  alpha = None
1999
  beta = None
2000
+ use_affine = self.use_affine_scaled_attention
 
 
 
2001
  if use_affine:
2002
  alpha = linear_clipping(self.alpha_proj(hidden_states)) # [B, S, H]
2003
  alpha = alpha.permute(0, 2, 1).unsqueeze(-1) # [B, H, S, 1]
 
2010
  self.affine_momentum * self.alpha_ma
2011
  + (1.0 - self.affine_momentum) * batch_mean
2012
  )
2013
+ if attn_analysis is not None:
2014
+ attn_analysis.alpha_per_head = alpha.detach()
2015
+ attn_analysis.beta_per_head = beta.detach()
2016
+ attn_analysis.alpha_moving_avg = self.alpha_ma.detach()
2017
 
2018
  if use_affine:
2019
+ if self.config._attn_implementation == "eager":
2020
+ # Eager: materialises softmax weights, full analysis available.
2021
+ attn_out, attn_weights = affine_scaled_eager_attention_forward(
2022
+ self, q, k, v, attention_mask,
2023
+ scaling=self.scaling, alpha=alpha, beta=beta,
2024
+ dropout=0.0 if not self.training else self.attention_dropout,
2025
+ attn_analysis=attn_analysis,
2026
+ **kwargs,
2027
+ )
2028
+ else:
2029
+ # Flash / SDPA: exact formula via V.cumsum, no weight tensors.
2030
+ # attn_weights_pre/post_affine remain None in AnalysisState.
2031
+ attn_out, attn_weights = affine_scaled_flash_attention_forward(
2032
+ self, q, k, v, attention_mask,
2033
+ scaling=self.scaling, alpha=alpha, beta=beta,
2034
+ dropout=0.0 if not self.training else self.attention_dropout,
2035
+ attn_analysis=attn_analysis,
2036
+ **kwargs,
2037
+ )
2038
  else:
2039
  attn_fn = eager_attention_forward
2040
  if self.config._attn_implementation != "eager":
 
2044
  dropout=0.0 if not self.training else self.attention_dropout,
2045
  scaling=self.scaling, **kwargs,
2046
  )
2047
+ if attn_analysis is not None:
2048
+ attn_analysis.attn_weights = (
2049
+ attn_weights.detach() if attn_weights is not None else None
2050
+ )
2051
+
2052
+ if attn_analysis is not None:
2053
+ attn_analysis.attn_output_raw = attn_out.detach()
2054
 
2055
  attn_out = attn_out.reshape(*input_shape, -1, self.head_dim)
2056
  if self.use_mea_attention:
2057
  attn_out = self.mea_output_norm(attn_out)
2058
+ if attn_analysis is not None:
2059
+ attn_analysis.attn_output_post_mea_norm = attn_out.detach()
2060
 
2061
  # ── Exclusive Self Attention (position B, pre-routing) ────────────
2062
  # Removes auto-position component before directional routing so that
 
2067
  v_ref_t = v_ref_t.to(attn_out.dtype)
2068
  proj = (attn_out * v_ref_t).sum(dim=-1, keepdim=True)
2069
  norm_sq = (v_ref_t * v_ref_t).sum(dim=-1, keepdim=True).clamp(min=self.xsa_eps)
2070
+ xsa_comp = (proj / norm_sq) * v_ref_t
2071
+ attn_out = attn_out - xsa_comp
2072
+ if attn_analysis is not None:
2073
+ attn_analysis.xsa_self_position_component = xsa_comp.detach()
2074
+ attn_analysis.attn_output_post_xsa = attn_out.detach()
2075
 
2076
  # ── Directional Routing (position C, post-XSA, pre-reshape) ──────
2077
  # Suppresses cross-domain interference directions from the head output.
 
2079
  # When use_xsa=False: directions span full head-space (no XSA pre-clean).
2080
  # When use_directional_routing=False: this block is skipped entirely.
2081
  if self.use_directional_routing:
2082
+ attn_out = self._apply_directional_routing(
2083
+ attn_out, hidden_states, attn_analysis=attn_analysis
2084
+ )
2085
 
2086
  # ── Reshape → o_proj → Gated Attention gate → dropout ────────────
2087
+ attn_out_flat = attn_out.reshape(*input_shape, -1).contiguous()
2088
+ if attn_analysis is not None:
2089
+ attn_analysis.attn_output_pre_gate = attn_out_flat.detach()
2090
+ gate_sig = torch.sigmoid(gate)
2091
+ attn_analysis.gate_sigmoid = gate_sig.detach()
2092
+ attn_out_gated = self.o_proj(attn_out_flat * gate_sig)
2093
+ else:
2094
+ attn_out_gated = self.o_proj(attn_out_flat * torch.sigmoid(gate))
2095
+
2096
+ attn_out_gated = self.dropout(attn_out_gated)
2097
 
2098
+ if attn_analysis is not None:
2099
+ attn_analysis.attn_output_final = attn_out_gated.detach()
2100
+
2101
+ return attn_out_gated, attn_weights, current_layer_fan
2102
  class PolyNorm(nn.Module):
2103
  def __init__(
2104
  self,
 
2137
  out = branch - alpha.to(branch.dtype) * proj_coeff * ref
2138
  return self._norm(out)
2139
 
2140
+ def forward(
2141
+ self,
2142
+ x: torch.Tensor,
2143
+ analysis: Optional[PolyNormAnalysis] = None,
2144
+ ) -> torch.Tensor:
2145
  # Caché de potencias: x_sq reutilizado en x1 y x2; x_cu = x·x_sq evita pow(3)
2146
  x_sq = x.pow(2)
2147
  x_cu = x * x_sq
 
2151
  x2 = x_sq * (x_sq * x_sq).mean(-1, keepdim=True).add(self.eps).rsqrt()
2152
  x3 = x_cu * (x_cu * x_cu).mean(-1, keepdim=True).add(self.eps).rsqrt()
2153
 
2154
+ if analysis is not None:
2155
+ analysis.x1 = x1.detach()
2156
+ analysis.x2_pre_exclusive = x2.detach()
2157
+ analysis.x3_pre_exclusive = x3.detach()
2158
+
2159
  # Fuerzas exclusivas aprendibles
2160
  alpha2, alpha3 = torch.sigmoid(self.exclusive_logits).unbind()
2161
 
2162
+ if analysis is not None:
2163
+ analysis.alpha2 = alpha2.detach()
2164
+ analysis.alpha3 = alpha3.detach()
2165
+ analysis.weights = self.weight.detach()
2166
+ analysis.bias = self.bias.detach()
2167
+
2168
  # Precalcular ref (x1) en fp32 y su norma al cuadrado — compartido por x2 y x3
2169
  x1_f = x1.float()
2170
  ref_norm_sq = x1_f.pow(2).sum(-1, keepdim=True).clamp_min(self.proj_eps)
 
2173
  x2 = self._exclusive(x2, x1, alpha2, x1_f, ref_norm_sq)
2174
  x3 = self._exclusive(x3, x1, alpha3, x1_f, ref_norm_sq)
2175
 
2176
+ if analysis is not None:
2177
+ analysis.x2_post_exclusive = x2.detach()
2178
+ analysis.x3_post_exclusive = x3.detach()
2179
+
2180
+ output = (
2181
  self.weight[0] * x3
2182
  + self.weight[1] * x2
2183
  + self.weight[2] * x1
2184
  + self.bias
2185
  )
2186
 
2187
+ if analysis is not None:
2188
+ analysis.output = output.detach()
2189
+
2190
+ return output
2191
+
2192
 
2193
  class NeoLLMMLP(nn.Module):
2194
  """MLP with FANformer integration and Learnable Multipliers."""
 
2214
  self.act_fn = PolyNorm(exclusive_init=0.05)
2215
  self.dropout = nn.Dropout(config.dropout_rate)
2216
 
2217
+ def forward(
2218
+ self,
2219
+ x: torch.Tensor,
2220
+ analysis: Optional[MLPAnalysis] = None,
2221
+ ) -> torch.Tensor:
2222
+ fan_a = analysis.fan if analysis is not None else None
2223
+ x_fan = self.fan_layer(x, analysis=fan_a)
2224
+
2225
+ gate_out = self.gate_proj(x_fan)
2226
+ up_out = self.up_proj(x_fan)
2227
+
2228
+ if analysis is not None:
2229
+ analysis.gate_proj_output = gate_out.detach()
2230
+ analysis.up_proj_output = up_out.detach()
2231
+
2232
+ poly_a = analysis.polynorm if analysis is not None else None
2233
+ act_out = self.act_fn(gate_out, analysis=poly_a)
2234
+ act_x_up = act_out * up_out
2235
+
2236
+ if analysis is not None:
2237
+ analysis.act_times_up = act_x_up.detach()
2238
+
2239
+ result = self.down_proj(self.dropout(act_x_up))
2240
+
2241
+ if analysis is not None:
2242
+ analysis.output = result.detach()
2243
+
2244
+ return result
2245
 
2246
 
2247
  class NeoLLMDecoderLayer(GradientCheckpointingLayer):
 
2304
  sources: list,
2305
  partial: torch.Tensor,
2306
  query: torch.Tensor,
2307
+ analysis_slot: Optional[AttnResAnalysis] = None,
2308
+ analysis_key: Optional[str] = None,
2309
  ) -> torch.Tensor:
2310
  """
2311
  Depth-wise softmax attention over preceding layer outputs.
 
2347
  B_vals: Optional[torch.Tensor] = None,
2348
  attn_res_sources: Optional[list] = None,
2349
  attn_res_partial: Optional[torch.Tensor] = None,
2350
+ layer_analysis: Optional[LayerAnalysis] = None,
2351
  output_attentions: Optional[bool] = False,
2352
  **kwargs: Unpack[FlashAttentionKwargs],
2353
  ) -> Tuple:
2354
+ # ── Snapshot input ────────────────────────────────────────────────
2355
+ if layer_analysis is not None:
2356
+ layer_analysis.hidden_states_input = hidden_states.detach()
2357
+
2358
  # ── Attention Residuals: compute pre-attention input ──────────────
2359
  # When active, the input to the attention sublayer is no longer the
2360
  # raw hidden_states (accumulated residual) but a softmax-weighted
 
2362
  # attn_res_partial carries the intra-block standard residual that
2363
  # connects the attention and MLP sublayers within this layer.
2364
  # When inactive, flow is identical to the original.
2365
+ ar_analysis = layer_analysis.attn_res if layer_analysis is not None else None
2366
  if self.use_attn_res and attn_res_sources is not None and attn_res_partial is not None:
2367
+ h_attn = self._attn_res(
2368
+ attn_res_sources, attn_res_partial, self.attn_res_query_attn,
2369
+ ar_analysis, "attn",
2370
+ )
2371
  residual_attn = attn_res_partial
2372
  else:
2373
  h_attn = hidden_states
2374
  residual_attn = hidden_states
2375
 
2376
  # ── Attention block ───────────────────────────────────────────────
2377
+ sn_pre = layer_analysis.seednorm_pre_attn if layer_analysis is not None else None
2378
+ h_normed = self.input_layernorm(h_attn, analysis=sn_pre)
2379
+ h_lns = self.lns_attn(h_normed)
2380
+ if layer_analysis is not None:
2381
+ layer_analysis.lns_attn_output = h_lns.detach()
2382
 
2383
  hidden_states, attn_weights, self.current_layer_fan = self.self_attn(
2384
+ hidden_states=h_lns,
2385
  attention_mask=attention_mask,
2386
  position_embeddings=position_embeddings,
2387
  first_layer_fan=first_layer_fan,
2388
+ attn_analysis=layer_analysis.attention if layer_analysis is not None else None,
2389
  **kwargs,
2390
  )
2391
+
2392
+ if layer_analysis is not None:
2393
+ layer_analysis.attn_contribution = hidden_states.detach()
2394
+
2395
+ gpas_attn_a = layer_analysis.gpas_attn if layer_analysis is not None else None
2396
+ h_tilde = self.gpas_attn(residual_attn + hidden_states, analysis=gpas_attn_a)
2397
+
2398
+ if layer_analysis is not None:
2399
+ layer_analysis.h_tilde = h_tilde.detach()
2400
 
2401
  # ── Attention Residuals: compute pre-MLP input ────────────────────
2402
  # After attention, the partial sum is updated with h_tilde.
2403
  # The pre-MLP AttnRes attends over the same sources but with h_tilde
2404
  # as the current partial — capturing the within-layer attention output.
2405
  if self.use_attn_res and attn_res_sources is not None:
2406
+ h_mlp = self._attn_res(
2407
+ attn_res_sources, h_tilde, self.attn_res_query_mlp,
2408
+ ar_analysis, "mlp",
2409
+ )
2410
  residual_mlp = h_tilde
2411
  else:
2412
  h_mlp = h_tilde
2413
  residual_mlp = h_tilde
2414
 
2415
  # ── MLP block ─────────────────────────────────────────────────────
2416
+ sn_post = layer_analysis.seednorm_post_attn if layer_analysis is not None else None
2417
+ h_normed2 = self.post_attention_layernorm(h_mlp, analysis=sn_post)
2418
+ h_lns2 = self.lns_mlp(h_normed2)
2419
+ if layer_analysis is not None:
2420
+ layer_analysis.lns_mlp_output = h_lns2.detach()
2421
+
2422
+ mlp_a = layer_analysis.mlp if layer_analysis is not None else None
2423
+ delta_m = self.mlp(h_lns2, analysis=mlp_a)
2424
+
2425
+ if layer_analysis is not None:
2426
+ layer_analysis.mlp_contribution = delta_m.detach()
2427
 
2428
  # ── JTok-M injection (additive alongside MLP residual) ────────────
2429
  aux_stats = None
 
2433
  z_flat = z_tilde.reshape(-1, z_tilde.shape[-1])
2434
  B_flat = B_vals.reshape(-1, B_vals.shape[-2], B_vals.shape[-1])
2435
 
2436
+ jtokm_a = layer_analysis.jtokm if layer_analysis is not None else None
2437
+ delta_r, aux_stats = self.jtokm(h_flat, z_flat, B_flat, analysis=jtokm_a)
2438
  delta_r = delta_r.reshape(orig_shape)
2439
 
2440
+ gpas_mlp_a = layer_analysis.gpas_mlp if layer_analysis is not None else None
2441
+ hidden_states = self.gpas_mlp(residual_mlp + delta_m + delta_r, analysis=gpas_mlp_a)
2442
  else:
2443
+ gpas_mlp_a = layer_analysis.gpas_mlp if layer_analysis is not None else None
2444
+ hidden_states = self.gpas_mlp(residual_mlp + delta_m, analysis=gpas_mlp_a)
2445
+
2446
+ if layer_analysis is not None:
2447
+ layer_analysis.hidden_states_output = hidden_states.detach()
2448
 
2449
  outputs = (hidden_states,)
2450
  if output_attentions:
 
2631
  else:
2632
  self.embed_tokens = value
2633
 
2634
+ def _build_layer_analysis(self) -> LayerAnalysis:
2635
+ """
2636
+ Construct a LayerAnalysis with sub-objects pre-allocated for every
2637
+ component that is active in the current config.
2638
+
2639
+ Fields that correspond to disabled flags remain None — the analysis
2640
+ consumer can check for None without needing to inspect config flags.
2641
+ Called once per layer per forward when analysis is active.
2642
+ """
2643
+ cfg = self.config
2644
+ return LayerAnalysis(
2645
+ seednorm_pre_attn = SeeDNormAnalysis(),
2646
+ seednorm_post_attn = SeeDNormAnalysis(),
2647
+ attention = AttentionAnalysis(fan=FANAnalysis()),
2648
+ mlp = MLPAnalysis(
2649
+ fan = FANAnalysis(),
2650
+ polynorm = PolyNormAnalysis(),
2651
+ ),
2652
+ gpas_attn = GPASAnalysis(),
2653
+ gpas_mlp = GPASAnalysis(),
2654
+ jtokm = JTokMAnalysis() if cfg.use_jtokm else None,
2655
+ attn_res = AttnResAnalysis() if getattr(cfg, "use_attn_res", False) else None,
2656
+ )
2657
+
2658
  def forward(
2659
  self,
2660
  input_ids: Optional[torch.LongTensor] = None,
 
2664
  output_hidden_states: Optional[bool] = None,
2665
  output_attentions: Optional[bool] = None,
2666
  return_dict: Optional[bool] = None,
2667
+ analysis_state: Optional[AnalysisState] = None,
2668
  **kwargs: Unpack[TransformersKwargs],
2669
  ) -> Tuple:
2670
  output_hidden_states = (
 
2684
  z_tilde = None
2685
  B_vals = None
2686
 
2687
+ gen_a = (
2688
+ analysis_state.generator
2689
+ if analysis_state is not None and self.config.use_token_generator
2690
+ else None
2691
+ )
2692
+
2693
  if inputs_embeds is None:
2694
  if self.config.use_token_generator:
2695
  if self.config.use_jtokm:
2696
  # Return internals for reuse by JTok-M surfaces
2697
  inputs_embeds, z_tilde, B_vals = self.token_generator(
2698
+ input_ids, return_internals=True, analysis=gen_a
2699
  )
2700
  # Reshape to [batch, seq, d_seed] and [batch, seq, d_seed, n_knots]
2701
  z_tilde = z_tilde.reshape(*input_ids.shape, self.config.generator_d_seed)
 
2705
  self.config.generator_num_knots,
2706
  )
2707
  else:
2708
+ inputs_embeds = self.token_generator(input_ids, analysis=gen_a)
2709
  else:
2710
  inputs_embeds = self.embed_tokens(input_ids)
2711
 
2712
+ if analysis_state is not None:
2713
+ analysis_state.embeddings = inputs_embeds.detach()
2714
+
2715
  if position_ids is None:
2716
  position_ids = torch.arange(
2717
  0, inputs_embeds.shape[1], device=inputs_embeds.device
 
2758
  else 1 # Full AttnRes: every layer is its own "block"
2759
  )
2760
 
2761
+ # Pre-allocate per-layer analysis list when analysis is active
2762
+ if analysis_state is not None:
2763
+ analysis_state.layers = []
2764
+
2765
  for layer_idx, decoder_layer in enumerate(self.layers[: self.config.num_hidden_layers]):
2766
  if output_hidden_states:
2767
  all_hidden_states = all_hidden_states + (hidden_states,)
 
2778
  attn_res_sources = attn_res_sources + [attn_res_partial]
2779
  attn_res_partial = hidden_states # start new block from current output
2780
 
2781
+ # Build per-layer analysis container (only in eval + analysis mode)
2782
+ layer_analysis = None
2783
+ if analysis_state is not None:
2784
+ layer_analysis = self._build_layer_analysis()
2785
+ layer_analysis.layer_idx = layer_idx
2786
+ analysis_state.layers.append(layer_analysis)
2787
+
2788
  layer_outputs = decoder_layer(
2789
  hidden_states,
2790
  position_embeddings=position_embeddings,
 
2794
  B_vals=B_vals,
2795
  attn_res_sources=attn_res_sources,
2796
  attn_res_partial=attn_res_partial if use_attn_res else None,
2797
+ layer_analysis=layer_analysis,
2798
  output_attentions=output_attentions,
2799
  **kwargs,
2800
  )
 
2820
  if output_hidden_states:
2821
  all_hidden_states = all_hidden_states + (hidden_states,)
2822
 
2823
+ # ── Finalise analysis snapshot ─────────────────────────────────────
2824
+ if analysis_state is not None:
2825
+ analysis_state.final_hidden_states = hidden_states.detach()
2826
+ analysis_state.jtokm_aux_stats = all_aux_stats if self.config.use_jtokm else None
2827
+ analysis_state.attn_res_sources_final = (
2828
+ attn_res_sources if use_attn_res else None
2829
+ )
2830
+
2831
  if not return_dict:
2832
  return tuple(
2833
  v for v in [hidden_states, None, all_hidden_states, all_attentions]
 
2870
  total_loss = CE_loss + L_aux
2871
 
2872
  where L_aux = λ · n_e · (1/L) · Σ_ℓ Σ_i p_i^ℓ · f_i^ℓ
2873
+
2874
+ ── Analysis mode ──────────────────────────────────────────────────────────
2875
+ Analysis is NEVER active during training (model.training=True).
2876
+ It is opt-in at inference time:
2877
+
2878
+ model.eval()
2879
+ model.enable_analysis() # arm the system
2880
+ with torch.no_grad():
2881
+ _ = model(input_ids)
2882
+ state = model.last_analysis # AnalysisState with all internals
2883
+
2884
+ model.disable_analysis() # disarm — zero overhead again
2885
+ # or: model.train() # training always disarms automatically
2886
+
2887
+ last_analysis is replaced at each forward call when analysis is armed.
2888
+ last_analysis is None between enable_analysis() and the first forward,
2889
+ and cleared by disable_analysis().
2890
+
2891
+ Example field access:
2892
+ state.layers[3].attention.alpha_per_head # Affine-Scaled α
2893
+ state.layers[0].mlp.polynorm.x2_post_exclusive
2894
+ state.generator.z_tilde # Leviathan latent
2895
+ state.layers[2].jtokm.routing_weights # JTok-M router
2896
+ state.layers[5].attn_res.weights_pre_attn # AttnRes softmax
2897
  """
2898
 
2899
  _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
 
2907
  if config.use_token_generator:
2908
  self._tied_weights_keys = {}
2909
 
2910
+ # ── Analysis infrastructure ───────────────────────────────────────
2911
+ # _analysis_armed: set by enable_analysis() / disable_analysis().
2912
+ # last_analysis: populated after each forward when armed + eval.
2913
+ # Neither is an nn.Parameter or buffer — zero effect on training.
2914
+ self._analysis_armed: bool = False
2915
+ self.last_analysis: Optional[AnalysisState] = None
2916
+
2917
  self.post_init()
2918
 
2919
+ # ── Public analysis API ───────────────────────────────────────────────
2920
+
2921
+ def enable_analysis(self) -> None:
2922
+ """
2923
+ Arm the analysis system.
2924
+
2925
+ Has no effect during training — analysis is always off when
2926
+ model.training=True, regardless of this flag. Safe to call at any
2927
+ point without affecting the training loop.
2928
+
2929
+ Usage::
2930
+
2931
+ model.eval()
2932
+ model.enable_analysis()
2933
+ with torch.no_grad():
2934
+ _ = model(input_ids)
2935
+ state = model.last_analysis
2936
+ """
2937
+ self._analysis_armed = True
2938
+
2939
+ def disable_analysis(self) -> None:
2940
+ """
2941
+ Disarm the analysis system and clear last_analysis.
2942
+
2943
+ After this call, forward passes produce zero analysis overhead.
2944
+ """
2945
+ self._analysis_armed = False
2946
+ self.last_analysis = None
2947
+
2948
+ def _make_analysis_state(
2949
+ self,
2950
+ input_ids: Optional[torch.Tensor],
2951
+ ) -> Optional[AnalysisState]:
2952
+ """
2953
+ Decide whether to build an AnalysisState for this forward pass.
2954
+
2955
+ Returns None (zero cost) when:
2956
+ - analysis is not armed, OR
2957
+ - the model is in training mode (self.training=True).
2958
+ Otherwise returns a fresh AnalysisState with top-level optional
2959
+ fields pre-allocated according to the active config flags.
2960
+ """
2961
+ if not self._analysis_armed or self.training:
2962
+ return None
2963
+ cfg = self.config
2964
+ return AnalysisState(
2965
+ input_ids = input_ids.detach() if input_ids is not None else None,
2966
+ generator = GeneratorAnalysis() if cfg.use_token_generator else None,
2967
+ layers = None, # filled by NeoLLMModel.forward
2968
+ jtokm_aux_stats = [] if cfg.use_jtokm else None,
2969
+ attn_res_sources_final = [] if getattr(cfg, "use_attn_res", False) else None,
2970
+ )
2971
+
2972
+ # ── Standard model API ────────────────────────────────────────────────
2973
+
2974
  def get_input_embeddings(self):
2975
  return self.model.get_input_embeddings()
2976
 
 
3020
  return_dict: Optional[bool] = None,
3021
  **kwargs: Unpack[TransformersKwargs],
3022
  ) -> CausalLMOutputWithPast:
3023
+ # ── Build analysis container (None during training or when disarmed) ──
3024
+ analysis_state = self._make_analysis_state(input_ids)
3025
+
3026
  model_out = self.model(
3027
  input_ids=input_ids,
3028
  attention_mask=attention_mask,
 
3030
  inputs_embeds=inputs_embeds,
3031
  output_hidden_states=output_hidden_states,
3032
  return_dict=return_dict,
3033
+ analysis_state=analysis_state,
3034
  **kwargs,
3035
  )
3036
 
 
3069
  )
3070
  logits = self.lm_head(hidden_states[:, slice_indices, :])
3071
 
3072
+ # ── Finalise and store analysis state ─────────────────────────────
3073
+ if analysis_state is not None:
3074
+ analysis_state.logits = logits.detach() if logits is not None else None
3075
+ self.last_analysis = analysis_state
3076
+
3077
  return CausalLMOutputWithPast(
3078
  loss=loss,
3079
  logits=logits,
 
3098
  "VectorMultiplier",
3099
  "LinearWithMultipliers",
3100
  "MEAHeadSeeDNorm",
3101
+ # Analysis dataclasses — exported so external tools can type-hint against them
3102
+ "AnalysisState",
3103
+ "LayerAnalysis",
3104
+ "AttentionAnalysis",
3105
+ "MLPAnalysis",
3106
+ "FANAnalysis",
3107
+ "SeeDNormAnalysis",
3108
+ "GPASAnalysis",
3109
+ "PolyNormAnalysis",
3110
+ "JTokMAnalysis",
3111
+ "AttnResAnalysis",
3112
+ "GeneratorAnalysis",
3113
  ]
3114
 
3115
  AutoConfig.register("neollm", NeoLLMConfig)