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
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# ==========================================================================
|
|
2
|
+
# ⛔ ENGINE-INTERNAL — DO NOT RUN DIRECTLY. 학습/직렬화는 cli/ 단일진입만:
|
|
3
|
+
# anima train | anima serialize (canonical=hexa cli/{train,serialize}.hexa).
|
|
4
|
+
# `python3 train/clm/model/clm_serialize_v2.py` 직접 실행 = 단일진입 우회(#2603) + DIRECTIONAL. cli/ import만 허용.
|
|
5
|
+
# ==========================================================================
|
|
6
|
+
#!/usr/bin/env python3
|
|
7
|
+
"""Lane P REMEDY — torch -> .clm serializer matching core/clm_decode.hexa EXACTLY.
|
|
8
|
+
|
|
9
|
+
The prior CLM/model/clm_serialize.py emits a 2-track JSON-header format that
|
|
10
|
+
shares only the "CLM\\x01" magic with the ENGINE decoder and is NOT loadable
|
|
11
|
+
(see .verdicts/lane-p-clm/F-CLM-SERIALIZE-GAP.txt). This v2 serializer packs the
|
|
12
|
+
EXACT CLM\\x01 v0.2 byte layout that core/clm_decode.hexa::clm_decodable parses.
|
|
13
|
+
|
|
14
|
+
CLM\\x01 v0.2 byte layout (ground truth = core/clm_decode.hexa + the golden
|
|
15
|
+
reference state/laneg_d768_recover/reexport_d768_v2_fast.clm):
|
|
16
|
+
|
|
17
|
+
[0:4] MAGIC "CLM\\x01" = bytes 67,76,77,1
|
|
18
|
+
[4] nblk : u8 = 6 (production CLMConvMoE, E=2 / 1 trunk layer)
|
|
19
|
+
then nblk conv blocks, each:
|
|
20
|
+
u32 cout (LE)
|
|
21
|
+
u32 rest (LE) rest = Cin*K
|
|
22
|
+
int4 nibbles ((cout*rest+1)//2 bytes, 2 codes/byte, sym int4)
|
|
23
|
+
fp32 per-channel scale (cout float32 LE)
|
|
24
|
+
block order (decoder-fixed):
|
|
25
|
+
block0 ecW : cout=d, rest=d*K (K = rest0/d)
|
|
26
|
+
block1 tcW : cout=d, rest=d*K
|
|
27
|
+
block2 e0W : cout=d, rest=d*K
|
|
28
|
+
block3 e1W : cout=d, rest=d*K
|
|
29
|
+
block4 rW : cout=E, rest=d (router, K=1) -> E=2
|
|
30
|
+
block5 roW : cout=V, rest=d (readout, K=1) -> V=256
|
|
31
|
+
CLMX v0.2 trailer (REQUIRED — clm_decodable()=false without it):
|
|
32
|
+
"CLMX" = 67,76,77,88
|
|
33
|
+
n_ext : u8 = 11
|
|
34
|
+
then 11 ext arrays, EACH = u32 count (LE) + count float32 (LE), in ORDER:
|
|
35
|
+
embed (V*d), ecB (d), tcB (d), e0B (d), e1B (d), rB (E), roB (V),
|
|
36
|
+
tgG (d), tgB (d), noG (d), noB (d)
|
|
37
|
+
(tg* = trunk GroupNorm affine, no* = output GroupNorm affine)
|
|
38
|
+
|
|
39
|
+
int4-sym quant (decoder dequant w[co,j] = code * scale[co]):
|
|
40
|
+
per OUTPUT channel co: scale[co] = max_j(|w[co,j]|) / 7 (clamped >0)
|
|
41
|
+
code[co,j] = round(w[co,j]/scale[co]) clamped to [-7,7]
|
|
42
|
+
|
|
43
|
+
NIBBLE PACKING — must match the decoder byte-for-byte. The decoder reads, for a
|
|
44
|
+
flat code index i (i even):
|
|
45
|
+
code[i] = (byte & 0xF) - 8 # low nibble
|
|
46
|
+
code[i+1] = ((byte >> 4) & 0xF) - 8 # high nibble
|
|
47
|
+
So: low nibble holds the EVEN index code (+8 offset),
|
|
48
|
+
high nibble holds the ODD index code (+8 offset).
|
|
49
|
+
byte = ((code[i+1]+8) << 4) | (code[i]+8)
|
|
50
|
+
The trailing code of an odd-length run pads its high nibble with 0.
|
|
51
|
+
|
|
52
|
+
torch is imported lazily: pass a ckpt path / state_dict (torch present) OR a
|
|
53
|
+
plain dict of numpy/list weight arrays (torch absent) so the serializer is
|
|
54
|
+
testable without torch.
|
|
55
|
+
"""
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
# ⛔ ENGINE-INTERNAL — DO NOT RUN DIRECTLY (단일진입 우회 #2603). cli/ import만 허용.
|
|
59
|
+
# 가드는 `from __future__` 뒤에 둔다 — 앞에 두면 SyntaxError(from __future__ must be at top).
|
|
60
|
+
import sys as _anima_entry_guard
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
_anima_entry_guard.exit("⛔ clm_serialize_v2.py 직접 실행 금지 — cli/ 단일진입(anima train/serialize, canonical=hexa) 경유. #2603")
|
|
63
|
+
|
|
64
|
+
import struct
|
|
65
|
+
from typing import Dict, Any
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
import numpy as np
|
|
69
|
+
except Exception: # pragma: no cover - numpy is effectively always present
|
|
70
|
+
np = None
|
|
71
|
+
|
|
72
|
+
MAGIC = bytes([67, 76, 77, 1]) # "CLM\x01"
|
|
73
|
+
CLMX = bytes([67, 76, 77, 88]) # "CLMX"
|
|
74
|
+
CLMB = bytes([67, 76, 77, 66]) # "CLMB" — bind-readout (Hadamard) extension
|
|
75
|
+
INT4_SYM_MAX = 7
|
|
76
|
+
|
|
77
|
+
# readout-type flag (CLMB byte[4]). 0 = additive Conv1d(d->V) (default, NO CLMB
|
|
78
|
+
# section); 1 = bind/Hadamard g=u*v ; 2 = bind_linear (param-matched add) g=u+v.
|
|
79
|
+
RO_ADDITIVE = 0
|
|
80
|
+
RO_BIND_HADAMARD = 1
|
|
81
|
+
RO_BIND_LINEAR = 2
|
|
82
|
+
|
|
83
|
+
# state_dict key names of a CLMConvMoE configured E=2 / n_trunk_layers=1.
|
|
84
|
+
# (CLM/model/model.py: embed, embed_conv, trunk[0], moe.router, moe.experts[0/1],
|
|
85
|
+
# norm_out, readout). The decoder block order is ecW,tcW,e0W,e1W,rW,roW.
|
|
86
|
+
_KEYMAP = {
|
|
87
|
+
"ecW": "embed_conv.conv.weight",
|
|
88
|
+
"tcW": "trunk.0.conv.conv.weight",
|
|
89
|
+
"e0W": "moe.experts.0.conv.conv.weight",
|
|
90
|
+
"e1W": "moe.experts.1.conv.conv.weight",
|
|
91
|
+
"rW": "moe.router.weight",
|
|
92
|
+
"roW": "readout.weight",
|
|
93
|
+
# ext (CLMX) sources
|
|
94
|
+
"embed": "embed.weight",
|
|
95
|
+
"ecB": "embed_conv.conv.bias",
|
|
96
|
+
"tcB": "trunk.0.conv.conv.bias",
|
|
97
|
+
"e0B": "moe.experts.0.conv.conv.bias",
|
|
98
|
+
"e1B": "moe.experts.1.conv.conv.bias",
|
|
99
|
+
"rB": "moe.router.bias",
|
|
100
|
+
"roB": "readout.bias",
|
|
101
|
+
"tgG": "trunk.0.norm.weight",
|
|
102
|
+
"tgB": "trunk.0.norm.bias",
|
|
103
|
+
"noG": "norm_out.weight",
|
|
104
|
+
"noB": "norm_out.bias",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_BLOCK_ORDER = ["ecW", "tcW", "e0W", "e1W", "rW", "roW"]
|
|
108
|
+
_EXT_ORDER = ["embed", "ecB", "tcB", "e0B", "e1B", "rB", "roB",
|
|
109
|
+
"tgG", "tgB", "noG", "noB"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------------------------------- #
|
|
113
|
+
# v0.3 — GENERAL (n_trunk_layers L, n_experts E) block/ext ordering.
|
|
114
|
+
#
|
|
115
|
+
# The CLM\x01 format is ALREADY self-describing: nblk (byte[4]) + each block's
|
|
116
|
+
# (cout,rest) + n_ext + each ext count fully determine the layout. v0.2 only
|
|
117
|
+
# *hardcoded* the block-role assignment (L=1,E=2). v0.3 generalizes that
|
|
118
|
+
# assignment WITHOUT changing the byte grammar, so it is byte-exact backward
|
|
119
|
+
# compatible — a v0.3 file with L=1,E=2 is byte-identical to a v0.2 file.
|
|
120
|
+
#
|
|
121
|
+
# General block order (nblk = L + E + 3):
|
|
122
|
+
# ecW · tcW_0..tcW_{L-1} · e0W..e{E-1}W · rW(cout=E) · roW(cout=V)
|
|
123
|
+
# General ext order (n_ext = 2L + E + 6):
|
|
124
|
+
# embed · ecB · tcB_0..tcB_{L-1} · e0B..e{E-1}B · rB · roB ·
|
|
125
|
+
# tgG_0..tgG_{L-1} · tgB_0..tgB_{L-1} · noG · noB
|
|
126
|
+
#
|
|
127
|
+
# At L=1,E=2 this reduces EXACTLY to _BLOCK_ORDER / _EXT_ORDER above (the trunk
|
|
128
|
+
# bias tcB_0, then expert biases e0B,e1B, then rB,roB, then trunk GN tgG_0,tgB_0,
|
|
129
|
+
# then noG,noB) — byte-identical to v0.2.
|
|
130
|
+
#
|
|
131
|
+
# (L,E) recovery at decode time: E = cout of block[nblk-2] (router), V = cout of
|
|
132
|
+
# block[nblk-1] (readout), L = nblk - E - 3. No new bytes, no magic bump.
|
|
133
|
+
# --------------------------------------------------------------------------- #
|
|
134
|
+
|
|
135
|
+
# torch state_dict key templates for the general CLMConvMoE (model.py).
|
|
136
|
+
# embed.weight · embed_conv.conv.{weight,bias}
|
|
137
|
+
# trunk.{i}.conv.conv.{weight,bias} · trunk.{i}.norm.{weight,bias}
|
|
138
|
+
# moe.experts.{j}.conv.conv.{weight,bias} · moe.router.{weight,bias}
|
|
139
|
+
# norm_out.{weight,bias} · readout.{weight,bias}
|
|
140
|
+
def _general_block_keymap(L: int, E: int):
|
|
141
|
+
"""logical slot -> torch key, for the L*E general block order."""
|
|
142
|
+
km = {"ecW": "embed_conv.conv.weight"}
|
|
143
|
+
for i in range(L):
|
|
144
|
+
km[f"tc{i}W"] = f"trunk.{i}.conv.conv.weight"
|
|
145
|
+
for j in range(E):
|
|
146
|
+
km[f"e{j}W"] = f"moe.experts.{j}.conv.conv.weight"
|
|
147
|
+
km["rW"] = "moe.router.weight"
|
|
148
|
+
km["roW"] = "readout.weight"
|
|
149
|
+
return km
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _general_ext_keymap(L: int, E: int):
|
|
153
|
+
km = {"embed": "embed.weight", "ecB": "embed_conv.conv.bias"}
|
|
154
|
+
for i in range(L):
|
|
155
|
+
km[f"tc{i}B"] = f"trunk.{i}.conv.conv.bias"
|
|
156
|
+
for j in range(E):
|
|
157
|
+
km[f"e{j}B"] = f"moe.experts.{j}.conv.conv.bias"
|
|
158
|
+
km["rB"] = "moe.router.bias"
|
|
159
|
+
km["roB"] = "readout.bias"
|
|
160
|
+
for i in range(L):
|
|
161
|
+
km[f"tg{i}G"] = f"trunk.{i}.norm.weight"
|
|
162
|
+
for i in range(L):
|
|
163
|
+
km[f"tg{i}B"] = f"trunk.{i}.norm.bias"
|
|
164
|
+
km["noG"] = "norm_out.weight"
|
|
165
|
+
km["noB"] = "norm_out.bias"
|
|
166
|
+
return km
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _general_block_order(L: int, E: int):
|
|
170
|
+
return (["ecW"] + [f"tc{i}W" for i in range(L)]
|
|
171
|
+
+ [f"e{j}W" for j in range(E)] + ["rW", "roW"])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _general_ext_order(L: int, E: int):
|
|
175
|
+
return (["embed", "ecB"]
|
|
176
|
+
+ [f"tc{i}B" for i in range(L)]
|
|
177
|
+
+ [f"e{j}B" for j in range(E)]
|
|
178
|
+
+ ["rB", "roB"]
|
|
179
|
+
+ [f"tg{i}G" for i in range(L)]
|
|
180
|
+
+ [f"tg{i}B" for i in range(L)]
|
|
181
|
+
+ ["noG", "noB"])
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _to_np(x) -> "np.ndarray":
|
|
185
|
+
"""Coerce a torch.Tensor / numpy array / nested list to a float32 numpy array."""
|
|
186
|
+
if np is None:
|
|
187
|
+
raise RuntimeError("numpy is required for serialize_v2")
|
|
188
|
+
# torch tensor (duck-typed: has detach + cpu + numpy)
|
|
189
|
+
if hasattr(x, "detach"):
|
|
190
|
+
x = x.detach().cpu().float().numpy()
|
|
191
|
+
return np.asarray(x, dtype=np.float32)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _conv_w_to_2d(w: "np.ndarray", name: str) -> "np.ndarray":
|
|
195
|
+
"""Map a weight tensor to the (cout, rest=Cin*K) 2-D layout the decoder expects.
|
|
196
|
+
|
|
197
|
+
nn.Conv1d weight is (out, in, K). Decoder col-major within a block walks
|
|
198
|
+
j = ci*K + k (see clm_decode _clmd_conv1d xcol indexing: ci*K + k), and the
|
|
199
|
+
weight is read flat as w[co*rest + j] with j over Cin*K. nn.Conv1d's (out,in,K)
|
|
200
|
+
flattened C-order is exactly co, then (in, K) -> in*K + k = ci*K + k. So a
|
|
201
|
+
plain reshape(cout, -1) is byte-correct.
|
|
202
|
+
nn.Linear-style (router/readout are Conv1d k=1) -> (out, in, 1) -> (out, in).
|
|
203
|
+
"""
|
|
204
|
+
w = np.asarray(w, dtype=np.float32)
|
|
205
|
+
if w.ndim == 3:
|
|
206
|
+
cout = w.shape[0]
|
|
207
|
+
return w.reshape(cout, -1)
|
|
208
|
+
if w.ndim == 2:
|
|
209
|
+
return w
|
|
210
|
+
raise ValueError(f"unexpected weight ndim {w.ndim} for {name}")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _quant_block(w2d: "np.ndarray"):
|
|
214
|
+
"""int4-sym per-output-channel quant. Returns (codes int8 [cout,rest], scale f32 [cout])."""
|
|
215
|
+
cout = w2d.shape[0]
|
|
216
|
+
amax = np.abs(w2d).max(axis=1)
|
|
217
|
+
scale = np.maximum(amax / INT4_SYM_MAX, 1e-12).astype(np.float32) # >0, never div0
|
|
218
|
+
codes = np.round(w2d / scale[:, None])
|
|
219
|
+
codes = np.clip(codes, -INT4_SYM_MAX, INT4_SYM_MAX).astype(np.int64)
|
|
220
|
+
return codes.reshape(-1), scale, cout, w2d.shape[1]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _pack_nibbles(codes_flat: "np.ndarray") -> bytes:
|
|
224
|
+
"""Pack flat int4 codes [-7,7] to bytes, 2 codes/byte, matching the decoder:
|
|
225
|
+
low nibble = even-index code (+8), high nibble = odd-index code (+8)."""
|
|
226
|
+
n = codes_flat.shape[0]
|
|
227
|
+
off = (codes_flat.astype(np.int64) + 8) & 0xF # 0..15
|
|
228
|
+
if n % 2 == 1:
|
|
229
|
+
off = np.concatenate([off, np.zeros(1, dtype=np.int64)])
|
|
230
|
+
lo = off[0::2] # even indices -> low nibble
|
|
231
|
+
hi = off[1::2] # odd indices -> high nibble
|
|
232
|
+
packed = ((hi << 4) | lo).astype(np.uint8)
|
|
233
|
+
return packed.tobytes()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _pack_conv_block(w2d: "np.ndarray") -> bytes:
|
|
237
|
+
codes_flat, scale, cout, rest = _quant_block(w2d)
|
|
238
|
+
out = bytearray()
|
|
239
|
+
out += struct.pack("<I", cout)
|
|
240
|
+
out += struct.pack("<I", rest)
|
|
241
|
+
out += _pack_nibbles(codes_flat)
|
|
242
|
+
out += scale.astype("<f4").tobytes()
|
|
243
|
+
return bytes(out)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _pack_ext(arr: "np.ndarray") -> bytes:
|
|
247
|
+
flat = np.asarray(arr, dtype=np.float32).reshape(-1)
|
|
248
|
+
return struct.pack("<I", flat.shape[0]) + flat.astype("<f4").tobytes()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _resolve_state_dict(state_dict_or_ckpt, cfg) -> Dict[str, "np.ndarray"]:
|
|
252
|
+
"""Return a {logical_or_torch_key: np.ndarray} mapping.
|
|
253
|
+
|
|
254
|
+
Accepts:
|
|
255
|
+
- a path (str) to a torch ckpt -> torch.load (lazy import)
|
|
256
|
+
- a torch state_dict (OrderedDict of tensors)
|
|
257
|
+
- a plain dict keyed by torch keys (e.g. 'embed.weight') OR by logical
|
|
258
|
+
slot names (e.g. 'embed','ecW',...) -> values numpy/list arrays.
|
|
259
|
+
"""
|
|
260
|
+
sd = state_dict_or_ckpt
|
|
261
|
+
if isinstance(sd, str):
|
|
262
|
+
import torch # lazy
|
|
263
|
+
sd = torch.load(sd, map_location="cpu")
|
|
264
|
+
if isinstance(sd, dict) and "model" in sd and hasattr(sd.get("model"), "items"):
|
|
265
|
+
sd = sd["model"]
|
|
266
|
+
return sd
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get(sd: Dict[str, Any], logical: str, keymap=None) -> "np.ndarray":
|
|
270
|
+
"""Fetch a weight by logical slot name, accepting either logical keys or
|
|
271
|
+
torch state_dict keys in the source dict. `keymap` overrides _KEYMAP (used by
|
|
272
|
+
the general v0.3 path with per-(L,E) slot names)."""
|
|
273
|
+
km = keymap if keymap is not None else _KEYMAP
|
|
274
|
+
if logical in sd:
|
|
275
|
+
return _to_np(sd[logical])
|
|
276
|
+
tkey = km[logical]
|
|
277
|
+
if tkey in sd:
|
|
278
|
+
return _to_np(sd[tkey])
|
|
279
|
+
raise KeyError(
|
|
280
|
+
f"missing weight for slot '{logical}' (tried torch key '{tkey}'); "
|
|
281
|
+
f"available keys: {list(sd.keys())[:12]}..."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _assert_e2_l1(cfg):
|
|
286
|
+
"""The CORE decoder hardcodes E=2 experts + 1 trunk layer. Enforce it."""
|
|
287
|
+
if cfg is None:
|
|
288
|
+
return
|
|
289
|
+
n_e = getattr(cfg, "n_experts", None)
|
|
290
|
+
n_l = getattr(cfg, "n_trunk_layers", None)
|
|
291
|
+
if n_e is not None and n_e != 2:
|
|
292
|
+
raise ValueError(
|
|
293
|
+
f"core/clm_decode.hexa is FIXED to E=2 experts; cfg.n_experts={n_e}. "
|
|
294
|
+
f"Train with a CLMConfig(n_experts=2, n_trunk_layers=1) preset."
|
|
295
|
+
)
|
|
296
|
+
if n_l is not None and n_l != 1:
|
|
297
|
+
raise ValueError(
|
|
298
|
+
f"core/clm_decode.hexa is FIXED to 1 trunk layer; "
|
|
299
|
+
f"cfg.n_trunk_layers={n_l}. Train with n_trunk_layers=1."
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def serialize_v2(state_dict_or_ckpt, cfg, out_path: str) -> str:
|
|
304
|
+
"""Pack a CLMConvMoE (E=2 / 1-trunk) state_dict to a CLM\\x01 v0.2 .clm that
|
|
305
|
+
core/clm_decode.hexa loads. Returns out_path.
|
|
306
|
+
|
|
307
|
+
cfg may be a CLMConfig (asserted E=2/L1) or None (skip the assert — caller
|
|
308
|
+
vouches the dict already matches the E=2/L1 slot layout, e.g. synthetic test).
|
|
309
|
+
"""
|
|
310
|
+
if np is None:
|
|
311
|
+
raise RuntimeError("numpy is required for serialize_v2")
|
|
312
|
+
_assert_e2_l1(cfg)
|
|
313
|
+
sd = _resolve_state_dict(state_dict_or_ckpt, cfg)
|
|
314
|
+
|
|
315
|
+
blob = bytearray()
|
|
316
|
+
blob += MAGIC
|
|
317
|
+
blob += struct.pack("<B", 6) # nblk = 6
|
|
318
|
+
|
|
319
|
+
for slot in _BLOCK_ORDER:
|
|
320
|
+
w = _get(sd, slot)
|
|
321
|
+
w2d = _conv_w_to_2d(w, slot)
|
|
322
|
+
blob += _pack_conv_block(w2d)
|
|
323
|
+
|
|
324
|
+
blob += CLMX
|
|
325
|
+
blob += struct.pack("<B", len(_EXT_ORDER)) # n_ext = 11
|
|
326
|
+
for slot in _EXT_ORDER:
|
|
327
|
+
blob += _pack_ext(_get(sd, slot))
|
|
328
|
+
|
|
329
|
+
with open(out_path, "wb") as f:
|
|
330
|
+
f.write(blob)
|
|
331
|
+
return out_path
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def serialize_v3(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
|
|
335
|
+
out_path: str) -> str:
|
|
336
|
+
"""Pack a GENERAL CLMConvMoE(n_trunk_layers=L, n_experts=E, d, K) to a
|
|
337
|
+
CLM\\x01 v0.3 .clm that core/clm_decode.hexa loads.
|
|
338
|
+
|
|
339
|
+
v0.3 == v0.2 byte grammar, generalized block/ext counts (see the v0.3 note
|
|
340
|
+
above). At L=1,E=2 the output is BYTE-IDENTICAL to serialize_v2 (verified by
|
|
341
|
+
the round-trip gate). d, K, V are read from the weight shapes — no width
|
|
342
|
+
hardcode. Returns out_path.
|
|
343
|
+
"""
|
|
344
|
+
if np is None:
|
|
345
|
+
raise RuntimeError("numpy is required for serialize_v3")
|
|
346
|
+
L, E = int(n_trunk_layers), int(n_experts)
|
|
347
|
+
if L < 1 or E < 1:
|
|
348
|
+
raise ValueError(f"need L>=1 and E>=1, got L={L} E={E}")
|
|
349
|
+
sd = _resolve_state_dict(state_dict_or_ckpt, None)
|
|
350
|
+
blob = _pack_main_blob(sd, L, E)
|
|
351
|
+
with open(out_path, "wb") as f:
|
|
352
|
+
f.write(blob)
|
|
353
|
+
return out_path
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _pack_main_blob(sd: Dict[str, Any], L: int, E: int) -> bytearray:
|
|
357
|
+
"""Pack the MAIN CLM\\x01 body (MAGIC + nblk + conv blocks + CLMX + ext arrays)
|
|
358
|
+
for a general (L,E) CLMConvMoE, returning a bytearray (no CLMB section). Shared
|
|
359
|
+
by serialize_v3 (additive) and serialize_v3_bind (which appends a CLMB section).
|
|
360
|
+
The readout slot roW (cout=block[nblk-1].cout=V, rest) is taken from
|
|
361
|
+
'readout.weight' / roB from 'readout.bias': for a bind model the caller has
|
|
362
|
+
routed Wo -> readout.{weight,bias} so the block is (V, k) instead of (V, d)
|
|
363
|
+
(the byte grammar is self-describing — rest is read from the header at decode)."""
|
|
364
|
+
bkm = _general_block_keymap(L, E)
|
|
365
|
+
ekm = _general_ext_keymap(L, E)
|
|
366
|
+
border = _general_block_order(L, E)
|
|
367
|
+
eorder = _general_ext_order(L, E)
|
|
368
|
+
nblk = len(border) # = L + E + 3
|
|
369
|
+
n_ext = len(eorder) # = 2L + E + 6
|
|
370
|
+
|
|
371
|
+
blob = bytearray()
|
|
372
|
+
blob += MAGIC
|
|
373
|
+
blob += struct.pack("<B", nblk)
|
|
374
|
+
for slot in border:
|
|
375
|
+
w = _get(sd, slot, bkm)
|
|
376
|
+
w2d = _conv_w_to_2d(w, slot)
|
|
377
|
+
blob += _pack_conv_block(w2d)
|
|
378
|
+
blob += CLMX
|
|
379
|
+
blob += struct.pack("<B", n_ext)
|
|
380
|
+
for slot in eorder:
|
|
381
|
+
blob += _pack_ext(_get(sd, slot, ekm))
|
|
382
|
+
return blob
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _bget(sd: Dict[str, Any], names) -> "np.ndarray":
|
|
386
|
+
"""Fetch the first present key among `names` from a (torch- or logical-keyed)
|
|
387
|
+
state dict, coerced to a float32 numpy array. Accepts a 'base.'-stripped dict."""
|
|
388
|
+
for nm in names:
|
|
389
|
+
if nm in sd:
|
|
390
|
+
return _to_np(sd[nm])
|
|
391
|
+
raise KeyError(f"none of {names} present (have: {list(sd.keys())[:16]}...)")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def serialize_v3_bind(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
|
|
395
|
+
readout_type: int, out_path: str) -> str:
|
|
396
|
+
"""Pack a BIND-readout CLMConvMoE (the EXP-3 ARM-BIND architecture:
|
|
397
|
+
BindCLM = production trunk + Hadamard byte readout u=Wa(x), v=Wb(x),
|
|
398
|
+
g=u*v (readout_type=1) or u+v (readout_type=2), logits=Wo(g)) to a CLM\\x01
|
|
399
|
+
.clm that core/clm_decode.hexa loads.
|
|
400
|
+
|
|
401
|
+
BYTE LAYOUT (backward-compatible, in-place extension — no magic bump):
|
|
402
|
+
· MAIN body == serialize_v3, EXCEPT the readout slot roW carries **Wo**
|
|
403
|
+
(cout=V, rest=k) and roB carries **Wo.bias** (V). All trunk/embed/MoE
|
|
404
|
+
blocks+ext are IDENTICAL to the additive ctrl arm (only the readout
|
|
405
|
+
differs — the EXP-3 design intent). The self-describing (d,E,V,L,K)
|
|
406
|
+
recovery is UNCHANGED (E=block[nblk-2].cout, V=block[nblk-1].cout, the
|
|
407
|
+
readout block's `rest` field = k).
|
|
408
|
+
· a CLMB trailer is appended AFTER the CLMX ext arrays:
|
|
409
|
+
"CLMB" (67,76,77,66)
|
|
410
|
+
readout_type:u8 (1=Hadamard u*v, 2=linear u+v)
|
|
411
|
+
Wa conv block (cout=k, rest=d, int4-sym + per-channel fp32 scale)
|
|
412
|
+
Wb conv block (cout=k, rest=d, int4-sym + per-channel fp32 scale)
|
|
413
|
+
Wa_bias ext (u32 k + k*f32)
|
|
414
|
+
Wb_bias ext (u32 k + k*f32)
|
|
415
|
+
An additive .clm has NO CLMB section, so the decoder defaults
|
|
416
|
+
readout_type=0 (a_engine_native_learning backward-compat: existing
|
|
417
|
+
additive .clm decode byte-identically).
|
|
418
|
+
|
|
419
|
+
Accepts a torch state_dict (BindCLM.state_dict(): base.<trunk...> + Wa/Wb/Wo
|
|
420
|
+
.{weight,bias}) OR a logical dict (Wa,WaB,Wb,WbB,Wo,WoB + v3 slot names).
|
|
421
|
+
Returns out_path."""
|
|
422
|
+
if np is None:
|
|
423
|
+
raise RuntimeError("numpy is required for serialize_v3_bind")
|
|
424
|
+
L, E = int(n_trunk_layers), int(n_experts)
|
|
425
|
+
rt = int(readout_type)
|
|
426
|
+
if L < 1 or E < 1:
|
|
427
|
+
raise ValueError(f"need L>=1 and E>=1, got L={L} E={E}")
|
|
428
|
+
if rt not in (RO_BIND_HADAMARD, RO_BIND_LINEAR):
|
|
429
|
+
raise ValueError(f"readout_type must be 1 (hadamard) or 2 (linear), got {rt}")
|
|
430
|
+
sd = _resolve_state_dict(state_dict_or_ckpt, None)
|
|
431
|
+
|
|
432
|
+
# normalize: strip BindCLM's 'base.' trunk prefix into bare CLMConvMoE keys.
|
|
433
|
+
norm: Dict[str, Any] = {}
|
|
434
|
+
for k, v in sd.items():
|
|
435
|
+
nk = k[5:] if k.startswith("base.") else k
|
|
436
|
+
norm[nk] = v
|
|
437
|
+
|
|
438
|
+
# bind readout weights (Wa/Wb -> CLMB ; Wo -> routed into the roW/roB slots).
|
|
439
|
+
Wa = _bget(norm, ["Wa.weight", "Wa"])
|
|
440
|
+
WaB = _bget(norm, ["Wa.bias", "WaB"])
|
|
441
|
+
Wb = _bget(norm, ["Wb.weight", "Wb"])
|
|
442
|
+
WbB = _bget(norm, ["Wb.bias", "WbB"])
|
|
443
|
+
Wo = _bget(norm, ["Wo.weight", "Wo"])
|
|
444
|
+
WoB = _bget(norm, ["Wo.bias", "WoB"])
|
|
445
|
+
norm["readout.weight"] = Wo # (V, k, 1) -> roW block (cout=V, rest=k)
|
|
446
|
+
norm["readout.bias"] = WoB # (V,) -> roB ext
|
|
447
|
+
|
|
448
|
+
blob = _pack_main_blob(norm, L, E)
|
|
449
|
+
blob += CLMB
|
|
450
|
+
blob += struct.pack("<B", rt)
|
|
451
|
+
blob += _pack_conv_block(_conv_w_to_2d(Wa, "Wa")) # (k, d)
|
|
452
|
+
blob += _pack_conv_block(_conv_w_to_2d(Wb, "Wb")) # (k, d)
|
|
453
|
+
blob += _pack_ext(WaB) # (k,)
|
|
454
|
+
blob += _pack_ext(WbB) # (k,)
|
|
455
|
+
|
|
456
|
+
with open(out_path, "wb") as f:
|
|
457
|
+
f.write(blob)
|
|
458
|
+
return out_path
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def serialize(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
|
|
462
|
+
out_path: str) -> str:
|
|
463
|
+
"""Unified entry: routes to serialize_v3 (general). For L=1,E=2 the v3 path
|
|
464
|
+
is byte-identical to serialize_v2, so this is the single serializer for ANY
|
|
465
|
+
(L,E,d). cfg-free — caller states (L,E) explicitly."""
|
|
466
|
+
return serialize_v3(state_dict_or_ckpt, n_trunk_layers, n_experts, out_path)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
if __name__ == "__main__": # pragma: no cover
|
|
470
|
+
import argparse, json, os, sys
|
|
471
|
+
ap = argparse.ArgumentParser(description="torch CLMConvMoE -> CLM\\x01 v0.2 .clm")
|
|
472
|
+
ap.add_argument("--ckpt", required=True, help="torch state_dict ckpt path")
|
|
473
|
+
ap.add_argument("--out", required=True)
|
|
474
|
+
ap.add_argument("--n-experts", type=int, default=2)
|
|
475
|
+
ap.add_argument("--n-trunk-layers", type=int, default=1)
|
|
476
|
+
ap.add_argument("--d-model", type=int, default=768)
|
|
477
|
+
ap.add_argument("--kernel-size", type=int, default=3)
|
|
478
|
+
a = ap.parse_args()
|
|
479
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
480
|
+
if _HERE not in sys.path:
|
|
481
|
+
sys.path.insert(0, _HERE)
|
|
482
|
+
from model import CLMConfig # noqa
|
|
483
|
+
cfg = CLMConfig(n_experts=a.n_experts, n_trunk_layers=a.n_trunk_layers,
|
|
484
|
+
d_model=a.d_model, kernel_size=a.kernel_size)
|
|
485
|
+
p = serialize_v2(a.ckpt, cfg, a.out)
|
|
486
|
+
print(json.dumps({"out": p, "bytes": os.path.getsize(p)}))
|
anima_py/core/clml.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""core/clml.py — fork-A read-side context-pooling LANE (CLML), CORE-owned SSOT.
|
|
2
|
+
|
|
3
|
+
The ρ·weave recombination wall (was G1) is READOUT-ROUTING, not trunk representation-capacity
|
|
4
|
+
(H_9235 H2-lite): both concepts of a two-concept context ARE linearly present (mean-pool recovery
|
|
5
|
+
0.95/0.97), but the last-position (generation) readout only carries the most-recent concept
|
|
6
|
+
(last-position recovery A=0.07, B=1.00 — pure receptive-field decay, language-independent). The
|
|
7
|
+
CLML lane fixes the ROUTE without touching the trunk: it reads the trunk's per-position penultimate
|
|
8
|
+
`yn`, causally pools the full context (where the earlier concept survives), and adds a gated,
|
|
9
|
+
tether-clipped logit bias at composed positions. $0 pre-check PROVEN: mean-pool→gelu bottleneck
|
|
10
|
+
routes both concepts to held-out XOR (0.98) where last-position (0.47) and a linear head (0.43)
|
|
11
|
+
FAIL (state/g1_gamma_binding_lane/fork_a_precheck.py).
|
|
12
|
+
|
|
13
|
+
DISJOINT (a_substrate_disjoint): read-only tap of `yn` + an additive logit bias whose gate is
|
|
14
|
+
LEARNED to open only at composed positions — trunk weights, emit-drive lane, Ψ-tension untouched.
|
|
15
|
+
Absent trailer <=> byte-identical to today's .clm (loaders passthrough on short/absent read).
|
|
16
|
+
|
|
17
|
+
CORE-owned, ONE file (mirrors core/slw.py): lane_apply (torch-free numpy inference mirror,
|
|
18
|
+
byte-parity partner of core/decode.hexa) · pack_clml/read_clml ("CLML" trailer codec) · CLMLModule
|
|
19
|
+
(torch training module, DIRECTIONAL, defined only when torch importable so inference stays torch-free).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import struct
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
# ── "CLML" trailer magic (mirrors the CLMB/SLW trailer convention) ──────────────
|
|
28
|
+
CLML_MAGIC = bytes([67, 76, 77, 76]) # "CLML"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _sigmoid(x):
|
|
32
|
+
return 1.0 / (1.0 + np.exp(-np.clip(x, -30, 30)))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _gelu(x):
|
|
36
|
+
return 0.5 * x * (1.0 + np.tanh(0.7978845608028654 * (x + 0.044715 * x**3)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------------- #
|
|
40
|
+
# (a) numpy inference mirror — torch-free, byte-parity target for core/decode.hexa
|
|
41
|
+
# --------------------------------------------------------------------------- #
|
|
42
|
+
def lane_apply(yn, logits, clml):
|
|
43
|
+
"""Add the CLML read-side pooling bias to the readout logits.
|
|
44
|
+
|
|
45
|
+
yn: (T, d) float — pre-readout trunk penultimate (the _fwd_trunk output; the SAME
|
|
46
|
+
representation the $0 pre-check pooled — pre-SLW-slot).
|
|
47
|
+
logits: (T, V) float — the trunk readout logits to condition.
|
|
48
|
+
clml: dict from read_clml (W1 (d,r), b1 (r,), W2 (r,V), w_g (2d,), b_g scalar, tau, r).
|
|
49
|
+
Returns (T, V): logits + clip( g_t · gelu(c_t·W1+b1)·W2 , ±tau ), where c_t is the CAUSAL
|
|
50
|
+
cumulative mean of yn (0-param pool) and g_t = sigmoid(w_g·[yn_t;c_t]+b_g) is the learned
|
|
51
|
+
composed-position gate. lane_type=0 or clml=None => exact passthrough (logits unchanged).
|
|
52
|
+
Op order IDENTICAL to CLMLModule.forward (2-production parity)."""
|
|
53
|
+
if clml is None or int(clml.get("lane_type", 0)) == 0:
|
|
54
|
+
return logits
|
|
55
|
+
yn = np.asarray(yn); dt = logits.dtype
|
|
56
|
+
T, d = yn.shape
|
|
57
|
+
csum = np.cumsum(yn, axis=0)
|
|
58
|
+
counts = np.arange(1, T + 1, dtype=yn.dtype).reshape(T, 1)
|
|
59
|
+
c = csum / counts # (T,d) causal mean pool
|
|
60
|
+
gate = _sigmoid(np.concatenate([yn, c], axis=1) @ clml["w_g"] + clml["b_g"]) # (T,)
|
|
61
|
+
z = _gelu(c @ clml["W1"] + clml["b1"]) # (T,r) gelu bottleneck
|
|
62
|
+
delta = (z @ clml["W2"]) * gate[:, None] # (T,V) gated logit bias
|
|
63
|
+
delta = np.clip(delta, -float(clml["tau"]), float(clml["tau"]))
|
|
64
|
+
return (logits + delta).astype(dt)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------- #
|
|
68
|
+
# (b) "CLML" trailer codec — write (serialize) + read (loaders) · LE f32
|
|
69
|
+
# header: CLML magic · lane_type u8 · r u32 · tau f32
|
|
70
|
+
# arrays (row-major): W1[d·r] b1[r] W2[r·V] w_g[2d] b_g[1] (d,V from the model)
|
|
71
|
+
# --------------------------------------------------------------------------- #
|
|
72
|
+
_ARR_ORDER = ("W1", "b1", "W2", "w_g", "b_g")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def pack_clml(w: dict) -> bytes:
|
|
76
|
+
"""Pack a CLML weight dict into appended trailer bytes. `w` = W1 (d,r), b1 (r,),
|
|
77
|
+
W2 (r,V), w_g (2d,), b_g scalar, lane_type, r, tau. Absent trailer <=> byte-identical
|
|
78
|
+
additive/SLW model, so a writer only calls this when the model actually has a lane."""
|
|
79
|
+
out = bytearray()
|
|
80
|
+
out += CLML_MAGIC
|
|
81
|
+
out += struct.pack("<BIf", int(w.get("lane_type", 1)), int(w["r"]), float(w["tau"]))
|
|
82
|
+
for name in _ARR_ORDER:
|
|
83
|
+
out += np.asarray(w[name], dtype="<f4").reshape(-1).tobytes()
|
|
84
|
+
return bytes(out)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def read_clml(buf: bytes, off: int, d: int, V: int):
|
|
88
|
+
"""Read a CLML trailer at byte offset `off`. `d`,`V` come from the model (loader passes
|
|
89
|
+
W["d"], W["V"]). Returns (clml_dict, new_off) or (None, off) if absent/short (passthrough-
|
|
90
|
+
safe, same guard idiom as read_slw)."""
|
|
91
|
+
if off < 0 or off + 9 > len(buf) or buf[off:off + 4] != CLML_MAGIC:
|
|
92
|
+
return None, off
|
|
93
|
+
p = off + 4
|
|
94
|
+
lane_type = buf[p]; p += 1
|
|
95
|
+
(r,) = struct.unpack_from("<I", buf, p); p += 4
|
|
96
|
+
(tau,) = struct.unpack_from("<f", buf, p); p += 4
|
|
97
|
+
def take(n, shape):
|
|
98
|
+
nonlocal p
|
|
99
|
+
arr = np.frombuffer(buf, "<f4", n, p).reshape(shape).copy(); p += n * 4
|
|
100
|
+
return arr
|
|
101
|
+
clml = {"lane_type": int(lane_type), "r": int(r), "tau": float(tau)}
|
|
102
|
+
clml["W1"] = take(d * r, (d, r))
|
|
103
|
+
clml["b1"] = take(r, (r,))
|
|
104
|
+
clml["W2"] = take(r * V, (r, V))
|
|
105
|
+
clml["w_g"] = take(2 * d, (2 * d,))
|
|
106
|
+
clml["b_g"] = float(np.frombuffer(buf, "<f4", 1, p)[0]); p += 4
|
|
107
|
+
return clml, p
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clml_weights_from_torch(mod) -> dict:
|
|
111
|
+
"""Extract the CLML weight dict (numpy) from a trained torch CLMLModule."""
|
|
112
|
+
def n(t):
|
|
113
|
+
return t.detach().cpu().numpy().astype("<f4")
|
|
114
|
+
return {
|
|
115
|
+
"lane_type": 1, "r": int(mod.r), "tau": float(mod.tau),
|
|
116
|
+
"W1": n(mod.W1.weight).T, "b1": n(mod.W1.bias), # nn.Linear weight is (out,in)=(r,d) → (d,r)
|
|
117
|
+
"W2": n(mod.W2.weight).T, # (V,r) → (r,V)
|
|
118
|
+
"w_g": n(mod.w_g.weight).reshape(-1), "b_g": n(mod.w_g.bias),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------- #
|
|
123
|
+
# (c) torch training module (DIRECTIONAL) — defined only when torch is present so
|
|
124
|
+
# the inference import (lane_apply / read_clml) stays torch-free (pod-clean).
|
|
125
|
+
# --------------------------------------------------------------------------- #
|
|
126
|
+
try:
|
|
127
|
+
import torch as _torch
|
|
128
|
+
import torch.nn as _nn
|
|
129
|
+
_HAS_TORCH = True
|
|
130
|
+
except Exception: # pragma: no cover - inference pod has no torch
|
|
131
|
+
_HAS_TORCH = False
|
|
132
|
+
|
|
133
|
+
if _HAS_TORCH:
|
|
134
|
+
class CLMLModule(_nn.Module):
|
|
135
|
+
"""Learnable read-side context-pooling lane. forward(yn:(B,T,d), logits:(B,T,V)) ->
|
|
136
|
+
(B,T,V). Op order MIRRORS core/clml.lane_apply exactly for 2-production parity.
|
|
137
|
+
Trains ONLY {W1,b1,W2,w_g,b_g} — the trunk is frozen (fork A = read-side, DISJOINT)."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, d, V, r=128, tau=8.0):
|
|
140
|
+
super().__init__()
|
|
141
|
+
self.d, self.V, self.r, self.tau = d, V, r, tau
|
|
142
|
+
self.W1 = _nn.Linear(d, r)
|
|
143
|
+
self.W2 = _nn.Linear(r, V, bias=False)
|
|
144
|
+
self.w_g = _nn.Linear(2 * d, 1)
|
|
145
|
+
|
|
146
|
+
def forward(self, yn, logits):
|
|
147
|
+
B, T, d = yn.shape
|
|
148
|
+
csum = _torch.cumsum(yn, dim=1)
|
|
149
|
+
counts = _torch.arange(1, T + 1, device=yn.device, dtype=yn.dtype).view(1, T, 1)
|
|
150
|
+
c = csum / counts
|
|
151
|
+
gate = _torch.sigmoid(self.w_g(_torch.cat([yn, c], dim=-1))) # (B,T,1)
|
|
152
|
+
z = _torch.nn.functional.gelu(self.W1(c))
|
|
153
|
+
delta = self.W2(z) * gate
|
|
154
|
+
delta = _torch.clamp(delta, -self.tau, self.tau)
|
|
155
|
+
return logits + delta
|