KitsuVp commited on
Commit
02c3769
·
verified ·
1 Parent(s): 0b7ae1a

Update modeling_neollm.py

Browse files
Files changed (1) hide show
  1. modeling_neollm.py +541 -60
modeling_neollm.py CHANGED
@@ -280,6 +280,8 @@ class MLPAnalysis:
280
  """
281
  Internals of a NeoLLMMLP forward pass.
282
  SwiGLU-like: down_proj(dropout(PolyNorm(gate_proj(fan)) · up_proj(fan)))
 
 
283
  """
284
  fan: Optional[FANAnalysis] = None # FAN components for MLP
285
  gate_proj_output: Optional[torch.Tensor] = None # gate_proj(x_fan) [B,S,I]
@@ -288,6 +290,37 @@ class MLPAnalysis:
288
  act_times_up: Optional[torch.Tensor] = None # PolyNorm(gate)·up [B,S,I]
289
  output: Optional[torch.Tensor] = None # after down_proj [B,S,D]
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
  @dataclass
293
  class JTokMAnalysis:
@@ -1911,8 +1944,8 @@ def _apply_repo_rope(
1911
  n_groups = H // H_kv
1912
  rotary_dim = inv_freq.shape[0] * 2 # inv_freq covers half the rotary dim
1913
 
1914
- # inv_freq arrives from rotary_emb at forward time via repo_rope_args
1915
- # already float32 on the correct device, no .to() needed, no DeviceCopy op.
1916
  # No autocast barrier: explicit .float() casts on z_q/z_k are sufficient
1917
  # to maintain float32 precision for the trig ops. Removing the context
1918
  # manager lets Inductor plan all intermediate tensors as part of a single
@@ -2182,9 +2215,37 @@ class NeoLLMAttention(nn.Module):
2182
  d_p=_d_p,
2183
  num_heads=config.num_attention_heads,
2184
  )
 
 
 
 
2185
  else:
2186
  self.repo_module = None
2187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2188
  def _apply_momentum_attention(
2189
  self,
2190
  q: torch.Tensor,
@@ -2320,7 +2381,6 @@ class NeoLLMAttention(nn.Module):
2320
  attention_mask: Optional[torch.Tensor] = None,
2321
  first_layer_fan: Optional[torch.Tensor] = None,
2322
  attn_analysis: Optional[AttentionAnalysis] = None,
2323
- repo_rope_args: Optional[Tuple[torch.Tensor, float]] = None,
2324
  **kwargs: Unpack[FlashAttentionKwargs],
2325
  ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
2326
  input_shape = hidden_states.shape[:-1]
@@ -2358,14 +2418,14 @@ class NeoLLMAttention(nn.Module):
2358
  # REPO path: f_ϕ predicts continuous per-head positions from the
2359
  # residual stream, then cos/sin are built inline from those positions
2360
  # so the rotation is differentiable w.r.t. REPOModule parameters.
2361
- # inv_freq and attention_scaling arrive via repo_rope_args, sourced
2362
- # directly from rotary_emb at forward time — no buffer on this module,
2363
- # no meta-tensor issue on lm_eval / to(device) paths.
2364
  # (Li et al., 2026, §3.2 — Eq. 6–7)
2365
  repo_a = attn_analysis.repo if attn_analysis is not None else None
2366
  z = self.repo_module(hidden_states, repo_analysis=repo_a) # [B, H, S]
2367
- inv_freq, attn_scaling = repo_rope_args
2368
- q, k = _apply_repo_rope(q, k, z, inv_freq, attn_scaling)
 
 
 
2369
  else:
2370
  # Standard path: integer positions pre-computed by NeoLLMModel.
2371
  q, k = apply_rotary_pos_emb(q, k, cos, sin)
@@ -2502,18 +2562,21 @@ class PolyNorm(nn.Module):
2502
  eps: float = 1e-6,
2503
  proj_eps: float = 1e-6,
2504
  exclusive_init: float = 0.5,
 
2505
  ):
2506
  super().__init__()
2507
- self.weight = nn.Parameter(torch.ones(3) / 3)
2508
- self.bias = nn.Parameter(torch.zeros(1))
2509
- self.eps = eps
2510
- self.proj_eps = proj_eps
2511
-
2512
- # Dos fuerzas exclusivas aprendibles en (0, 1), una por rama de orden alto.
2513
- # Se parametrizan con logits para que sigmoid mantenga alpha ∈ (0, 1).
2514
- exclusive_init = float(min(max(exclusive_init, 1e-4), 1.0 - 1e-4))
2515
- init = torch.full((2,), exclusive_init, dtype=torch.float32)
2516
- self.exclusive_logits = nn.Parameter(torch.logit(init))
 
 
2517
 
2518
  def _norm(self, x):
2519
  return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
@@ -2543,7 +2606,7 @@ class PolyNorm(nn.Module):
2543
  x_sq = x.pow(2)
2544
  x_cu = x * x_sq
2545
 
2546
- # Tres ramas normalizadas (same as original PolyNorm)
2547
  x1 = x * x_sq.mean(-1, keepdim=True).add(self.eps).rsqrt()
2548
  x2 = x_sq * (x_sq * x_sq).mean(-1, keepdim=True).add(self.eps).rsqrt()
2549
  x3 = x_cu * (x_cu * x_cu).mean(-1, keepdim=True).add(self.eps).rsqrt()
@@ -2553,26 +2616,31 @@ class PolyNorm(nn.Module):
2553
  analysis.x2_pre_exclusive = x2.detach()
2554
  analysis.x3_pre_exclusive = x3.detach()
2555
 
2556
- # Fuerzas exclusivas aprendibles
2557
- alpha2, alpha3 = torch.sigmoid(self.exclusive_logits).unbind()
 
2558
 
2559
- if analysis is not None:
2560
- analysis.alpha2 = alpha2.detach()
2561
- analysis.alpha3 = alpha3.detach()
2562
- analysis.weights = self.weight.detach()
2563
- analysis.bias = self.bias.detach()
2564
 
2565
- # Precalcular ref (x1) en fp32 y su norma al cuadrado — compartido por x2 y x3
2566
- x1_f = x1.float()
2567
- ref_norm_sq = x1_f.pow(2).sum(-1, keepdim=True).clamp_min(self.proj_eps)
2568
 
2569
- # Ortogonalización parcial de las ramas de orden alto respecto a la lineal
2570
- x2 = self._exclusive(x2, x1, alpha2, x1_f, ref_norm_sq)
2571
- x3 = self._exclusive(x3, x1, alpha3, x1_f, ref_norm_sq)
2572
 
