anima-python 0.13.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
anima_py/core/model.py ADDED
@@ -0,0 +1,462 @@
1
+ """core/model.py — UNIFIED anima torch training model (CONV + BYTE mouths).
2
+
3
+ Holds BOTH trunks in one CORE-owned file (owner directive: core-related lives in
4
+ core/), mirroring core/decode.py (unified CONV+BYTE decoder) and core/serialize.py
5
+ (unified serializer):
6
+ * CONV mouth — CLMConvMoE (dilated conv trunk + MoE, `.clm` format) + the H_9200 E1
7
+ SLW gated-write forward-slot (core/slw.py, engaged by cfg.slw).
8
+ * BYTE mouth — ByteGPT (24-layer GPT-2-class byte transformer, `.bin` format), below.
9
+ Both emit the exact state_dict keys core/serialize.py reads and match core/decode.py's
10
+ forward math (2-production byte-parity). cli/train.py imports both from here.
11
+
12
+ CLM conv-MoE model skeleton (toy scale).
13
+
14
+ Implements the CLM P0 architecture (CLM/P0_ARCHITECTURE.md):
15
+
16
+ byte text (V=256)
17
+ -> dilated conv embed
18
+ -> dilated conv trunk (no attention)
19
+ -> MoE conv layer (router picks among N small conv experts = mitosis cells)
20
+ -> byte readout (next-byte prediction)
21
+
22
+ Three router variants are exposed via a flag (see RouterConfig.variant):
23
+
24
+ "A" : entropy-regularized routing (content axis)
25
+ "B" : top-k routing + load-balance aux (routing axis)
26
+ "AB" : both A and B combined (dual-axis, untried in prior art)
27
+
28
+ This is a CPU/Mac toy-scale skeleton. It is intentionally small
29
+ (d=64, 2 layers, 4 experts) so the load-balance probe runs at $0 on a laptop.
30
+ Per the design's Q4, toy-scale results are INTUITION ONLY (non-gate); the real
31
+ F-CLM-MONO judgment is the full-scale multi-rung fire (H_847 §3).
32
+
33
+ No attention is used anywhere: every operator is conv / linear, which keeps the
34
+ inference path inside the AKIDA primitive envelope (P0 §2, F-CLM-AKIDA-MAP).
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import math
40
+ from dataclasses import dataclass, field
41
+ from typing import Optional
42
+
43
+ import torch
44
+ import torch.nn as nn
45
+ import torch.nn.functional as F
46
+ from torch.utils.checkpoint import checkpoint as _grad_checkpoint
47
+
48
+
49
+ # --------------------------------------------------------------------------- #
50
+ # Config
51
+ # --------------------------------------------------------------------------- #
52
+ @dataclass
53
+ class CLMConfig:
54
+ """Tiny CLM config. Defaults are the P0 `tiny` rung (d64/L2/E4)."""
55
+
56
+ vocab_size: int = 256 # byte vocabulary (V=256), P0 Q3 monopoly lever
57
+ d_model: int = 64 # channel width
58
+ n_trunk_layers: int = 2 # dilated conv trunk depth
59
+ n_experts: int = 4 # MoE conv experts (= mitosis cells)
60
+ kernel_size: int = 3 # conv kernel (causal)
61
+ expert_kernel_size: int = 3 # per-expert conv kernel
62
+ dilation_base: int = 2 # trunk dilation grows as base**layer
63
+ max_dilation: int = 512 # CAP on trunk dilation (= dilation = min(base**i, max_dilation)).
64
+ # A dilation >= seq_len pads the sequence with ~base**i zeros and
65
+ # sees no real taps, so an uncapped base**i at deep L (e.g. L=30 ->
66
+ # 2**29) explodes the causal-pad buffer (OOM) for ZERO modeling gain.
67
+ # Capping at 512 keeps the receptive field >= a 512-token window and
68
+ # is byte-eq-neutral for the L=1 golden (min(2**0,512)=1, unchanged).
69
+ top_k: int = 1 # experts selected per position (variant B/AB)
70
+ grad_checkpoint: bool = False # RUNTIME-only activation recompute (torch.utils.checkpoint).
71
+ # When True, each trunk layer is recomputed in backward instead of
72
+ # retained, cutting peak activation memory ~n_trunk_layers-fold so a
73
+ # 7B d6208/L30/E30 rung fits one 80GB H100 (the reserved
74
+ # --grad-checkpoint flag, now WIRED). Touches NO serialized weight
75
+ # bytes -> byte-eq PRESERVED (like the dilation cap): a checkpointed
76
+ # forward computes the IDENTICAL output; only the autograd tape's
77
+ # activation retention changes. Default False keeps golden/3B unchanged.
78
+
79
+ # router-variant knobs (see RouterConfig)
80
+ variant: str = "AB" # "A" | "B" | "AB"
81
+ entropy_coef: float = 0.01 # arm A: entropy-regularization weight
82
+ load_balance_coef: float = 0.01 # arm B: load-balance aux-loss weight
83
+
84
+ dropout: float = 0.0
85
+
86
+ # H_9200 E1 — gated-write forward-slot (SLW). OFF by default => byte-identical
87
+ # to the additive-readout CLMConvMoE (no SLW submodule allocated, no trailer
88
+ # emitted). When slw=True, a content-addressed slot memory is written/read
89
+ # causally on the post-norm penultimate before readout, replacing the
90
+ # order-BLIND additive cbind with an asymmetric write(address)/read(key) that
91
+ # attacks the D>RF independence wall. See state/9200_e1_303m/E1_SLW_module_spec.md.
92
+ slw: bool = False # allocate + engage the SLW forward-slot module
93
+ slw_n_slot: int = 8 # number of addressable slots
94
+ slw_k: int = 64 # role/read key dim (address space); d_s = d_model
95
+
96
+ def router_config(self) -> "RouterConfig":
97
+ v = self.variant.upper()
98
+ if v not in ("A", "B", "AB"):
99
+ raise ValueError(f"variant must be A|B|AB, got {self.variant!r}")
100
+ return RouterConfig(
101
+ variant=v,
102
+ n_experts=self.n_experts,
103
+ top_k=self.top_k,
104
+ entropy_coef=self.entropy_coef if v in ("A", "AB") else 0.0,
105
+ load_balance_coef=self.load_balance_coef if v in ("B", "AB") else 0.0,
106
+ hard_top_k=v in ("B", "AB"),
107
+ )
108
+
109
+
110
+ @dataclass
111
+ class RouterConfig:
112
+ variant: str
113
+ n_experts: int
114
+ top_k: int
115
+ entropy_coef: float
116
+ load_balance_coef: float
117
+ hard_top_k: bool
118
+
119
+
120
+ # --------------------------------------------------------------------------- #
121
+ # Causal dilated conv block (trunk)
122
+ # --------------------------------------------------------------------------- #
123
+ class CausalDilatedConv1d(nn.Module):
124
+ """1D causal conv with left-padding so position t never sees t+1.
125
+
126
+ Causality matters for a next-byte LM: the readout at t must depend only on
127
+ bytes <= t. We left-pad by (kernel_size - 1) * dilation and drop the right
128
+ overhang.
129
+ """
130
+
131
+ def __init__(self, channels: int, kernel_size: int, dilation: int):
132
+ super().__init__()
133
+ self.kernel_size = kernel_size
134
+ self.dilation = dilation
135
+ self.pad = (kernel_size - 1) * dilation
136
+ self.conv = nn.Conv1d(channels, channels, kernel_size, dilation=dilation)
137
+
138
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
139
+ # x: (B, C, T)
140
+ x = F.pad(x, (self.pad, 0))
141
+ return self.conv(x)
142
+
143
+
144
+ class TrunkLayer(nn.Module):
145
+ """Residual gated dilated-conv trunk layer (no attention)."""
146
+
147
+ def __init__(self, cfg: CLMConfig, dilation: int):
148
+ super().__init__()
149
+ self.conv = CausalDilatedConv1d(cfg.d_model, cfg.kernel_size, dilation)
150
+ self.norm = nn.GroupNorm(1, cfg.d_model) # layernorm over channels
151
+ self.act = nn.GELU()
152
+ self.drop = nn.Dropout(cfg.dropout)
153
+
154
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
155
+ h = self.conv(x)
156
+ h = self.norm(h)
157
+ h = self.act(h)
158
+ h = self.drop(h)
159
+ return x + h
160
+
161
+
162
+ # --------------------------------------------------------------------------- #
163
+ # MoE conv layer
164
+ # --------------------------------------------------------------------------- #
165
+ class ConvExpert(nn.Module):
166
+ """A small causal conv expert = one mitosis cell (P0 Q2)."""
167
+
168
+ def __init__(self, cfg: CLMConfig):
169
+ super().__init__()
170
+ self.conv = CausalDilatedConv1d(cfg.d_model, cfg.expert_kernel_size, dilation=1)
171
+ self.act = nn.GELU()
172
+
173
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
174
+ return self.act(self.conv(x))
175
+
176
+
177
+ @dataclass
178
+ class MoEStats:
179
+ """Per-forward routing diagnostics (filled by MoEConvLayer.forward)."""
180
+
181
+ # mean router probability mass assigned to each expert over the batch*time
182
+ usage: torch.Tensor # (n_experts,)
183
+ aux_loss: torch.Tensor # scalar (load-balance + entropy terms)
184
+ entropy: torch.Tensor # scalar mean per-token routing entropy
185
+
186
+
187
+ class MoEConvLayer(nn.Module):
188
+ """Mixture of conv experts with a per-position softmax router.
189
+
190
+ Router operates per (batch, time) position. Variants:
191
+
192
+ A (entropy-reg) : soft mix of all experts; entropy bonus encourages
193
+ the router to keep its per-token distribution from
194
+ collapsing onto one expert (subtracted from loss).
195
+ B (top-k + lb) : hard top-k gating; a load-balance aux loss
196
+ (Switch-Transformer style) penalizes uneven usage.
197
+ AB (both) : top-k routing + load-balance + entropy bonus.
198
+ """
199
+
200
+ def __init__(self, cfg: CLMConfig):
201
+ super().__init__()
202
+ self.rc = cfg.router_config()
203
+ self.experts = nn.ModuleList(ConvExpert(cfg) for _ in range(cfg.n_experts))
204
+ # router: per-position linear over channels -> expert logits
205
+ self.router = nn.Conv1d(cfg.d_model, cfg.n_experts, kernel_size=1)
206
+
207
+ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, MoEStats]:
208
+ # x: (B, C, T)
209
+ B, C, T = x.shape
210
+ n_e = self.rc.n_experts
211
+
212
+ logits = self.router(x) # (B, n_e, T)
213
+ probs = F.softmax(logits, dim=1) # per-position dist
214
+
215
+ # per-token routing entropy (nats), averaged
216
+ ent_tok = -(probs * torch.log(probs + 1e-9)).sum(dim=1) # (B, T)
217
+ entropy = ent_tok.mean()
218
+
219
+ # stack expert outputs: (B, n_e, C, T)
220
+ ex_out = torch.stack([e(x) for e in self.experts], dim=1)
221
+
222
+ if self.rc.hard_top_k:
223
+ # hard top-k gate (variant B / AB)
224
+ k = min(self.rc.top_k, n_e)
225
+ topv, topi = probs.topk(k, dim=1) # (B, k, T)
226
+ # renormalize the kept gate weights
227
+ gate = topv / (topv.sum(dim=1, keepdim=True) + 1e-9) # (B, k, T)
228
+ mask = torch.zeros_like(probs).scatter_(1, topi, gate) # (B, n_e, T)
229
+ else:
230
+ # soft mixture over all experts (variant A)
231
+ mask = probs # (B, n_e, T)
232
+
233
+ # weighted combine: (B, n_e, 1, T) * (B, n_e, C, T) -> sum over experts
234
+ y = (mask.unsqueeze(2) * ex_out).sum(dim=1) # (B, C, T)
235
+
236
+ # ----- usage stats + aux losses ----------------------------------- #
237
+ # "usage" = fraction of routing mass each expert receives (soft probs,
238
+ # the importance signal). Used by the load-balance loss and by the
239
+ # offline balance probe.
240
+ usage = probs.mean(dim=(0, 2)) # (n_e,)
241
+
242
+ aux = x.new_zeros(())
243
+ if self.rc.load_balance_coef > 0.0:
244
+ # Switch-Transformer load-balance: n_e * sum_i (f_i * P_i)
245
+ # f_i = fraction of tokens dispatched to expert i (top-1 routing
246
+ # fraction), P_i = mean router prob for expert i.
247
+ # VECTORIZED: a Python `[(top1==i).mean() for i in range(n_e)]` loop
248
+ # launches n_e separate reductions + GPU syncs per forward (the
249
+ # per-step bottleneck at n_e=30). one_hot+mean is a single fused op,
250
+ # mathematically identical (per-class fraction == one-hot mean).
251
+ top1 = probs.argmax(dim=1) # (B, T)
252
+ f_i = F.one_hot(top1, n_e).to(probs.dtype).mean(dim=(0, 1)) # (n_e,)
253
+ p_i = usage
254
+ lb = n_e * (f_i * p_i).sum()
255
+ aux = aux + self.rc.load_balance_coef * lb
256
+ if self.rc.entropy_coef > 0.0:
257
+ # entropy bonus: maximize routing entropy -> subtract from loss
258
+ aux = aux - self.rc.entropy_coef * entropy
259
+
260
+ return y, MoEStats(usage=usage, aux_loss=aux, entropy=entropy)
261
+
262
+
263
+ # --------------------------------------------------------------------------- #
264
+ # Full model
265
+ # --------------------------------------------------------------------------- #
266
+ class CLMConvMoE(nn.Module):
267
+ """Conv-native byte LM with a single MoE conv layer (toy skeleton)."""
268
+
269
+ def __init__(self, cfg: CLMConfig):
270
+ super().__init__()
271
+ self.cfg = cfg
272
+ self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
273
+ # dilated conv embed (P0 §0: "dilated conv embed")
274
+ self.embed_conv = CausalDilatedConv1d(cfg.d_model, cfg.kernel_size, dilation=1)
275
+
276
+ # CAP dilation at cfg.max_dilation (WaveNet/TCN-style saturation). An
277
+ # uncapped base**i at deep L explodes the causal pad (L=30 -> 2**29 ~5e8
278
+ # left-pad) for no modeling benefit beyond a seq-length receptive field.
279
+ # min(2**0, cap)=1 so the L=1 golden is byte-eq-unchanged.
280
+ dils = [min(cfg.dilation_base ** i, cfg.max_dilation) for i in range(cfg.n_trunk_layers)]
281
+ self.trunk = nn.ModuleList(TrunkLayer(cfg, d) for d in dils)
282
+ self.moe = MoEConvLayer(cfg)
283
+ self.norm_out = nn.GroupNorm(1, cfg.d_model)
284
+ self.readout = nn.Conv1d(cfg.d_model, cfg.vocab_size, kernel_size=1)
285
+ # H_9200 E1 — optional gated-write forward-slot. None => byte-identical
286
+ # additive-readout CLMConvMoE (existing golden path untouched). The SLW
287
+ # module is CORE-owned (core/slw.py, owner directive: core lives in core/);
288
+ # imported lazily so this model file carries no SLW dependency when off.
289
+ self.slw = None
290
+ if getattr(cfg, "slw", False):
291
+ from slw import SLWModule # core/slw.py (on sys.path via cli/train.py)
292
+ self.slw = SLWModule(cfg.d_model, cfg.slw_n_slot, cfg.slw_k)
293
+
294
+ def forward(
295
+ self, tokens: torch.Tensor, targets: Optional[torch.Tensor] = None
296
+ ) -> dict:
297
+ # tokens: (B, T) long
298
+ x = self.embed(tokens) # (B, T, C)
299
+ x = x.transpose(1, 2) # (B, C, T)
300
+ x = self.embed_conv(x)
301
+ # Gradient checkpointing (runtime-only, byte-eq-neutral): when enabled and
302
+ # training, recompute each trunk layer's activations in backward instead of
303
+ # retaining them. At L30/d6208 the retained-activation chain is what OOMs an
304
+ # 80GB H100 (28GB fp32 weights + 8bit-optim state leave too little for 30
305
+ # layers of (B,6208,512) activations); checkpointing trades ~1 extra forward
306
+ # for ~L-fold less activation memory. use_reentrant=False = the modern
307
+ # non-reentrant autograd path (handles no-input-grad embeds cleanly).
308
+ ckpt = getattr(self.cfg, "grad_checkpoint", False) and self.training and x.requires_grad
309
+ for layer in self.trunk:
310
+ if ckpt:
311
+ x = _grad_checkpoint(layer, x, use_reentrant=False)
312
+ else:
313
+ x = layer(x)
314
+ x, stats = self.moe(x)
315
+ x = self.norm_out(x)
316
+ # H_9200 E1 — gated-write forward-slot on the post-norm penultimate
317
+ # (before readout). None => additive golden path (byte-identical).
318
+ if self.slw is not None:
319
+ x = self.slw(x)
320
+ logits = self.readout(x) # (B, V, T)
321
+
322
+ out = {
323
+ "logits": logits,
324
+ "usage": stats.usage,
325
+ "aux_loss": stats.aux_loss,
326
+ "routing_entropy": stats.entropy,
327
+ }
328
+ if targets is not None:
329
+ ce = F.cross_entropy(
330
+ logits.transpose(1, 2).reshape(-1, self.cfg.vocab_size),
331
+ targets.reshape(-1),
332
+ )
333
+ out["ce_loss"] = ce
334
+ out["loss"] = ce + stats.aux_loss
335
+ return out
336
+
337
+ @torch.no_grad()
338
+ def num_params(self) -> int:
339
+ return sum(p.numel() for p in self.parameters())
340
+
341
+
342
+ def build_model(variant: str = "AB", **overrides) -> CLMConvMoE:
343
+ """Convenience factory. `variant` in {A, B, AB}."""
344
+ cfg = CLMConfig(variant=variant, **overrides)
345
+ return CLMConvMoE(cfg)
346
+
347
+
348
+ # ═══════════════════════════════════════════════════════════════════════════
349
+ # BYTE mouth — ByteGPT trunk (`anima-python train --arch bytegpt`, `.bin` format).
350
+ # The SYMMETRIC sibling of CLMConvMoE (the CONV mouth) folded into this UNIFIED
351
+ # core/model.py, mirroring core/decode.py (unified CONV+BYTE decoder) and
352
+ # core/serialize.py (unified serializer). ByteGPT = 24-layer GPT-2-class byte
353
+ # transformer; forward math is byte-faithful to core/decode.py's BYTE mouth and
354
+ # its state_dict keys are exactly what core/serialize.py::serialize(pt→bin) reads
355
+ # (tok/pos/blocks.{i}.ln1/attn.in_proj/attn.out_proj/ln2/mlp.0/mlp.2/ln_f/head).
356
+ # WHY: the CLEAN ρ·weave recombination wall (was G1 · H_1129) lives on ByteGPT (single=2) vs CLMConvMoE's single=0
357
+ # coverage floor; the arch-agnostic trunk-objective levers run on either trunk.
358
+ # ═══════════════════════════════════════════════════════════════════════════
359
+ class Block(nn.Module):
360
+ """Pre-LN GPT-2 transformer block — byte-faithful to core/decode.py BYTE mouth.
361
+
362
+ x = x + MHA(LN1(x)) ; x = x + MLP(LN2(x)). nn.MultiheadAttention gives the
363
+ in_proj_weight[3d,d]+in_proj_bias[3d] (rows Q|K|V) and out_proj.{weight[d,d],bias}
364
+ the serializer expects; the MLP nn.Sequential(Linear(d,4d), GELU, Linear(4d,d))
365
+ gives mlp.0 / mlp.2. Dropout p is settable (savant lever no-ops it to 0 for bytegpt)."""
366
+
367
+ def __init__(self, d: int, n_head: int, p: float = 0.0):
368
+ super().__init__()
369
+ self.ln1 = nn.LayerNorm(d)
370
+ self.attn = nn.MultiheadAttention(d, n_head, dropout=p, batch_first=True)
371
+ self.ln2 = nn.LayerNorm(d)
372
+ # GELU default (erf-exact) matches core/decode.py BYTE _bg_gelu (0.5x(1+erf(x/√2))).
373
+ self.mlp = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(),
374
+ nn.Linear(4 * d, d), nn.Dropout(p))
375
+
376
+ def forward(self, x, attn_mask):
377
+ h = self.ln1(x)
378
+ a, _ = self.attn(h, h, h, attn_mask=attn_mask, need_weights=False)
379
+ x = x + a
380
+ return x + self.mlp(self.ln2(x))
381
+
382
+
383
+ class ByteGPTConfig:
384
+ """Minimal config carrying the serializer's 5 header fields. vocab=256 (byte LM)."""
385
+
386
+ def __init__(self, vocab: int = 256, d: int = 768, n_layer: int = 24,
387
+ n_head: int = 12, block: int = 1024):
388
+ self.vocab = vocab
389
+ self.d = d
390
+ self.n_layer = n_layer
391
+ self.n_head = n_head
392
+ self.block = block
393
+
394
+ def as_dict(self) -> dict:
395
+ return {"vocab": self.vocab, "d": self.d, "n_layer": self.n_layer,
396
+ "n_head": self.n_head, "block": self.block}
397
+
398
+
399
+ class ByteGPT(nn.Module):
400
+ """24-layer GPT-2-class byte transformer. Emits the serializer's exact state_dict keys
401
+ and matches core/decode.py BYTE mouth forward math.
402
+
403
+ forward(idx, targets=None) returns a dict SHAPE-COMPATIBLE with cli/train.py's loop and
404
+ the arch-agnostic objective losses:
405
+ * logits : (B, V, T) — transposed so the objective losses / _ce see (B,V,T)
406
+ exactly like CLMConvMoE's readout output.
407
+ * penultimate : (B, d, T) — the pre-head hidden ln_f(x) (post-final-LayerNorm),
408
+ transposed to (B,d,T) so constructive_bind/predictive_info consume it
409
+ like the CLM trunk penultimate site (norm_out output).
410
+ * aux_loss : scalar 0.0 — ByteGPT has no MoE load-balance/entropy aux; kept so the
411
+ loop's `loss = obj_loss + out["aux_loss"]` is arch-uniform.
412
+ * ce_loss/loss: when targets given (parity with CLMConvMoE.forward).
413
+ """
414
+
415
+ def __init__(self, cfg: ByteGPTConfig):
416
+ super().__init__()
417
+ self.cfg = cfg
418
+ d, V, L, H, block = cfg.d, cfg.vocab, cfg.n_layer, cfg.n_head, cfg.block
419
+ self.block = block
420
+ self.tok = nn.Embedding(V, d)
421
+ self.pos = nn.Embedding(block, d)
422
+ self.drop = nn.Dropout(0.0)
423
+ self.blocks = nn.ModuleList([Block(d, H, 0.0) for _ in range(L)])
424
+ self.ln_f = nn.LayerNorm(d)
425
+ self.head = nn.Linear(d, V, bias=False)
426
+ self.head.weight = self.tok.weight # tied head (serializer reads tok as head)
427
+
428
+ def _penultimate(self, idx):
429
+ """Run the trunk to the post-final-LN hidden (pre-head). Returns (B,T,d)."""
430
+ B, T = idx.shape
431
+ pos = torch.arange(T, device=idx.device)
432
+ x = self.tok(idx) + self.pos(pos)[None, :, :]
433
+ x = self.drop(x)
434
+ mask = torch.triu(torch.full((T, T), float("-inf"), device=idx.device), diagonal=1)
435
+ for b in self.blocks:
436
+ x = b(x, mask)
437
+ return self.ln_f(x) # (B, T, d)
438
+
439
+ def forward(self, idx, targets=None) -> dict:
440
+ h = self._penultimate(idx) # (B, T, d)
441
+ logits_btv = self.head(h) # (B, T, V)
442
+ logits = logits_btv.transpose(1, 2) # (B, V, T) — CLM-compatible
443
+ out = {
444
+ "logits": logits,
445
+ "penultimate": h.transpose(1, 2), # (B, d, T)
446
+ "aux_loss": logits.new_zeros(()),
447
+ }
448
+ if targets is not None:
449
+ V = self.cfg.vocab
450
+ ce = F.cross_entropy(logits_btv.reshape(-1, V), targets.reshape(-1))
451
+ out["ce_loss"] = ce
452
+ out["loss"] = ce
453
+ return out
454
+
455
+ @torch.no_grad()
456
+ def num_params(self) -> int:
457
+ # tied head shares tok.weight, so parameters() already counts it once.
458
+ return sum(p.numel() for p in self.parameters())
459
+
460
+
461
+ def build_bytegpt(cfg: ByteGPTConfig) -> ByteGPT:
462
+ return ByteGPT(cfg)