Update modeling_neollm.py
Browse files- 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
|
| 1915 |
-
#
|
| 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 |
-
|
| 2368 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 2508 |
-
self.bias
|
| 2509 |
-
self.eps
|
| 2510 |
-
self.
|
| 2511 |
-
|
| 2512 |
-
|
| 2513 |
-
|
| 2514 |
-
|
| 2515 |
-
|
| 2516 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 2557 |
-
|
|
|
|
| 2558 |
|
| 2559 |
-
|
| 2560 |
-
|
| 2561 |
-
|
| 2562 |
-
|
| 2563 |
-
|
| 2564 |
|
| 2565 |
-
|
| 2566 |
-
|
| 2567 |
-
|
| 2568 |
|
| 2569 |
-
|
| 2570 |
-
|
| 2571 |
-
|
| 2572 |
|
| 2573 |
-
|
| 2574 |
-
|
| 2575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3770 |
-
all_aux_stats
|
| 3771 |
-
|
| 3772 |
-
|
| 3773 |
-
|
| 3774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|