2573
- if analysis is not None:
2574
- analysis.x2_post_exclusive = x2.detach()
2575
- analysis.x3_post_exclusive = x3.detach()
 
 
 
 
2576
 
2577
  output = (
2578
  self.weight[0] * x3
@@ -2587,6 +2655,379 @@ class PolyNorm(nn.Module):
2587
  return output
2588
 
2589
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2590
  class NeoLLMMLP(nn.Module):
2591
  """MLP with FANformer integration and Learnable Multipliers."""
2592
 
@@ -2608,7 +3049,7 @@ class NeoLLMMLP(nn.Module):
2608
  config.intermediate_size, config.hidden_size,
2609
  bias=False, use_row_multiplier=True, use_column_multiplier=True,
2610
  )
2611
- self.act_fn = PolyNorm(exclusive_init=0.05)
2612
  self.dropout = nn.Dropout(config.dropout_rate)
2613
 
2614
  def forward(
@@ -2664,7 +3105,12 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
2664
  self.use_jtokm = config.use_jtokm
2665
 
2666
  self.self_attn = NeoLLMAttention(config, layer_idx)
2667
- self.mlp = NeoLLMMLP(config)
 
 
 
 
 
2668
  self.input_layernorm = SeeDNorm(config.hidden_size, eps=config.rms_norm_eps)
2669
  self.post_attention_layernorm = SeeDNorm(config.hidden_size, eps=config.rms_norm_eps)
2670
  self.lns_attn = LNS(layer_idx)
@@ -2746,7 +3192,6 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
2746
  attn_res_partial: Optional[torch.Tensor] = None,
2747
  layer_analysis: Optional[LayerAnalysis] = None,
2748
  output_attentions: Optional[bool] = False,
2749
- repo_rope_args: Optional[Tuple[torch.Tensor, float]] = None,
2750
  **kwargs: Unpack[FlashAttentionKwargs],
2751
  ) -> Tuple:
2752
  # ── Snapshot input ────────────────────────────────────────────────
@@ -2784,7 +3229,6 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
2784
  position_embeddings=position_embeddings,
2785
  first_layer_fan=first_layer_fan,
2786
  attn_analysis=layer_analysis.attention if layer_analysis is not None else None,
2787
- repo_rope_args=repo_rope_args,
2788
  **kwargs,
2789
  )
2790
 
@@ -2819,7 +3263,11 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
2819
  layer_analysis.lns_mlp_output = h_lns2.detach()
2820
 
2821
  mlp_a = layer_analysis.mlp if layer_analysis is not None else None
2822
- delta_m = self.mlp(h_lns2, analysis=mlp_a)
 
 
 
 
2823
 
2824
  if layer_analysis is not None:
2825
  layer_analysis.mlp_contribution = delta_m.detach()
@@ -2850,6 +3298,8 @@ class NeoLLMDecoderLayer(GradientCheckpointingLayer):
2850
  outputs += (attn_weights,)
2851
  if aux_stats is not None:
2852
  outputs += (aux_stats,)
 
 
2853
  return outputs
2854
 
2855
 
@@ -3304,6 +3754,17 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3304
 
3305
  self.post_init()
3306
 
 
 
 
 
 
 
 
 
 
 
 
3307
  def get_input_embeddings(self):
3308
  if self.config.use_token_generator:
3309
  return self.token_generator
@@ -3329,6 +3790,7 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3329
  getattr(cfg, "use_repo", False)
3330
  and layer_idx >= getattr(cfg, "repo_start_layer", cfg.num_hidden_layers // 3)
3331
  )
 
3332
  return LayerAnalysis(
3333
  seednorm_pre_attn = SeeDNormAnalysis(),
3334
  seednorm_post_attn = SeeDNormAnalysis(),
@@ -3338,8 +3800,9 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3338
  repo = REPOAnalysis() if _repo_active else None,
3339
  ),
3340
  mlp = MLPAnalysis(
3341
- fan = FANAnalysis(),
3342
- polynorm = PolyNormAnalysis(),
 
3343
  ),
3344
  gpas_attn = GPASAnalysis(),
3345
  gpas_mlp = GPASAnalysis(),
@@ -3435,17 +3898,6 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3435
  position_embeddings = self.rotary_emb(hidden_states, position_ids)
3436
  self.first_layer_fan = None
3437
 
3438
- # ── REPO: pass inv_freq by reference at forward time ──────────────────
3439
- # rotary_emb.inv_freq is already on the correct device (managed by
3440
- # NeoLLMRotaryEmbedding as a buffer) — no .to(), no DeviceCopy op.
3441
- # Computed once here and passed through the decoder layer chain so
3442
- # NeoLLMAttention never needs to store it as a buffer itself, avoiding
3443
- # the meta-tensor issue that occurs when lm_eval calls .to(device).
3444
- repo_rope_args = (
3445
- (self.rotary_emb.inv_freq, self.rotary_emb.attention_scaling)
3446
- if getattr(self.config, "use_repo", False) else None
3447
- )
3448
-
3449
  # ── Attention Residuals state ──────────────────────────────────────
3450
  # Full AttnRes (attn_res_num_blocks=0): sources grows by one entry per
3451
  # decoder layer — all previous outputs are kept, max N=num_layers+1.
@@ -3506,7 +3958,6 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3506
  attn_res_partial=attn_res_partial if use_attn_res else None,
3507
  layer_analysis=layer_analysis,
3508
  output_attentions=output_attentions,
3509
- repo_rope_args=repo_rope_args,
3510
  **kwargs,
3511
  )
3512
  hidden_states = layer_outputs[0]
@@ -3522,6 +3973,15 @@ class NeoLLMModel(NeoLLMPreTrainedModel):
3522
  if self.config.use_jtokm and len(layer_outputs) > (2 if output_attentions else 1):
3523
  all_aux_stats.append(layer_outputs[-1])
3524
 
 
 
 
 
 
 
 
 
 
3525
  if (self.first_layer_fan is None
3526
  and hasattr(decoder_layer, "current_layer_fan")):
3527
  self.first_layer_fan = decoder_layer.current_layer_fan
@@ -3766,12 +4226,30 @@ class NeoLLMForCausalLM(NeoLLMPreTrainedModel, GenerationMixin):
3766
  )
3767
  # Add JTok-M load-balancing auxiliary loss
