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/README.md +39 -0
- anima_py/__init__.py +28 -0
- anima_py/__main__.py +7 -0
- anima_py/cli/__init__.py +1 -0
- anima_py/cli/anima.py +271 -0
- anima_py/cli/corpus.py +150 -0
- anima_py/cli/evaluate.py +1350 -0
- anima_py/cli/rho_axon.py +801 -0
- anima_py/cli/serialize.py +174 -0
- anima_py/cli/serialize_bind.py +100 -0
- anima_py/cli/sweep.py +509 -0
- anima_py/cli/train.py +1667 -0
- anima_py/core/__init__.py +1 -0
- anima_py/core/brain.py +601 -0
- anima_py/core/clm_serialize_v2.py +486 -0
- anima_py/core/clml.py +155 -0
- anima_py/core/decode.py +1386 -0
- anima_py/core/engine_cli.py +10307 -0
- anima_py/core/engine_g.py +129 -0
- anima_py/core/generator.py +318 -0
- anima_py/core/hippo_lane.py +133 -0
- anima_py/core/model.py +462 -0
- anima_py/core/pure_field.py +334 -0
- anima_py/core/rho_fan.py +442 -0
- anima_py/core/serialize.py +826 -0
- anima_py/core/serialize_standalone.py +186 -0
- anima_py/core/slw.py +211 -0
- anima_py/core/verify_clm_v2.py +844 -0
- anima_python-0.13.1.dist-info/METADATA +52 -0
- anima_python-0.13.1.dist-info/RECORD +34 -0
- anima_python-0.13.1.dist-info/WHEEL +5 -0
- anima_python-0.13.1.dist-info/entry_points.txt +2 -0
- anima_python-0.13.1.dist-info/licenses/LICENSE +21 -0
- anima_python-0.13.1.dist-info/top_level.txt +1 -0
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)
|