ShourenWSR commited on
Commit
d892ced
·
verified ·
1 Parent(s): f0c979f

SYNKA 25687: verified modeling_loop_lm.py

Browse files
Files changed (1) hide show
  1. modeling_loop_lm.py +849 -0
modeling_loop_lm.py ADDED
@@ -0,0 +1,849 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Looped-MoE modeling code (port of the seedvar `loop-lm` architecture).
2
+
3
+ Provenance
4
+ ----------
5
+ Ported from ``modeling_loop_lm.py`` as published with
6
+ ``ml-ryanlee/seedvar-looped-moe-1e18-d704-seed42..47`` (arXiv 2605.09165,
7
+ *Sparse Layers are Critical to Scaling Looped Language Models*). That file is the
8
+ architecture specification: the published checkpoints are its training product,
9
+ and the diagnostics pipeline is already validated against it.
10
+
11
+ This is a **port, not a copy**. The numerics are kept faithful (see "Faithful to
12
+ the original" below) because the baseline's whole job is to reproduce the
13
+ published architecture; the deviations are all in service of three requirements
14
+ from HANDOVER §4.3 that the original file does not meet:
15
+
16
+ 1. **Semantic parameters have no defaults.** The original ``LoopLMConfig``
17
+ defaults ``d_model=1024``, ``num_experts=8`` and so on. Defaults are how a
18
+ silently-wrong run happens: a typo'd key name falls back to a plausible
19
+ number and the run looks fine. Every shape/semantic field here is required
20
+ and a missing one raises.
21
+ 2. **Every loop step is hookable, by explicit index.** The loop counter is
22
+ threaded down to each block, so a consumer never recovers the loop axis by
23
+ reshaping a flattened layer axis (which silently yields transposed
24
+ semantics).
25
+ 3. **Router logits are exposed with their semantics labelled.** Both the
26
+ pre-softmax logits and the post-softmax probabilities are handed out, tagged
27
+ via ``src.model.loop_trace.ROUTER_LOGITS_ARE_PRESOFTMAX``.
28
+
29
+ Scope
30
+ -----
31
+ Only the ``looped-moe`` variant is implemented. The original file carries four
32
+ variants (base / looped / moe / looped-moe); all four architectures this project
33
+ trains -- baseline and DVF-a/b/c -- are looped-moe, differing only in the (L, R, E)
34
+ triple. Porting the unused three would be dead code (project rule: no entities
35
+ beyond necessity). They remain available in the original file if ever needed.
36
+
37
+ Shape parameters, and what the experiment varies
38
+ ------------------------------------------------
39
+ ==================== ====== =========================================
40
+ config field symbol meaning
41
+ ==================== ====== =========================================
42
+ num_layers_in_stack L physical layers in the shared stack
43
+ num_stacks R times the stack is called (loop count)
44
+ num_experts E experts per MoE layer
45
+ num_active k experts activated per token
46
+ ==================== ====== =========================================
47
+
48
+ The DVF ("dual vector foil") series holds L*R = 16 and E*L = 64 fixed and only
49
+ moves capacity around: baseline (8,2,8), DVF-a (4,4,16), DVF-b (2,8,32),
50
+ DVF-c (1,16,64).
51
+
52
+ Faithful to the original (do not "fix" these -- they are muP, not bugs)
53
+ ----------------------------------------------------------------------
54
+ * ``RMSNorm`` has **no** gain parameter.
55
+ * Attention is scaled by ``1/d_k``, not ``1/sqrt(d_k)``.
56
+ * Softmax upcasts to float32.
57
+ * Expert FFN width is ``d_ff // num_active`` -- divided by k, *not* by E. This is
58
+ what makes E*L=64 hold total expert parameters constant across the DVF series.
59
+ * Initialisation is muP: ``std = std_base / sqrt(width_ratio)`` with
60
+ ``std_base = sqrt(2/(fan_in_base + fan_out_base))`` against a d_base=128 proxy.
61
+
62
+ Deliberate deviation
63
+ --------------------
64
+ ``RotaryPositionalEmbedding`` builds its rotation table with vectorised torch ops
65
+ instead of the original's ``max_seq_len * d_k/2`` nested Python loop, which costs
66
+ minutes at seq_len 4096. ``tests/test_rope_equivalence.py`` pins the vectorised
67
+ table against a literal transcription of the original loop.
68
+ """
69
+
70
+ from __future__ import annotations
71
+
72
+ import math
73
+ from typing import Any, Optional
74
+
75
+ import torch
76
+ import torch.nn as nn
77
+ import torch.nn.functional as F
78
+ from einops import einsum, rearrange, reduce, repeat
79
+ from torch import Tensor
80
+ from torch.nn.functional import grouped_mm, silu
81
+ from transformers import PretrainedConfig, PreTrainedModel
82
+ from transformers.generation import GenerationMixin
83
+ from transformers.modeling_outputs import CausalLMOutputWithPast
84
+
85
+ # This file is loaded in three different ways, and the trace types have to
86
+ # resolve in all of them:
87
+ # 1. as part of this repo -> `src.model.loop_trace`
88
+ # 2. via HuggingFace trust_remote_code -> copied into a generated package under
89
+ # `transformers_modules/<ckpt>/`, where the sibling is a *relative* import
90
+ # 3. as a loose script with the checkpoint directory on sys.path
91
+ # Case 2 is the one that matters for HANDOVER §4.7: the diagnostics pipeline is a
92
+ # separate repository that has never heard of `pretrain`, and `save_trajectory`
93
+ # bundles `loop_trace.py` next to this file so the checkpoint stands alone.
94
+ try:
95
+ from src.model.loop_trace import ACTIVE_TRACE_SINK, TraceMeta, TracePoint, TraceSink
96
+ except ImportError: # pragma: no cover - covered by the cold-load test
97
+ try:
98
+ from .loop_trace import ACTIVE_TRACE_SINK, TraceMeta, TracePoint, TraceSink # type: ignore[no-redef]
99
+ except ImportError:
100
+ from loop_trace import ACTIVE_TRACE_SINK, TraceMeta, TracePoint, TraceSink # type: ignore[no-redef]
101
+
102
+ __all__ = ["LoopMoEConfig", "LoopMoEForCausalLM", "LoopedMoETransformer"]
103
+
104
+ # muP proxy-model widths. The initialisation std of every weight is derived from
105
+ # a d_base=128 model and rescaled by width_ratio = d_model / 128.
106
+ HEAD_TAIL_NUM_EXPERTS = 8
107
+ """Experts in a head/tail layer, fixed across every ablation configuration.
108
+
109
+ Recipe 2026-09-12 section 2.1: head and tail are "identical in all configurations"
110
+ (1 layer, 8 experts, top-2). It is deliberately independent of the loop block's E --
111
+ S4 gives the loop 64 experts per layer and its head still has 8 -- so the loop block's
112
+ count must not be reused here.
113
+ """
114
+
115
+ BASE_D_MODEL = 128
116
+ BASE_D_FF = 384
117
+
118
+
119
+ def softmax(logits: Tensor, dim: int) -> Tensor:
120
+ """Max-shifted softmax in float32 (verbatim semantics from the original)."""
121
+ logits = logits.float()
122
+ max_values = torch.max(logits, dim=dim, keepdim=True).values
123
+ shifted = logits - max_values
124
+ shifted_exps = torch.exp(shifted)
125
+ shifted_exp_sums = torch.sum(shifted_exps, dim=dim, keepdim=True)
126
+ return shifted_exps / shifted_exp_sums
127
+
128
+
129
+ class Linear(nn.Module):
130
+ """Bias-free linear layer with muP initialisation."""
131
+
132
+ def __init__(self, in_features, out_features, width_ratio, std_base, device=None, dtype=None):
133
+ super().__init__()
134
+ # Registered before init so the shape exists under HF meta-device loading.
135
+ self.weight = nn.Parameter(torch.empty(out_features, in_features, dtype=dtype, device=device))
136
+ # Kept so the init can be replayed: torchtitan builds the model on the
137
+ # meta device and then calls `init_weights()` on materialised (but
138
+ # uninitialised) storage, so constructor-time init alone leaves the
139
+ # model full of garbage.
140
+ self._init_std = std_base / math.sqrt(width_ratio)
141
+ self.reset_parameters()
142
+
143
+ def reset_parameters(self) -> None:
144
+ std = self._init_std
145
+ nn.init.trunc_normal_(self.weight, mean=0.0, std=std, a=-3 * std, b=3 * std)
146
+
147
+ def forward(self, x: Tensor) -> Tensor:
148
+ return einsum(self.weight, x, "d_out d_in, ... d_in -> ... d_out")
149
+
150
+
151
+ class Embedding(nn.Module):
152
+ def __init__(self, num_embeddings, embedding_dim, device=None, dtype=None):
153
+ super().__init__()
154
+ self.weight = nn.Parameter(torch.empty(num_embeddings, embedding_dim, dtype=dtype, device=device))
155
+ self.reset_parameters()
156
+
157
+ def reset_parameters(self) -> None:
158
+ nn.init.trunc_normal_(self.weight, mean=0.0, std=1.0, a=-3, b=3)
159
+
160
+ def forward(self, token_ids: Tensor) -> Tensor:
161
+ return self.weight[token_ids]
162
+
163
+
164
+ class RMSNorm(nn.Module):
165
+ """RMS norm **without** a gain parameter (muP convention)."""
166
+
167
+ def __init__(self, d_model: int, eps: float = 1e-5, device=None, dtype=None):
168
+ super().__init__()
169
+ self.d_model = d_model
170
+ self.eps = eps
171
+
172
+ def forward(self, x: Tensor) -> Tensor:
173
+ in_dtype = x.dtype
174
+ x = x.to(torch.float32)
175
+ mean_squared_sum = (1 / self.d_model) * einsum(x, x, "... seq d, ... seq d -> ... seq")
176
+ rms = torch.sqrt(mean_squared_sum + self.eps)
177
+ rms_norm = einsum(x, 1 / rms, "... seq d, ... seq -> ... seq d")
178
+ return rms_norm.to(in_dtype)
179
+
180
+
181
+ class PositionwiseFeedforward(nn.Module):
182
+ """SwiGLU: W2(SiLU(W1 x) * W3 x)."""
183
+
184
+ def __init__(self, d_model: int, d_ff: int, width_ratio: float, device=None, dtype=None):
185
+ super().__init__()
186
+ w_std_base = math.sqrt(2 / (BASE_D_MODEL + BASE_D_FF))
187
+ self.w1 = Linear(d_model, d_ff, width_ratio, w_std_base, device=device, dtype=dtype)
188
+ self.w2 = Linear(d_ff, d_model, width_ratio, w_std_base, device=device, dtype=dtype)
189
+ self.w3 = Linear(d_model, d_ff, width_ratio, w_std_base, device=device, dtype=dtype)
190
+
191
+ def forward(self, x: Tensor) -> Tensor:
192
+ return self.w2(silu(self.w1(x)) * self.w3(x))
193
+
194
+
195
+ class RotaryPositionalEmbedding(nn.Module):
196
+ """RoPE with a precomputed [seq, d_k/2, 2, 2] rotation table.
197
+
198
+ Vectorised rebuild of the original's nested Python loop; see the module
199
+ docstring and ``tests/test_rope_equivalence.py``.
200
+ """
201
+
202
+ def __init__(self, theta: float, d_k: int, max_seq_len: int, device=None, dtype=None):
203
+ super().__init__()
204
+ # Retained so the table can be rebuilt: `to_empty()` replaces buffer
205
+ # storage with uninitialised memory just as it does for parameters, so a
206
+ # meta-device build leaves the rotation table as garbage unless
207
+ # `reset_parameters()` regenerates it.
208
+ self._rope_theta, self._rope_d_k = theta, d_k
209
+ self._rope_max_seq_len, self._rope_dtype = max_seq_len, dtype
210
+ rotations = self._build_table(theta, d_k, max_seq_len, device, dtype)
211
+ self.register_buffer("rotations", rotations, persistent=True)
212
+
213
+ @staticmethod
214
+ def _build_table(theta: float, d_k: int, max_seq_len: int, device, dtype) -> Tensor:
215
+ """[seq, d_k/2, 2, 2] rotation table.
216
+
217
+ Angles are built in float64 and only then cast down. At seq_len 4096 the
218
+ largest angle is ~4096 rad, where float32 spacing is ~2.4e-4; computing
219
+ cos/sin at float32 there loses ~4 decimal digits. The original does this
220
+ implicitly (Python floats are float64), so float64 here is both more
221
+ accurate and what keeps the table equal to the reference.
222
+ """
223
+ positions = torch.arange(max_seq_len, device=device, dtype=torch.float64)
224
+ pair_idx = torch.arange(d_k // 2, device=device, dtype=torch.float64)
225
+ inv_freq = theta ** (2 * pair_idx / d_k)
226
+ angles = positions[:, None] / inv_freq[None, :]
227
+ cos, sin = torch.cos(angles), torch.sin(angles)
228
+ # rows of the 2x2 rotation: [[cos, -sin], [sin, cos]]
229
+ table = torch.stack(
230
+ [torch.stack([cos, -sin], dim=-1), torch.stack([sin, cos], dim=-1)], dim=-2
231
+ )
232
+ return table.to(dtype if dtype is not None else torch.float32)
233
+
234
+ @torch.no_grad()
235
+ def reset_parameters(self) -> None:
236
+ """Regenerate the rotation table in place (buffers survive nothing)."""
237
+ self.rotations.copy_(
238
+ self._build_table(
239
+ self._rope_theta, self._rope_d_k, self._rope_max_seq_len,
240
+ self.rotations.device, self.rotations.dtype,
241
+ )
242
+ )
243
+
244
+ def forward(self, x: Tensor, token_positions: Tensor) -> Tensor:
245
+ rot = self.rotations[token_positions].to(dtype=x.dtype)
246
+ x_pairs = rearrange(x, "... seq_dim (feature_dim i) -> ... seq_dim feature_dim i", i=2)
247
+ y_pairs = einsum(
248
+ rot,
249
+ x_pairs,
250
+ "... seq_dim feature_dim i j, ... seq_dim feature_dim j -> ... seq_dim feature_dim i",
251
+ )
252
+ return rearrange(y_pairs, "... seq_dim feature_dim i -> ... seq_dim (feature_dim i)")
253
+
254
+
255
+ class MultiheadSelfAttention(nn.Module):
256
+ """Causal MHSA with RoPE. muP: attention logits scaled by 1/d_k."""
257
+
258
+ def __init__(self, d_model: int, num_heads: int, max_seq_len: int, theta: float,
259
+ width_ratio: float, device=None, dtype=None):
260
+ super().__init__()
261
+ if d_model % num_heads != 0:
262
+ raise ValueError(f"d_model ({d_model}) must be divisible by num_heads ({num_heads})")
263
+ self.d_model = d_model
264
+ self.num_heads = num_heads
265
+
266
+ attn_std_base = math.sqrt(2 / (BASE_D_MODEL + BASE_D_MODEL))
267
+ self.q_proj = Linear(d_model, d_model, width_ratio, attn_std_base, device=device, dtype=dtype)
268
+ self.k_proj = Linear(d_model, d_model, width_ratio, attn_std_base, device=device, dtype=dtype)
269
+ self.v_proj = Linear(d_model, d_model, width_ratio, attn_std_base, device=device, dtype=dtype)
270
+ self.output_proj = Linear(d_model, d_model, width_ratio, attn_std_base, device=device, dtype=dtype)
271
+ self.rope = RotaryPositionalEmbedding(theta, d_model // num_heads, max_seq_len, device, dtype)
272
+
273
+ def forward(self, x: Tensor, token_positions: Optional[Tensor] = None) -> Tensor:
274
+ d_k = self.d_model // self.num_heads
275
+ q_heads = rearrange(self.q_proj(x), "... seq (heads d_k) -> ... heads seq d_k", d_k=d_k)
276
+ k_heads = rearrange(self.k_proj(x), "... seq (heads d_k) -> ... heads seq d_k", d_k=d_k)
277
+ v_heads = rearrange(self.v_proj(x), "... seq (heads d_v) -> ... heads seq d_v", d_v=d_k)
278
+
279
+ if token_positions is None:
280
+ token_positions = rearrange(torch.arange(x.shape[-2], device=x.device), "seq -> 1 seq")
281
+ q_heads = self.rope(q_heads, token_positions)
282
+ k_heads = self.rope(k_heads, token_positions)
283
+
284
+ mha_heads = F.scaled_dot_product_attention(
285
+ q_heads, k_heads, v_heads, is_causal=True, scale=1.0 / d_k
286
+ )
287
+ return self.output_proj(rearrange(mha_heads, "... heads seq d_v -> ... seq (heads d_v)"))
288
+
289
+
290
+ class Router(nn.Module):
291
+ """Top-k softmax router. Returns pre-softmax logits *and* probabilities.
292
+
293
+ The two are returned side by side, and the caller labels which is which via
294
+ ``ROUTER_LOGITS_ARE_PRESOFTMAX``. There is no jitter noise and no temperature;
295
+ routing is deterministic given the input (matching the original).
296
+ """
297
+
298
+ def __init__(self, d_model: int, num_experts: int, num_active: int, width_ratio: float,
299
+ device=None, dtype=None):
300
+ super().__init__()
301
+ std_base = math.sqrt(2 / (BASE_D_MODEL + num_experts))
302
+ self.gate = Linear(d_model, num_experts, width_ratio, std_base, device=device, dtype=dtype)
303
+ self.num_active = num_active
304
+
305
+ def forward(self, x: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor]:
306
+ logits = self.gate(x) # [B, S, E] -- pre-softmax
307
+ probs = softmax(logits, dim=-1) # [B, S, E] -- over all E experts
308
+ top_scores, top_experts = torch.topk(probs, k=self.num_active, dim=-1)
309
+ # Renormalise within the selected set so the combine weights sum to 1.
310
+ top_scores = top_scores / torch.sum(top_scores, dim=-1, keepdim=True)
311
+ return logits, probs, top_scores, top_experts
312
+
313
+
314
+ class GroupedMoEPrenormBlock(nn.Module):
315
+ """Pre-norm block whose FFN is a grouped top-k MoE.
316
+
317
+ Layout: x -> +attn(ln1(x)) -> +moe(ln2(.)). Aux losses are returned rather
318
+ than stashed on the module, so nothing has to be reset between loop steps.
319
+ """
320
+
321
+ @staticmethod
322
+ def _init_expert_weights(num_experts, in_features, out_features, width_ratio, std_base,
323
+ device, dtype) -> nn.Parameter:
324
+ w = torch.empty(num_experts, in_features, out_features, device=device, dtype=dtype)
325
+ std_scaled = std_base / math.sqrt(width_ratio)
326
+ nn.init.trunc_normal_(w, mean=0.0, std=std_scaled, a=-3 * std_scaled, b=3 * std_scaled)
327
+ return nn.Parameter(w)
328
+
329
+ @torch.no_grad()
330
+ def reset_parameters(self) -> None:
331
+ """Re-init the grouped expert weights (see Linear.reset_parameters)."""
332
+ std = self._expert_init_std
333
+ for w in (self.experts_w1, self.experts_w2, self.experts_w3):
334
+ nn.init.trunc_normal_(w, mean=0.0, std=std, a=-3 * std, b=3 * std)
335
+
336
+ def __init__(self, d_model: int, num_heads: int, d_ff: int, num_experts: int, num_active: int,
337
+ max_seq_len: int, theta: float, width_ratio: float, device=None, dtype=None):
338
+ super().__init__()
339
+ self.ln1 = RMSNorm(d_model, device=device, dtype=dtype)
340
+ self.attn = MultiheadSelfAttention(d_model, num_heads, max_seq_len, theta, width_ratio, device, dtype)
341
+ self.ln2 = RMSNorm(d_model, device=device, dtype=dtype)
342
+ self.router = Router(d_model, num_experts, num_active, width_ratio, device=device, dtype=dtype)
343
+
344
+ self.num_experts = num_experts
345
+ self.num_active = num_active
346
+
347
+ # NOTE: divided by num_active (k), not by num_experts (E). This is what
348
+ # keeps total expert parameters constant across the DVF series.
349
+ d_ff_expert = d_ff // num_active
350
+ w_std_base = math.sqrt(2 / (BASE_D_MODEL + BASE_D_FF))
351
+ self._expert_init_std = w_std_base / math.sqrt(width_ratio)
352
+ self.experts_w1 = self._init_expert_weights(num_experts, d_model, d_ff_expert, width_ratio, w_std_base, device, dtype)
353
+ self.experts_w2 = self._init_expert_weights(num_experts, d_ff_expert, d_model, width_ratio, w_std_base, device, dtype)
354
+ self.experts_w3 = self._init_expert_weights(num_experts, d_model, d_ff_expert, width_ratio, w_std_base, device, dtype)
355
+
356
+ def forward(
357
+ self,
358
+ x: Tensor,
359
+ token_positions: Optional[Tensor] = None,
360
+ *,
361
+ loop_step: Optional[int] = None,
362
+ layer_idx: Optional[int] = None,
363
+ block: Optional[str] = None,
364
+ unrolled_pos: Optional[int] = None,
365
+ trace_sink: Optional[TraceSink] = None,
366
+ ) -> tuple[Tensor, Tensor, Tensor]:
367
+ batch, seq, dim = x.shape
368
+ total_tokens = batch * seq
369
+
370
+ norm1_out = self.ln1(x)
371
+ attn_out = self.attn(norm1_out, token_positions)
372
+ assert x.shape == attn_out.shape
373
+ resid1_out = attn_out + x
374
+
375
+ norm2_out = self.ln2(resid1_out)
376
+ logits, probs, top_scores, top_experts = self.router(norm2_out)
377
+
378
+ # `softmax` computes in float32 and does not cast back, so `top_scores`
379
+ # is float32 regardless of the activation dtype. The combine weights get
380
+ # multiplied into bf16 expert outputs below, so they must match, or
381
+ # einsum raises "expected m1 and m2 to have the same dtype".
382
+ #
383
+ # This is invisible in the original: seedvar's published checkpoints are
384
+ # float32, where the cast is a no-op. Under the bf16 training this
385
+ # project uses (LT2 runs pure bf16, HANDOVER §4.1) it is a hard failure
386
+ # on the first forward.
387
+ #
388
+ # Only the combine weights are cast. `probs` and `logits` stay float32
389
+ # for the aux-loss and z-loss reductions, which is where the extra
390
+ # precision is worth having.
391
+ top_scores = top_scores.to(x.dtype)
392
+
393
+ # Flatten and sort by expert so grouped_mm can run one matmul per expert.
394
+ x_flat = rearrange(norm2_out, "b s d -> (b s) d")
395
+ flat_expert_ids = rearrange(top_experts, "b s k -> (b s k)")
396
+ flat_scores = rearrange(top_scores, "b s k -> (b s k)")
397
+ flat_positions = torch.arange(total_tokens, device=x.device)
398
+ flat_token_ids = repeat(flat_positions, "n -> (n k)", k=self.num_active)
399
+
400
+ sort_indices = flat_expert_ids.argsort(stable=True)
401
+ sorted_expert_ids = flat_expert_ids[sort_indices]
402
+ sorted_token_ids = flat_token_ids[sort_indices]
403
+ sorted_scores = flat_scores[sort_indices]
404
+ sorted_x = x_flat[sorted_token_ids]
405
+
406
+ counts = torch.bincount(sorted_expert_ids, minlength=self.num_experts)
407
+ offs = counts.cumsum(0).to(torch.int32)
408
+
409
+ h1 = grouped_mm(sorted_x, self.experts_w1, offs=offs)
410
+ h3 = grouped_mm(sorted_x, self.experts_w3, offs=offs)
411
+ gated = silu(h1) * h3
412
+ expert_out = grouped_mm(gated, self.experts_w2, offs=offs)
413
+
414
+ expert_out = einsum(expert_out, sorted_scores, "n d, n -> n d")
415
+ output_flat = torch.zeros(total_tokens, dim, device=x.device, dtype=expert_out.dtype)
416
+ output_flat.index_add_(0, sorted_token_ids, expert_out)
417
+ experts_out = rearrange(output_flat, "(b s) d -> b s d", b=batch, s=seq)
418
+
419
+ # Aux losses, per HANDOVER §3.3':
420
+ # L_LB = E * sum_i f_i * p_i (switch-style load balancing)
421
+ # L_RZ = mean( (logsumexp logits)^2 ) (router z-loss)
422
+ # Both are computed per layer per loop step; the caller averages over the
423
+ # unrolled depth (num_stacks * num_layers_in_stack).
424
+ fi = counts.float() / (total_tokens * self.num_active)
425
+ pi = reduce(probs, "b s e -> e", "mean")
426
+ lb = self.num_experts * einsum(fi, pi, "e, e ->")
427
+
428
+ logsumexp = torch.logsumexp(logits.float(), dim=-1)
429
+ lz = reduce(logsumexp**2, "... -> ", "mean")
430
+
431
+ assert experts_out.shape == resid1_out.shape
432
+ final_out = resid1_out + experts_out
433
+
434
+ # Write into every sink that is armed. Under FSDP2 the `trace_sink`
435
+ # keyword arrives as a per-block COPY (see `_ActiveTraceSink`), so the
436
+ # module-level one is the only sink the trainer can actually read back;
437
+ # the keyword remains for callers that pass their own dict directly
438
+ # (the toy launcher, the offline probes), where it is the same object.
439
+ # Writing to both is harmless: each is first-write-wins.
440
+ sinks = [s for s in (ACTIVE_TRACE_SINK.sink, trace_sink) if s is not None]
441
+ if sinks:
442
+ if loop_step is None or layer_idx is None or block is None or unrolled_pos is None:
443
+ raise ValueError(
444
+ "trace_sink was provided but loop_step/layer_idx/block/unrolled_pos "
445
+ "were not. Both axes must be explicit counters, never inferred."
446
+ )
447
+ if block not in ("head", "loop", "tail"):
448
+ raise ValueError(f"block must be head/loop/tail, got {block!r}")
449
+ # Keyed by the depth coordinate: head, the loop's first layer and tail all
450
+ # carry loop_step=layer_idx=0, so the old pair-key silently collapsed them.
451
+ key = unrolled_pos
452
+ # First write wins. Under selective activation checkpointing the
453
+ # block's forward runs a second time during backward to recompute
454
+ # activations, so every key is legitimately visited twice -- that is
455
+ # how AC works, not a bug. The recomputed values are identical by
456
+ # construction, so keeping the first and ignoring the rest is both
457
+ # correct and cheap.
458
+ #
459
+ # An earlier version raised on the second visit. That guard was aimed
460
+ # at double-*counting*, which first-write-wins prevents directly; as
461
+ # written it instead killed every AC-enabled run at the first traced
462
+ # step. `test_ac_recomputation_does_not_disturb_the_trace` pins the
463
+ # property that actually matters: same keys, same values, with AC on.
464
+ # Detached views, not copies -- see the lifetime contract in
465
+ # src/model/loop_trace.py.
466
+ point = TracePoint(
467
+ block=block,
468
+ unrolled_pos=unrolled_pos,
469
+ # From the tensors themselves, never from the model config: this layer's
470
+ # E and k are what produced these numbers, and head/tail differ from the
471
+ # loop block.
472
+ num_experts=int(logits.shape[-1]),
473
+ top_k=int(top_experts.shape[-1]),
474
+ loop_step=loop_step,
475
+ layer_idx=layer_idx,
476
+ router_logits=logits.detach(),
477
+ router_probs=probs.detach(),
478
+ topk_idx=top_experts.detach(),
479
+ topk_weights=top_scores.detach(),
480
+ residual=final_out.detach(),
481
+ )
482
+ for sink in sinks:
483
+ sink.setdefault(key, point)
484
+
485
+ return final_out, lb, lz
486
+
487
+
488
+ class LoopedStack(nn.Module):
489
+ """The stack of L MoE blocks that gets called R times."""
490
+
491
+ def __init__(self, context_length: int, d_model: int, num_layers_in_stack: int, num_heads: int,
492
+ d_ff: int, rope_theta: float, width_ratio: float, num_experts: int,
493
+ num_active: int, device=None, dtype=None):
494
+ super().__init__()
495
+ self.layers = nn.ModuleList(
496
+ [
497
+ GroupedMoEPrenormBlock(
498
+ d_model, num_heads, d_ff, num_experts, num_active,
499
+ context_length, rope_theta, width_ratio, device, dtype,
500
+ )
501
+ for _ in range(num_layers_in_stack)
502
+ ]
503
+ )
504
+
505
+ def forward(
506
+ self,
507
+ x: Tensor,
508
+ *,
509
+ loop_step: int,
510
+ unrolled_pos_start: int,
511
+ trace_sink: Optional[TraceSink] = None,
512
+ ) -> tuple[Tensor, Tensor, Tensor]:
513
+ """`unrolled_pos_start` is the depth coordinate this call's first layer occupies.
514
+
515
+ Passed in rather than recomputed from `loop_step`, so the caller owns the depth
516
+ axis in one place: the stack does not need to know how many layers ran before it.
517
+ """
518
+ lb_total = x.new_zeros(())
519
+ lz_total = x.new_zeros(())
520
+ for layer_idx, layer in enumerate(self.layers):
521
+ x, lb, lz = layer(
522
+ x, loop_step=loop_step, layer_idx=layer_idx, block="loop",
523
+ unrolled_pos=unrolled_pos_start + layer_idx, trace_sink=trace_sink,
524
+ )
525
+ lb_total = lb_total + lb
526
+ lz_total = lz_total + lz
527
+ return x, lb_total, lz_total
528
+
529
+
530
+ class LoopedMoETransformer(nn.Module):
531
+ """Looped MoE transformer: one shared stack applied ``num_stacks`` times.
532
+
533
+ The loop is an explicit Python ``for``; ``loop_step`` is the loop variable and
534
+ is threaded all the way down to each block. Nothing downstream ever has to
535
+ recover it from tensor shapes.
536
+ """
537
+
538
+ def __init__(self, vocab_size: int, context_length: int, d_model: int, num_layers_in_stack: int,
539
+ num_stacks: int, num_heads: int, d_ff: int, num_experts: int, num_active: int,
540
+ rope_theta: float, width_ratio: float, num_head_layers: int = 0,
541
+ num_tail_layers: int = 0, head_tail_num_experts: int = HEAD_TAIL_NUM_EXPERTS,
542
+ device=None, dtype=None):
543
+ super().__init__()
544
+ self.num_stacks = num_stacks
545
+ self.num_layers_in_stack = num_layers_in_stack
546
+ self.total_layers = num_stacks * num_layers_in_stack
547
+ self.num_head_layers = num_head_layers
548
+ self.num_tail_layers = num_tail_layers
549
+ # The unrolled depth, and the denominator the aux losses are averaged over.
550
+ # Written as head + loop + tail rather than the recipe's shorthand "2 + D*L":
551
+ # S15 has two head and two tail layers, so a literal 2 would divide by the wrong
552
+ # number there -- and it would not fail, it would just make the auxiliary losses
553
+ # quietly larger than intended.
554
+ self.unrolled_depth = num_head_layers + self.total_layers + num_tail_layers
555
+
556
+ self.token_embeddings = Embedding(vocab_size, d_model, device=device, dtype=dtype)
557
+ # Head and tail are ordinary MoE layers that run once. They keep E=8/top-2
558
+ # regardless of the loop block's expert count (recipe section 2.1: "identical in
559
+ # every configuration"), so the loop block's E is deliberately not passed here.
560
+ make_outer = lambda: GroupedMoEPrenormBlock(
561
+ d_model, num_heads, d_ff, head_tail_num_experts, num_active,
562
+ context_length, rope_theta, width_ratio, device, dtype,
563
+ )
564
+ self.head_layers = nn.ModuleList([make_outer() for _ in range(num_head_layers)])
565
+ self.tail_layers = nn.ModuleList([make_outer() for _ in range(num_tail_layers)])
566
+ self.stack = LoopedStack(
567
+ context_length, d_model, num_layers_in_stack, num_heads, d_ff, rope_theta,
568
+ width_ratio, num_experts, num_active, device=device, dtype=dtype,
569
+ )
570
+ self.ln_final = RMSNorm(d_model, device=device, dtype=dtype)
571
+ std_base_lm_head = math.sqrt(2 / (BASE_D_MODEL + vocab_size))
572
+ self.lm_head = Linear(d_model, vocab_size, width_ratio, std_base_lm_head, device=device, dtype=dtype)
573
+
574
+ @classmethod
575
+ def from_config(cls, config: "LoopMoEConfig", *, device=None, dtype=None) -> "LoopedMoETransformer":
576
+ """THE way to build this model from a config. Both call sites use it.
577
+
578
+ There were two: the HF wrapper and `pretrain/train_spec.py`, each with its own
579
+ hand-written keyword list. When head/tail layers were added, the training path's
580
+ list was not updated, so it silently built a model with no head or tail while its
581
+ config said otherwise -- it trained, the loss looked plausible, and every artifact
582
+ recorded the config's depth rather than the depth that ran. Nothing could raise,
583
+ because a shorter model is a perfectly valid model.
584
+
585
+ A single entry point makes that class of drift impossible rather than merely
586
+ tested-for: a field added to the config is read here once, and both paths get it.
587
+ """
588
+ return cls(
589
+ vocab_size=config.vocab_size,
590
+ context_length=config.context_length,
591
+ d_model=config.d_model,
592
+ num_layers_in_stack=config.num_layers_in_stack,
593
+ num_stacks=config.num_stacks,
594
+ num_heads=config.num_heads,
595
+ d_ff=config.d_ff,
596
+ num_experts=config.num_experts,
597
+ num_active=config.num_active,
598
+ rope_theta=config.rope_theta,
599
+ width_ratio=config.width_ratio,
600
+ num_head_layers=config.num_head_layers,
601
+ num_tail_layers=config.num_tail_layers,
602
+ device=device,
603
+ dtype=dtype,
604
+ )
605
+
606
+ def forward(
607
+ self,
608
+ x: Tensor,
609
+ *,
610
+ trace_sink: Optional[TraceSink] = None,
611
+ ) -> tuple[Tensor, Tensor, Tensor]:
612
+ lb_total = None
613
+ lz_total = None
614
+
615
+ x = self.token_embeddings(x)
616
+
617
+ def run_outer(layers, block: str, pos: int, x, lb_total, lz_total):
618
+ for i, layer in enumerate(layers):
619
+ x, lb, lz = layer(
620
+ x, loop_step=0, layer_idx=0, block=block, unrolled_pos=pos + i,
621
+ trace_sink=trace_sink,
622
+ )
623
+ lb_total = lb if lb_total is None else lb_total + lb
624
+ lz_total = lz if lz_total is None else lz_total + lz
625
+ return x, lb_total, lz_total
626
+
627
+ # head -> loop x num_stacks -> tail, with one running depth coordinate. head and
628
+ # tail record loop_step=layer_idx=0 because neither coordinate means anything
629
+ # outside the loop; `block` and `unrolled_pos` are what identifies them.
630
+ x, lb_total, lz_total = run_outer(self.head_layers, "head", 0, x, lb_total, lz_total)
631
+ pos = self.num_head_layers
632
+ for loop_step in range(self.num_stacks):
633
+ x, lb, lz = self.stack(
634
+ x, loop_step=loop_step, unrolled_pos_start=pos, trace_sink=trace_sink,
635
+ )
636
+ pos += self.num_layers_in_stack
637
+ lb_total = lb if lb_total is None else lb_total + lb
638
+ lz_total = lz if lz_total is None else lz_total + lz
639
+ x, lb_total, lz_total = run_outer(self.tail_layers, "tail", pos, x, lb_total, lz_total)
640
+
641
+ x = self.lm_head(self.ln_final(x))
642
+
643
+ # Averaged over the *unrolled* depth: every physical layer contributes once per
644
+ # loop step, and head/tail contribute once each (recipe section 2.1). Equals
645
+ # `total_layers` exactly when there are no head/tail layers, which is every
646
+ # pre-ablation configuration -- so their published losses are unchanged.
647
+ return x, lb_total / self.unrolled_depth, lz_total / self.unrolled_depth
648
+
649
+
650
+ def _require(kwargs: dict[str, Any], name: str) -> Any:
651
+ """Fetch a required config field or raise.
652
+
653
+ Project rule (HANDOVER §4.3 / §5.6): semantic parameters get no defaults.
654
+ A default is a silent-wrong-answer generator -- a mistyped or dropped key
655
+ becomes a plausible number instead of an error.
656
+ """
657
+ if name not in kwargs or kwargs[name] is None:
658
+ raise ValueError(
659
+ f"LoopMoEConfig: required field {name!r} is missing. Semantic "
660
+ "parameters have no defaults in this project; state it explicitly."
661
+ )
662
+ return kwargs.pop(name)
663
+
664
+
665
+ class LoopMoEConfig(PretrainedConfig):
666
+ """Config for the looped-MoE architecture. **Every field is required.**
667
+
668
+ Compatible with ``save_pretrained``/``from_pretrained``: a config.json written
669
+ by this class round-trips, and one is rejected loudly if a field is absent.
670
+ """
671
+
672
+ model_type = "loop-moe"
673
+
674
+ # Tells transformers not to introspect defaults by constructing `cls()` with
675
+ # no arguments -- which this class deliberately rejects. Without it,
676
+ # `save_pretrained` fails inside `_get_generation_parameters`. This is the
677
+ # supported escape hatch for configs whose fields are all required.
678
+ has_no_defaults_at_init = True
679
+
680
+ def __init__(self, **kwargs: Any):
681
+ # `from_pretrained` on a *torch-saved* config, and some HF-internal paths,
682
+ # construct with no arguments at all; only a fully-specified call is valid.
683
+ self.vocab_size = _require(kwargs, "vocab_size")
684
+ self.context_length = _require(kwargs, "context_length")
685
+ self.d_model = _require(kwargs, "d_model")
686
+ self.num_heads = _require(kwargs, "num_heads")
687
+ self.d_ff = _require(kwargs, "d_ff")
688
+ self.rope_theta = _require(kwargs, "rope_theta")
689
+ self.width_ratio = _require(kwargs, "width_ratio")
690
+ self.num_layers_in_stack = _require(kwargs, "num_layers_in_stack") # L
691
+ self.num_stacks = _require(kwargs, "num_stacks") # R
692
+ self.num_experts = _require(kwargs, "num_experts") # E
693
+ self.num_active = _require(kwargs, "num_active") # k
694
+ self.lb_loss_factor = _require(kwargs, "lb_loss_factor")
695
+ # Head/tail layers: MoE layers run once, outside the loop (ablation recipe 2026-09-12
696
+ # section 2.1). 0 means the architecture has none, which is not a guess -- it is what
697
+ # every configuration built before this recipe actually is, and
698
+ # `test_config_registry` pins their parameter counts as unchanged. Real ablation
699
+ # configs never rely on the fallback: `config_registry.build_ablation` states both
700
+ # counts for every entry, and a test asserts it does.
701
+ self.num_head_layers = int(kwargs.pop("num_head_layers", 0))
702
+ self.num_tail_layers = int(kwargs.pop("num_tail_layers", 0))
703
+ self.lz_loss_factor = _require(kwargs, "lz_loss_factor")
704
+
705
+ # Stated, not inherited. `PretrainedConfig` defaults this to True, and the
706
+ # only reason the embedding and the LM head are not already sharing storage
707
+ # is that this model never implemented `get_output_embeddings()`. The day
708
+ # someone adds it for tool compatibility, every configuration would start
709
+ # tying weights -- a different model, trained to a different loss, with
710
+ # nothing in any artifact saying so. The architecture uses untied weights
711
+ # (the parameter counts in the recipe assume it), so the config says so.
712
+ kwargs.pop("tie_word_embeddings", None)
713
+ self.tie_word_embeddings = False
714
+
715
+ self._validate()
716
+
717
+ # Derived, for readers; never an input. The unrolled depth now includes the
718
+ # layers that run once outside the loop.
719
+ self.num_layers = self.num_stacks * self.num_layers_in_stack
720
+ self.unrolled_depth = self.num_head_layers + self.num_layers + self.num_tail_layers
721
+
722
+ # The original config mirrored `context_length` into `max_length` for
723
+ # lm-evaluation-harness. transformers >=5 classifies `max_length` as a
724
+ # generation parameter and refuses to serialise a config carrying one
725
+ # (the check is `hasattr`, so even a property trips it). `context_length`
726
+ # is therefore the single source of truth for sequence length; pass
727
+ # `max_length` to the harness explicitly at eval time instead.
728
+ # Popped so that loading a seedvar-era config.json cannot reintroduce it.
729
+ kwargs.pop("max_length", None)
730
+
731
+ super().__init__(**kwargs)
732
+
733
+ def _validate(self) -> None:
734
+ """Reject out-of-domain values loudly rather than failing deep in a kernel."""
735
+ positive = (
736
+ "vocab_size", "context_length", "d_model", "num_heads", "d_ff",
737
+ "num_layers_in_stack", "num_stacks", "num_experts", "num_active",
738
+ )
739
+ for name in positive:
740
+ value = getattr(self, name)
741
+ if not isinstance(value, int) or value < 1:
742
+ raise ValueError(f"LoopMoEConfig.{name} must be a positive int, got {value!r}")
743
+ if self.d_model % self.num_heads != 0:
744
+ raise ValueError(
745
+ f"d_model ({self.d_model}) must be divisible by num_heads ({self.num_heads})"
746
+ )
747
+ if self.num_active > self.num_experts:
748
+ raise ValueError(
749
+ f"num_active ({self.num_active}) cannot exceed num_experts ({self.num_experts})"
750
+ )
751
+ if self.d_ff % self.num_active != 0:
752
+ raise ValueError(
753
+ f"d_ff ({self.d_ff}) must be divisible by num_active ({self.num_active}); "
754
+ "expert width is d_ff // num_active and truncation would silently "
755
+ "change the parameter count."
756
+ )
757
+ for name in ("num_head_layers", "num_tail_layers"):
758
+ value = getattr(self, name)
759
+ if not isinstance(value, int) or value < 0:
760
+ raise ValueError(f"LoopMoEConfig.{name} must be a non-negative int, got {value!r}")
761
+ if (self.d_model // self.num_heads) % 2 != 0:
762
+ raise ValueError(
763
+ f"head dim ({self.d_model // self.num_heads}) must be even for RoPE"
764
+ )
765
+
766
+ @property
767
+ def trace_meta(self) -> TraceMeta:
768
+ """Shape/provenance block handed to the metrics collector."""
769
+ return TraceMeta(
770
+ num_stacks=self.num_stacks,
771
+ num_layers_in_stack=self.num_layers_in_stack,
772
+ num_experts=self.num_experts,
773
+ num_active=self.num_active,
774
+ )
775
+
776
+
777
+ class LoopMoEForCausalLM(PreTrainedModel, GenerationMixin):
778
+ """HF-compatible causal LM wrapper.
779
+
780
+ Kept HF-shaped on purpose: HANDOVER §4.7 makes "the diagnostics pipeline
781
+ ingests our checkpoints unchanged" an acceptance criterion, and that pipeline
782
+ loads models through ``from_pretrained``.
783
+ """
784
+
785
+ config_class = LoopMoEConfig
786
+
787
+ def __init__(self, config: LoopMoEConfig):
788
+ super().__init__(config)
789
+ self.model = LoopedMoETransformer.from_config(config)
790
+ self.post_init()
791
+
792
+ def get_input_embeddings(self):
793
+ return self.model.token_embeddings
794
+
795
+ def set_input_embeddings(self, value):
796
+ self.model.token_embeddings = value
797
+
798
+ def forward(
799
+ self,
800
+ input_ids: torch.LongTensor,
801
+ attention_mask: Optional[Tensor] = None, # unused: the mask is built in
802
+ labels: Optional[torch.LongTensor] = None,
803
+ trace_sink: Optional[TraceSink] = None,
804
+ **kwargs: Any,
805
+ ) -> CausalLMOutputWithPast:
806
+ """Forward pass.
807
+
808
+ Returns a ``CausalLMOutputWithPast`` whose ``loss`` is the *total* loss
809
+ (CE + weighted aux). The unweighted components are attached as
810
+ ``task_loss`` / ``lb_loss`` / ``z_loss`` so the training loop can log the
811
+ breakdown without recomputing anything.
812
+
813
+ **Label contract (documented here 2026-08-21, `ABCI_ERR_20260821_0405_
814
+ gate1_label_shift_root_cause.md`)**: ``labels`` must already be
815
+ next-token-shifted by the caller -- ``labels[..., t] == input_ids[..., t+1]``,
816
+ with the last position set to ``-100`` (no target exists after it). This
817
+ method does **not** shift internally; it passes ``labels`` to
818
+ ``F.cross_entropy`` exactly as given. Before this date the only written
819
+ record of this contract was a comment in
820
+ ``src/model/loop_trace.py`` ("the dataloader supplies pre-shifted
821
+ labels during training, so the shift is explicit here") -- not here, at
822
+ the definition itself. That gap let four separate call sites
823
+ (the pretraining dataloader path aside, which was correct) independently
824
+ get this wrong the same way, rather than it being four unrelated
825
+ mistakes. Any caller not shifting first -- e.g. ``model(input_ids=ids,
826
+ labels=ids)`` -- silently trains/evaluates on the trivial
827
+ copy-the-current-token target instead of next-token prediction.
828
+ """
829
+ logits, lb, lz = self.model(input_ids, trace_sink=trace_sink)
830
+
831
+ loss = task_loss = None
832
+ if labels is not None:
833
+ task_loss = F.cross_entropy(logits.view(-1, logits.size(-1)), labels.view(-1))
834
+ loss = (
835
+ task_loss
836
+ + self.config.lb_loss_factor * lb
837
+ + self.config.lz_loss_factor * lz
838
+ )
839
+
840
+ out = CausalLMOutputWithPast(loss=loss, logits=logits)
841
+ # Unweighted components; the trainer pairs them with the factors from
842
+ # config to build LossComponents.
843
+ out.task_loss = task_loss
844
+ out.lb_loss = lb
845
+ out.z_loss = lz
846
+ return out
847
+
848
+ def prepare_inputs_for_generation(self, input_ids, **kwargs):
849
+ return {"input_ids": input_ids}