3768
  if self.config.use_jtokm and all_aux_stats:
3769
- aux_loss = compute_jtokm_aux_loss(
3770
- all_aux_stats,
3771
- n_e=self.config.jtokm_num_experts,
3772
- weight=self.config.jtokm_aux_loss_weight,
3773
- )
3774
- loss = loss + aux_loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3775
  logits = None
3776
  else:
3777
  slice_indices = (
@@ -3812,6 +4290,8 @@ __all__ = [
3812
  "MEAHeadSeeDNorm",
3813
  "HadamardOProj",
3814
  "REPOModule",
 
 
3815
  # Analysis dataclasses — exported so external tools can type-hint against them
3816
  "AnalysisState",
3817
  "LayerAnalysis",
@@ -3823,6 +4303,7 @@ __all__ = [
3823
  "PolyNormAnalysis",
3824
  "HadamardAnalysis",
3825
  "REPOAnalysis",
 
3826
  "JTokMAnalysis",
3827
  "AttnResAnalysis",
3828
  "GeneratorAnalysis",
 
280
  """
281
  Internals of a NeoLLMMLP forward pass.
282
  SwiGLU-like: down_proj(dropout(PolyNorm(gate_proj(fan)) · up_proj(fan)))
283
+ When use_versatile_ffn=True, the versatile sub-object carries all
284
+ VersatileFFN internals and fan/gate/up/polynorm fields remain None.
285
  """
286
  fan: Optional[FANAnalysis] = None # FAN components for MLP
287
  gate_proj_output: Optional[torch.Tensor] = None # gate_proj(x_fan) [B,S,I]
 
290
  act_times_up: Optional[torch.Tensor] = None # PolyNorm(gate)·up [B,S,I]
291
  output: Optional[torch.Tensor] = None # after down_proj [B,S,D]
292
 
293
+ # ── VersatileFFN (conditional on use_versatile_ffn) ──────────────────
294
+ versatile: Optional["VersatileFFNAnalysis"] = None # None when standard MLP
295
+
296
+
297
+ @dataclass
298
+ class VersatileFFNAnalysis:
299
+ """
300
+ Internals of a VersatileFFN forward pass.
301
+ Only populated when use_versatile_ffn=True.
302
+
303
+ Reference: Nie et al. (2026). arXiv:2512.14531.
304
+
305
+ depth_probs: Gumbel-Softmax distribution p [B, S, max_depth] — only in
306
+ training (hard=True STE). None during inference.
307
+ expected_loops: E[L] per token [B, S] — differentiable proxy for difficulty.
308
+ During inference computed from discrete argmax+1.
309
+ moe_weight: λ = (L_max - E[L]) / L_max [B, S] — fusion gate scalar.
310
+ Near 1 → width dominates (easy token).
311
+ Near 0 → depth dominates (hard token).
312
+ loop_choice: argmax(depth_logits) [B, S] — discrete depth selected at
313
+ inference. None during training.
314
+ x_depth: Output of the depth-versatile path [B, S, D].
315
+ x_width: Output of the width-versatile MoE path [B, S, D].
316
+ """
317
+ depth_probs: Optional[torch.Tensor] = None # [B, S, max_depth] training only
318
+ expected_loops: Optional[torch.Tensor] = None # [B, S]
319
+ moe_weight: Optional[torch.Tensor] = None # [B, S]
320
+ loop_choice: Optional[torch.Tensor] = None # [B, S] inference only
321
+ x_depth: Optional[torch.Tensor] = None # [B, S, D]
322
+ x_width: Optional[torch.Tensor] = None # [B, S, D]
323
+
324
 
325
  @dataclass
326
  class JTokMAnalysis:
 
1944
  n_groups = H // H_kv
1945
  rotary_dim = inv_freq.shape[0] * 2 # inv_freq covers half the rotary dim
1946
 
1947
+ # inv_freq is already float32 on the correct device (registered as buffer
1948
+ # via set_repo_inv_freq) no .to() needed, no DeviceCopy op.
1949
  # No autocast barrier: explicit .float() casts on z_q/z_k are sufficient
1950
  # to maintain float32 precision for the trig ops. Removing the context
1951
  # manager lets Inductor plan all intermediate tensors as part of a single
 
2215
  d_p=_d_p,
2216
  num_heads=config.num_attention_heads,
2217
  )
2218
+ # _repo_inv_freq is registered as a non-persistent buffer by
2219
+ # set_repo_inv_freq(), called from NeoLLMModel.__init__ after
2220
+ # rotary_emb is built. Declaring it here would conflict.
2221
+ self._repo_attn_scaling: float = 1.0
2222
  else:
2223
  self.repo_module = None
2224
 
2225
+ def set_repo_inv_freq(
2226
+ self,
2227
+ inv_freq: torch.Tensor,
2228
+ attention_scaling: float,
2229
+ ) -> None:
2230
+ """
2231
+ Inject the rotary frequency vector from NeoLLMRotaryEmbedding so that
2232
+ REPO can build cos/sin inline from continuous positions.
2233
+
2234
+ Called once by NeoLLMModel.__init__ after rotary_emb is constructed.
2235
+ Only has effect when use_repo=True for this layer.
2236
+
2237
+ Args:
2238
+ inv_freq: [rotary_dim/2] — frozen inv_freq buffer from
2239
+ NeoLLMRotaryEmbedding.
2240
+ attention_scaling: float — attention_scaling from the same module.
2241
+ """
2242
+ if self.use_repo:
2243
+ # Register as non-persistent buffer so .to(device) / .cuda() moves
2244
+ # it automatically — eliminates the DeviceCopy op that splits the
2245
+ # CUDAGraph into 2 partitions when _apply_repo_rope runs.
2246
+ self.register_buffer("_repo_inv_freq", inv_freq.float(), persistent=False)
2247
+ self._repo_attn_scaling = attention_scaling
2248
+
2249
  def _apply_momentum_attention(
2250
  self,
2251
  q: torch.Tensor,
 
2381
  attention_mask: Optional[torch.Tensor] = None,
2382
  first_layer_fan: Optional[torch.Tensor] = None,
2383
  attn_analysis: Optional[AttentionAnalysis] = None,
 
2384
  **kwargs: Unpack[FlashAttentionKwargs],
2385
  ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
2386
  input_shape = hidden_states.shape[:-1]
 
2418
  # REPO path: f_ϕ predicts continuous per-head positions from the
2419
  # residual stream, then cos/sin are built inline from those positions
2420
  # so the rotation is differentiable w.r.t. REPOModule parameters.
 
 
 
2421
  # (Li et al., 2026, §3.2 — Eq. 6–7)
2422
  repo_a = attn_analysis.repo if attn_analysis is not None else None
2423
  z = self.repo_module(hidden_states, repo_analysis=repo_a) # [B, H, S]
2424
+ q, k = _apply_repo_rope(
2425
+ q, k, z,
2426
+ self._repo_inv_freq,
2427
+ self._repo_attn_scaling,
2428
+ )
2429
  else:
2430
  # Standard path: integer positions pre-computed by NeoLLMModel.
2431
  q, k = apply_rotary_pos_emb(q, k, cos, sin)
 
2562
  eps: float = 1e-6,
2563
  proj_eps: float = 1e-6,
2564
  exclusive_init: float = 0.5,
2565
+ exclusive: bool = True,
2566
  ):
2567
  super().__init__()
2568
+ self.weight = nn.Parameter(torch.ones(3) / 3)
2569
+ self.bias = nn.Parameter(torch.zeros(1))
2570
+ self.eps = eps
2571
+ self.exclusive = exclusive
2572
+
2573
+ if exclusive:
2574
+ self.proj_eps = proj_eps
2575
+ # Dos fuerzas exclusivas aprendibles en (0, 1), una por rama de orden alto.
2576
+ # Se parametrizan con logits para que sigmoid mantenga alpha ∈ (0, 1).
2577
+ exclusive_init = float(min(max(exclusive_init, 1e-4), 1.0 - 1e-4))
2578
+ init = torch.full((2,), exclusive_init, dtype=torch.float32)
2579
+ self.exclusive_logits = nn.Parameter(torch.logit(init))
2580
 
2581
  def _norm(self, x):
2582
  return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
 
2606
  x_sq = x.pow(2)
2607
  x_cu = x * x_sq
2608
 
2609
+ # Tres ramas normalizadas
2610
  x1 = x * x_sq.mean(-1, keepdim=True).add(self.eps).rsqrt()
2611
  x2 = x_sq * (x_sq * x_sq).mean(-1, keepdim=True).add(self.eps).rsqrt()
2612
  x3 = x_cu * (x_cu * x_cu).mean(-1, keepdim=True).add(self.eps).rsqrt()
 
2616
  analysis.x2_pre_exclusive = x2.detach()
2617
  analysis.x3_pre_exclusive = x3.detach()
2618
 
2619
+ if self.exclusive:
2620
+ # Fuerzas exclusivas aprendibles
2621
+ alpha2, alpha3 = torch.sigmoid(self.exclusive_logits).unbind()
2622
 
2623
+ if analysis is not None:
2624
+ analysis.alpha2 = alpha2.detach()
2625
+ analysis.alpha3 = alpha3.detach()
2626
+ analysis.weights = self.weight.detach()
2627
+ analysis.bias = self.bias.detach()
2628
 
2629
+ # Precalcular ref (x1) en fp32 y su norma al cuadrado — compartido por x2 y x3
2630
+ x1_f = x1.float()
2631
+ ref_norm_sq = x1_f.pow(2).sum(-1, keepdim=True).clamp_min(self.proj_eps)
2632
 
2633
+ # Ortogonalización parcial de las ramas de orden alto respecto a la lineal
2634
+ x2 = self._exclusive(x2, x1, alpha2, x1_f, ref_norm_sq)
2635
+ x3 = self._exclusive(x3, x1, alpha3, x1_f, ref_norm_sq)
2636
 
2637
+ if analysis is not None:
2638
+ analysis.x2_post_exclusive = x2.detach()
2639
+ analysis.x3_post_exclusive = x3.detach()
2640
+ else:
2641
+ if analysis is not None:
2642
+ analysis.weights = self.weight.detach()
2643
+ analysis.bias = self.bias.detach()
2644
 
2645
  output = (
2646
  self.weight[0] * x3
 
2655
  return output
2656
 
2657
 
2658
+ def compute_versatile_aux_loss(
2659
+ aux_stats_list: list,
2660
+ n_experts: int,
2661
+ weight: float,
2662
+ ) -> torch.Tensor:
2663
+ """
2664
+ Load-balancing auxiliary loss for VersatileFFN width-path expert routing.
2665
+
2666
+ Identical formula to JTok-M: L_aux = λ · n_e · Σ_i p_i · f_i
2667
+ averaged over all decoder layers with active VersatileFFN.
2668
+
2669
+ Args:
2670
+ aux_stats_list: list of (p_sum [n_e], f_sum [n_e], N_tokens) per layer.
2671
+ n_experts: total number of virtual experts.
2672
+ weight: λ coefficient.
2673
+ Returns:
2674
+ Scalar loss tensor.
2675
+ """
2676
+ total_loss = None
2677
+ for p_sum, f_sum, N in aux_stats_list:
2678
+ p_i = p_sum / N
2679
+ layer_loss = weight * n_experts * (p_i * f_sum).sum()
2680
+ total_loss = layer_loss if total_loss is None else total_loss + layer_loss
2681
+ if total_loss is None:
2682
+ return torch.tensor(0.0)
2683
+ return total_loss / len(aux_stats_list)
2684
+
2685
+
2686
+ class VersatileFFN(nn.Module):
2687
+ """
2688
+ VersatileFFN: dual-process feed-forward network with parameter reuse.
2689
+
2690
+ Drop-in replacement for NeoLLMMLP. Shares the same weight matrices but
2691
+ reuses them across two complementary paths (Nie et al., 2026):
2692
+
2693
+ Width-Versatile path (virtual MoE, Eq. 7–8):
2694
+ Derives N virtual sub-experts by slicing non-overlapping contiguous
2695
+ segments of the intermediate dimension. A Top-K router selects
2696
+ ``active_experts`` experts per token. No additional parameters beyond
2697
+ the router ``expert_gate [hidden, N_experts]``.
2698
+
2699
+ Depth-Versatile path (recursive, Eq. 9–11):
2700
+ Applies the full shared MLP (inner SeeDNorm + FANLayer + gate/up/down)
2701
+ recursively up to ``max_depth`` times. A Gumbel-Softmax loop predictor
2702
+ (``depth_predictor [hidden, max_depth]``) decides per-token depth.
2703
+ During training: always L_max iterations with soft STE weighting.
2704
+ During inference: early exit at argmax(p) to save FLOPs.
2705
+
2706
+ Difficulty-aware fusion (Eq. 12–13):
2707
+ λ = (L_max − E[L]) / L_max ∈ [0, 1)
2708
+ output = λ · Y_width + (1 − λ) · Y_depth
2709
+ Easy tokens (low E[L] → λ → 1) favour the fast width path.
2710
+ Hard tokens (high E[L] → λ → 0) favour the deep recursive path.
2711
+
2712
+ NeoLLM-specific adaptations vs. the OLMo reference implementation:
2713
+ - FANLayer runs once per forward (shared between both paths).
2714
+ - Expert slicing uses contiguous segments (no SwiGLU half-shift)
2715
+ because gate and up are separate projections here.
2716
+ - PolyNorm (shared parameters) is used as the activation in both paths.
2717
+ - Multipliers from LinearWithMultipliers are applied per-slice in the
2718
+ width path to correctly replicate the full MLP forward.
2719
+ - Gumbel temperature stored as a persistent float32 buffer so it
2720
+ survives checkpoint save/load without external tracking.
2721
+ - Width path load-balancing returns (output, aux_stats) for integration
2722
+ with the existing NeoLLMForCausalLM aux-loss accumulation pattern.
2723
+
2724
+ Reference:
2725
+ Nie et al. (2026). "VersatileFFN: Achieving Parameter Efficiency in
2726
+ LLMs via Adaptive Wide-and-Deep Reuse." arXiv:2512.14531.
2727
+ """
2728
+
2729
+ def __init__(self, config: NeoLLMConfig):
2730
+ super().__init__()
2731
+
2732
+ self.total_experts = getattr(config, "versatile_total_experts", 8)
2733
+ self.active_experts = getattr(config, "versatile_active_experts", 2)
2734
+ self.max_depth = getattr(config, "versatile_max_depth", 4)
2735
+ self.hidden_size = config.hidden_size
2736
+ self.intermediate_size = config.intermediate_size
2737
+
2738
+ # ── Shared MLP weights (identical layout to NeoLLMMLP) ──────────────
2739
+ fan_ratio = getattr(config, "fan_ratio_ffn", 0.0625)
2740
+ fan_dim = config.hidden_size + int(config.hidden_size * fan_ratio)
2741
+
2742
+ self.fan_layer = FANLayer(hidden_size=config.hidden_size, fan_ratio=fan_ratio)
2743
+ self.gate_proj = LinearWithMultipliers(
2744
+ fan_dim, config.intermediate_size,
2745
+ bias=False, use_row_multiplier=True, use_column_multiplier=False,
2746
+ )
2747
+ self.up_proj = nn.Linear(fan_dim, config.intermediate_size, bias=False)
2748
+ self.down_proj = LinearWithMultipliers(
2749
+ config.intermediate_size, config.hidden_size,
2750
+ bias=False, use_row_multiplier=True, use_column_multiplier=True,
2751
+ )
2752
+ self.act_fn = PolyNorm(exclusive=False)
2753
+ self.dropout = nn.Dropout(config.dropout_rate)
2754
+
2755
+ # ── Inner normalization for depth-recursive steps ────────��───────────
2756
+ # Applied before each recursive MLP application (mirrors the paper's
2757
+ # ff_norm inside each depth loop).
2758
+ self.ff_norm = SeeDNorm(config.hidden_size, eps=config.rms_norm_eps)
2759
+
2760
+ # ── Width path: expert router + contiguous segment indices ──────────
2761
+ seg = config.intermediate_size // self.total_experts
2762
+ self.expert_segment = seg
2763
+ self.expert_gate = nn.Linear(config.hidden_size, self.total_experts, bias=False)
2764
+
2765
+ # Static expert slice indices [total_experts, seg] — non-persistent
2766
+ # buffer so .to(device) moves them automatically.
2767
+ idx_list = [
2768
+ torch.arange(i * seg, (i + 1) * seg)
2769
+ for i in range(self.total_experts)
2770
+ ]
2771
+ self.register_buffer("expert_idx", torch.stack(idx_list), persistent=False)
2772
+
2773
+ # ── Depth path: loop count predictor ────────────────────────────────
2774
+ self.depth_predictor = nn.Linear(config.hidden_size, self.max_depth, bias=False)
2775
+
2776
+ # Gumbel temperature as a persistent scalar buffer.
2777
+ # Decays externally via update_gumbel_temperature() each training step.
2778
+ temp_start = getattr(config, "versatile_gumbel_temp_start", 5.0)
2779
+ self.register_buffer(
2780
+ "gumbel_temp",
2781
+ torch.tensor(temp_start, dtype=torch.float32),
2782
+ persistent=True,
2783
+ )
2784
+ self._gumbel_temp_end = getattr(config, "versatile_gumbel_temp_end", 0.1)
2785
+ self._gumbel_temp_decay = getattr(config, "versatile_gumbel_temp_decay", 0.99984)
2786
+
2787
+ # ── Public API ────────────────────────────────────────────────────────────
2788
+
2789
+ def update_gumbel_temperature(self) -> None:
2790
+ """Decay Gumbel temperature by one step. Call once per training step."""
2791
+ new_t = max(
2792
+ self.gumbel_temp.item() * self._gumbel_temp_decay,
2793
+ self._gumbel_temp_end,
2794
+ )
2795
+ self.gumbel_temp.fill_(new_t)
2796
+
2797
+ # ── Private helpers ───────────────────────────────────────────────────────
2798
+
2799
+ def _expert_forward(
2800
+ self,
2801
+ x_fan: torch.Tensor, # [N, fan_dim]
2802
+ x_in: torch.Tensor, # [N, hidden_size] — residual base
2803
+ idx: torch.Tensor, # [seg] — which intermediate neurons to use
2804
+ ) -> torch.Tensor:
2805
+ """
2806
+ Virtual expert forward for a single expert (Eq. 6–8 of paper).
2807
+
2808
+ Slices gate_proj, up_proj, down_proj weights to the expert segment.
2809
+ Multipliers from LinearWithMultipliers are applied per-slice to keep
2810
+ the computation exactly equivalent to the full MLP forward.
2811
+
2812
+ Returns x_in + expert_out (residual included, matching Eq. 8).
2813
+ """
2814
+ # Gate projection: sliced rows + row multiplier
2815
+ g_w = self.gate_proj.linear.weight[idx] # [seg, fan_dim]
2816
+ g_mul = self.gate_proj.row_multiplier.multiplier[idx] # [seg]
2817
+ gate = F.linear(x_fan, g_w) * g_mul # [N, seg]
2818
+
2819
+ # Up projection: sliced rows, no multiplier on up_proj
2820
+ u_w = self.up_proj.weight[idx] # [seg, fan_dim]
2821
+ up = F.linear(x_fan, u_w) # [N, seg]
2822
+
2823
+ # Activation — PolyNorm is shape-agnostic (weight [3], bias [1])
2824
+ act = self.act_fn(gate) * up # [N, seg]
2825
+ act = self.dropout(act)
2826
+
2827
+ # Down projection: column multiplier on input, then row on output
2828
+ col_mul = self.down_proj.column_multiplier.multiplier[idx] # [seg]
2829
+ d_w = self.down_proj.linear.weight[:, idx] # [hidden, seg]
2830
+ row_mul = self.down_proj.row_multiplier.multiplier # [hidden]
2831
+ out = F.linear(act * col_mul, d_w) * row_mul # [N, hidden]
2832
+
2833
+ return x_in + out
2834
+
2835
+ def _full_forward_step(self, x: torch.Tensor) -> torch.Tensor:
2836
+ """
2837
+ One recursive MLP application for the depth path (Eq. 9).
2838
+
2839
+ Includes inner SeeDNorm + FANLayer so each iteration starts from a
2840
+ normalized, periodicity-augmented view of the current hidden state.
2841
+ Residual connection applied inside to match the paper's formulation.
2842
+ """
2843
+ og = x
2844
+ x = self.ff_norm(x)
2845
+ x_f = self.fan_layer(x)
2846
+ gate = self.gate_proj(x_f)
2847
+ up = self.up_proj(x_f)
2848
+ act = self.act_fn(gate) * up
2849
+ act = self.dropout(act)
2850
+ return og + self.down_proj(act)
2851
+
2852
+ # ── Forward ─────────────────────────────────────────────────────────────���─
2853
+
2854
+ def forward(
2855
+ self,
2856
+ x: torch.Tensor,
2857
+ analysis: Optional[MLPAnalysis] = None,
2858
+ ) -> Tuple[torch.Tensor, Optional[tuple]]:
2859
+ """
2860
+ Args:
2861
+ x: [B, S, hidden_size] — pre-normalized input
2862
+ (SeeDNorm + LNS already applied by NeoLLMDecoderLayer).
2863
+ analysis: MLPAnalysis with .versatile pre-allocated when analysis
2864
+ mode is active. None during training (zero overhead).
2865
+
2866
+ Returns:
2867
+ (output [B, S, hidden_size], aux_stats)
2868
+ aux_stats = (p_sum [N_experts], f_sum [N_experts], N_tokens)
2869
+ for load-balancing loss, or None in inference if width path skipped.
2870
+ """
2871
+ B, S, D = x.shape
2872
+
2873
+ # Depth predictor reads difficulty from the pre-normalized hidden state
2874
+ depth_logits = self.depth_predictor(x) # [B, S, max_depth]
2875
+
2876
+ # FANLayer runs once — output shared between width and depth paths
2877
+ x_fan = self.fan_layer(x) # [B, S, fan_dim]
2878
+
2879
+ # ═════════════════════ TRAINING ══════════════════════════════════════
2880
+ if self.training:
2881
+ # Gumbel-Softmax with hard=True (STE): discrete forward, continuous
2882
+ # backward. Annealed temperature controls sharpness of the selection.
2883
+ depth_probs = F.gumbel_softmax(
2884
+ depth_logits,
2885
+ tau=float(self.gumbel_temp),
2886
+ hard=True,
2887
+ dim=-1,
2888
+ ) # [B, S, max_depth]
2889
+
2890
+ # ── Depth path: always L_max iterations (static graph for compile) ─
2891
+ depth_outputs = []
2892
+ current_x = x
2893
+ for _ in range(self.max_depth):
2894
+ current_x = self._full_forward_step(current_x)
2895
+ depth_outputs.append(current_x)
2896
+
2897
+ # Soft weighted combination — gradient flows through depth_probs
2898
+ depth_stack = torch.stack(depth_outputs, dim=-1) # [B,S,D,L]
2899
+ x_depth = (depth_stack * depth_probs.unsqueeze(2)).sum(dim=-1) # [B,S,D]
2900
+
2901
+ # ── Width path: Top-K routing over virtual experts ────────────────
2902
+ routing_logits = self.expert_gate(x) # [B,S,N]
2903
+ topk_w, topk_i = torch.topk(routing_logits, k=self.active_experts, dim=-1)
2904
+ topk_w = torch.softmax(topk_w, dim=-1) # [B,S,k]
2905
+
2906
+ x_flat = x.reshape(-1, D) # [N,D]
2907
+ x_fan_flat = x_fan.reshape(-1, x_fan.shape[-1]) # [N,fan]
2908
+ topk_i_f = topk_i.reshape(-1, self.active_experts) # [N,k]
2909
+ topk_w_f = topk_w.reshape(-1, self.active_experts) # [N,k]
2910
+ N_tok = x_flat.shape[0]
2911
+
2912
+ x_moe_flat = torch.zeros_like(x_flat)
2913
+
2914
+ for eid in range(self.total_experts):
2915
+ mask = (topk_i_f == eid)
2916
+ tok_idx, k_idx = torch.where(mask)
2917
+ if tok_idx.numel() == 0:
2918
+ continue
2919
+ w_e = topk_w_f[tok_idx, k_idx].unsqueeze(-1)
2920
+ out_e = self._expert_forward(
2921
+ x_fan_flat[tok_idx], x_flat[tok_idx], self.expert_idx[eid]
2922
+ )
2923
+ x_moe_flat.index_add_(
2924
+ 0, tok_idx, (out_e * w_e).to(x_moe_flat.dtype)
2925
+ )
2926
+
2927
+ x_moe = x_moe_flat.reshape(B, S, D)
2928
+
2929
+ # Load-balancing aux stats (same pattern as JTok-M)
2930
+ r_probs_flat = torch.softmax(
2931
+ routing_logits.reshape(-1, self.total_experts), dim=-1
2932
+ ) # [N_tok, N_experts]
2933
+ p_sum = r_probs_flat.sum(dim=0) # [N_experts]
2934
+ f_counts = torch.zeros(
2935
+ self.total_experts, device=x.device, dtype=x.dtype
2936
+ )
2937
+ for eid in range(self.total_experts):
2938
+ f_counts[eid] = (topk_i_f == eid).float().sum()
2939
+ f_sum = f_counts / (N_tok * self.active_experts) # [N_experts]
2940
+ aux_stats = (p_sum, f_sum, N_tok)
2941
+
2942
+ # ── Difficulty-aware fusion (Eq. 12–13) ──────────────────────────
2943
+ loop_idx = torch.arange(
2944
+ 1, self.max_depth + 1, device=x.device, dtype=depth_probs.dtype
2945
+ )
2946
+ expected_L = (depth_probs * loop_idx).sum(dim=-1) # [B, S]
2947
+ moe_weight = (self.max_depth - expected_L) / self.max_depth # [B, S]
2948
+ output = (
2949
+ x_depth * (1.0 - moe_weight.unsqueeze(-1))
2950
+ + x_moe * moe_weight.unsqueeze(-1)
2951
+ )
2952
+ loop_choice = None # not used during training
2953
+
2954
+ # ═════════════════════ INFERENCE ══════════════════════════════════════
2955
+ else:
2956
+ loop_choice = depth_logits.argmax(dim=-1) # [B, S]
2957
+ max_loop = int(loop_choice.max().item())
2958
+
2959
+ # Depth path: early exit — only compute needed iterations
2960
+ depth_outputs = []
2961
+ current_x = x
2962
+ for _ in range(max_loop + 1):
2963
+ current_x = self._full_forward_step(current_x)
2964
+ depth_outputs.append(current_x)
2965
+
2966
+ depth_stack = torch.stack(depth_outputs, dim=-1) # [B,S,D,run]
2967
+ gather_idx = (
2968
+ loop_choice.unsqueeze(-1).unsqueeze(-1).expand(B, S, D, 1)
2969
+ )
2970
+ x_depth = depth_stack.gather(dim=-1, index=gather_idx).squeeze(-1)
2971
+
2972
+ # Fusion weight from discrete choice
2973
+ expected_L = (loop_choice + 1).float() # [B, S]
2974
+ moe_weight = (self.max_depth - expected_L) / self.max_depth # [B, S]
2975
+
2976
+ # Width path: conditional on λ > 0 (Conditional Parallelism)
2977
+ active_mask = (moe_weight > 1e-6) # [B, S]
2978
+ x_moe = torch.zeros_like(x)
2979
+ aux_stats = None
2980
+ depth_probs = None
2981
+
2982
+ if active_mask.any():
2983
+ x_flat_all = x.reshape(-1, D)
2984
+ x_fan_flat_all = x_fan.reshape(-1, x_fan.shape[-1])
2985
+ active_flat = active_mask.reshape(-1)
2986
+ x_active = x_flat_all[active_flat]
2987
+ x_fan_active = x_fan_flat_all[active_flat]
2988
+
2989
+ r_log = self.expert_gate(x_active) # [Na, N]
2990
+ tw, ti = torch.topk(r_log, k=self.active_experts, dim=-1)
2991
+ tw = torch.softmax(tw, dim=-1)
2992
+
2993
+ x_moe_active = torch.zeros_like(x_active)
2994
+ for eid in range(self.total_experts):
2995
+ mask_e = (ti == eid)
2996
+ tok_idx, k_idx = torch.where(mask_e)
2997
+ if tok_idx.numel() == 0:
2998
+ continue
2999
+ w_e = tw[tok_idx, k_idx].unsqueeze(-1)
3000
+ out_e = self._expert_forward(
3001
+ x_fan_active[tok_idx], x_active[tok_idx], self.expert_idx[eid]
3002
+ )
3003
+ x_moe_active.index_add_(
3004
+ 0, tok_idx, (out_e * w_e).to(x_moe_active.dtype)
3005
+ )
3006
+
3007
+ x_moe_flat = x_moe.reshape(-1, D)
3008
+ x_moe_flat[active_flat] = x_moe_active
3009
+ x_moe = x_moe_flat.reshape(B, S, D)
3010
+
3011
+ output = (
3012
+ x_depth * (1.0 - moe_weight.unsqueeze(-1))
3013
+ + x_moe * moe_weight.unsqueeze(-1)
3014
+ )
3015
+
3016
+ # ── Analysis deposits ─────────────────────────────────────────────────
3017
+ if analysis is not None:
3018
+ if analysis.versatile is not None:
3019
+ va = analysis.versatile
3020
+ va.depth_probs = depth_probs.detach() if depth_probs is not None else None
3021
+ va.expected_loops = expected_L.detach()
3022
+ va.moe_weight = moe_weight.detach()
3023
+ va.loop_choice = loop_choice.detach() if loop_choice is not None else None
3024
+ va.x_depth = x_depth.detach()
3025
+ va.x_width = x_moe.detach()
3026
+ analysis.output = output.detach()
3027
+
3028
+ return output, aux_stats
3029
+
3030
+
3031
  class NeoLLMMLP(nn.Module):
3032
  """MLP with FANformer integration and Learnable Multipliers."""
3033
 
 
3049
  config.intermediate_size, config.hidden_size,
3050
  bias=False, use_row_multiplier=True, use_column_multiplier=True,
3051
  )
3052
+ self.act_fn = PolyNorm(exclusive_init=0.00, exclusive=getattr(config, "polynorm_exclusive", True))
3053
  self.dropout = nn.Dropout(config.dropout_rate)
3054
 
3055
  def forward(
 
3105
  self.use_jtokm = config.use_jtokm
3106
 
3107
  self.self_attn = NeoLLMAttention(config, layer_idx)
3108
+ self.mlp = (
3109
+ VersatileFFN(config)
3110
+ if getattr(config, "use_versatile_ffn", False)
3111
+ else NeoLLMMLP(config)
3112
+ )
3113
+ self.use_versatile_ffn = getattr(config, "use_versatile_ffn", False)
3114
  self.input_layernorm = SeeDNorm(config.hidden_size, eps=config.rms_norm_eps)
3115
  self.post_attention_layernorm = SeeDNorm(config.hidden_size, eps=config.rms_norm_eps)
3116
  self.lns_attn = LNS(layer_idx)
 
3192
  attn_res_partial: Optional[torch.Tensor] = None,
3193
  layer_analysis: Optional[LayerAnalysis] = None,
3194
  output_attentions: Optional[bool] = False,
 
3195
  **kwargs: Unpack[FlashAttentionKwargs],
3196
  ) -> Tuple:
3197
  # ── Snapshot input ────────────────────────────────────────────────
 
3229
  position_embeddings=position_embeddings,
3230
  first_layer_fan=first_layer_fan,
3231
  attn_analysis=layer_analysis.attention if layer_analysis is not None else None,
 
3232
  **kwargs,
3233
  )
3234
 
 
3263
  layer_analysis.lns_mlp_output = h_lns2.detach()
3264
 
3265
  mlp_a = layer_analysis.mlp if layer_analysis is not None else None
3266
+ if self.use_versatile_ffn:
3267
+ delta_m, versatile_aux = self.mlp(h_lns2, analysis=mlp_a)
3268
+ else:
3269
+ delta_m = self.mlp(h_lns2, analysis=mlp_a)
3270
+ versatile_aux = None
3271
 
3272
  if layer_analysis is not None:
3273
  layer_analysis.mlp_contribution = delta_m.detach()
 
3298
  outputs += (attn_weights,)
3299
  if aux_stats is not None:
3300
  outputs += (aux_stats,)
3301
+ if versatile_aux is not None:
3302
+ outputs += (versatile_aux,)
3303
  return outputs
3304
 
3305
 
 
3754
 
3755
  self.post_init()
3756
 
3757
+ # ── REPO: inject inv_freq into every attention layer that uses it ─────
3758
+ # Done after post_init so rotary_emb.inv_freq is already initialized.
3759
+ # Layers below repo_start_layer never call set_repo_inv_freq (their
3760
+ # use_repo flag is False) so the call is harmless for those layers.
3761
+ if getattr(config, "use_repo", False):
3762
+ for layer in self.layers:
3763
+ layer.self_attn.set_repo_inv_freq(
3764
+ self.rotary_emb.inv_freq,
3765
+ self.rotary_emb.attention_scaling,
3766
+ )
3767
+
3768
  def get_input_embeddings(self):
3769
  if self.config.use_token_generator:
3770
  return self.token_generator
 
3790
  getattr(cfg, "use_repo", False)
3791
  and layer_idx >= getattr(cfg, "repo_start_layer", cfg.num_hidden_layers // 3)
3792
  )
3793
+ _versatile = getattr(cfg, "use_versatile_ffn", False)
3794
  return LayerAnalysis(
3795
  seednorm_pre_attn = SeeDNormAnalysis(),
3796
  seednorm_post_attn = SeeDNormAnalysis(),
 
3800
  repo = REPOAnalysis() if _repo_active else None,
3801
  ),
3802
  mlp = MLPAnalysis(
3803
+ fan = FANAnalysis() if not _versatile else None,
3804
+ polynorm = PolyNormAnalysis() if not _versatile else None,
3805
+ versatile = VersatileFFNAnalysis() if _versatile else None,
3806
  ),
3807
  gpas_attn = GPASAnalysis(),
3808
  gpas_mlp = GPASAnalysis(),
 
3898
  position_embeddings = self.rotary_emb(hidden_states, position_ids)
3899
  self.first_layer_fan = None
3900
 
 
 
 
 
 
 
 
 
 
 
 
3901
  # ── Attention Residuals state ──────────────────────────────────────
3902
  # Full AttnRes (attn_res_num_blocks=0): sources grows by one entry per
3903
  # decoder layer — all previous outputs are kept, max N=num_layers+1.
 
3958
  attn_res_partial=attn_res_partial if use_attn_res else None,
3959
  layer_analysis=layer_analysis,
3960
  output_attentions=output_attentions,
 
3961
  **kwargs,
3962
  )
3963
  hidden_states = layer_outputs[0]
 
3973
  if self.config.use_jtokm and len(layer_outputs) > (2 if output_attentions else 1):
3974
  all_aux_stats.append(layer_outputs[-1])
3975
 
3976
+ # Collect VersatileFFN aux stats (second-to-last if jtokm also present,
3977
+ # or last if jtokm is absent). Only non-None during training.
3978
+ if getattr(self.config, "use_versatile_ffn", False):
3979
+ for item in layer_outputs[1:]:
3980
+ if isinstance(item, tuple) and len(item) == 3:
3981
+ # (p_sum, f_sum, N_tokens) signature
3982
+ all_aux_stats.append(("versatile", item))
3983
+ break
3984
+
3985
  if (self.first_layer_fan is None
3986
  and hasattr(decoder_layer, "current_layer_fan")):
3987
  self.first_layer_fan = decoder_layer.current_layer_fan
 
4226
  )
4227
  # Add JTok-M load-balancing auxiliary loss
4228
  if self.config.use_jtokm and all_aux_stats:
4229
+ jtokm_stats = [
4230
+ s for s in all_aux_stats
4231
+ if not (isinstance(s, tuple) and len(s) == 2 and s[0] == "versatile")
4232
+ ]
4233
+ if jtokm_stats:
4234
+ aux_loss = compute_jtokm_aux_loss(
4235
+ jtokm_stats,
4236
+ n_e=self.config.jtokm_num_experts,
4237
+ weight=self.config.jtokm_aux_loss_weight,
4238
+ )
4239
+ loss = loss + aux_loss
4240
+ # Add VersatileFFN load-balancing auxiliary loss
4241
+ if getattr(self.config, "use_versatile_ffn", False) and all_aux_stats:
4242
+ versatile_stats = [
4243
+ s[1] for s in all_aux_stats
4244
+ if isinstance(s, tuple) and len(s) == 2 and s[0] == "versatile"
4245
+ ]
4246
+ if versatile_stats:
4247
+ v_loss = compute_versatile_aux_loss(
4248
+ versatile_stats,
4249
+ n_experts=self.config.versatile_total_experts,
4250
+ weight=self.config.versatile_aux_loss_weight,
4251
+ )
4252
+ loss = loss + v_loss
4253
  logits = None
4254
  else:
4255
  slice_indices = (
 
4290
  "MEAHeadSeeDNorm",
4291
  "HadamardOProj",
4292
  "REPOModule",
4293
+ "VersatileFFN",
4294
+ "compute_versatile_aux_loss",
4295
  # Analysis dataclasses — exported so external tools can type-hint against them
4296
  "AnalysisState",
4297
  "LayerAnalysis",
 
4303
  "PolyNormAnalysis",
4304
  "HadamardAnalysis",
4305
  "REPOAnalysis",
4306
+ "VersatileFFNAnalysis",
4307
  "JTokMAnalysis",
4308
  "AttnResAnalysis",
4309
  "GeneratorAnalysis",