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.
@@ -0,0 +1,826 @@
1
+ # ==========================================================================
2
+ # ⛔ ENGINE-INTERNAL — DO NOT RUN DIRECTLY. 학습/직렬화는 cli/ 단일진입만:
3
+ # anima train | anima serialize (canonical=hexa cli/{train,serialize}.hexa).
4
+ # `python3 core/serialize.py` 직접 실행 = 단일진입 우회(#2603) + DIRECTIONAL. cli/ import만 허용.
5
+ # ==========================================================================
6
+ """core/serialize.py — UNIFIED PY SERIALIZE ENGINE: byte-faithful 1:1 merge of the
7
+ two per-mouth serializer backends into ONE module (parallel to core/decode.py, the
8
+ unified CONV+BYTE decoder).
9
+
10
+ * CONV (CLM ConvMoE) mouth = the verbatim body of archive/train/clm/model/
11
+ clm_serialize_v2.py — torch/numpy state_dict → .clm
12
+ v0.2/v0.3 (`serialize_v3` = the byte-grammar SSOT
13
+ that core/decode.hexa's CONV mouth parses).
14
+ * BYTE (ByteGPT transformer) = the verbatim body of tool/bytegpt_serialize.py —
15
+ torch .pt → engine .bin (5×u32 header) BRIDGE.
16
+
17
+ Per CLAUDE.md a_clm_gen_pipeline + a_engine_native_learning: serialize is the
18
+ LEARNING-side bridge (it must UNPICKLE a torch .pt = irreducibly Python — train/
19
+ serialize MAY use torch; this is NOT the verdict scorer). This module is the numpy/
20
+ struct bridge for BOTH mouth artifacts; it exposes the UNION of both backends'
21
+ public names so a caller can do `import serialize as S` (→ CLM `serialize_v3`) OR
22
+ `import serialize as BGS` (→ ByteGPT `serialize`) with ZERO call-site churn (a pure
23
+ drop-in for clm_serialize_v2.py + bytegpt_serialize.py — mirrors how core/decode.py
24
+ unioned clm_decode + bytegpt_decode).
25
+
26
+ Organization:
27
+ (a) SHARED — struct/numpy imports + the CLM magic byte constants.
28
+ (b) CONV (CLM) — the full clm_serialize_v2.py public API, VERBATIM (serialize_v2,
29
+ serialize_v3, serialize_v3_bind, and every helper). serialize_v3 is the byte
30
+ SSOT (byte-identical to the golden reexport_d768_v2_fast.clm) — copied
31
+ verbatim, NOT "improved".
32
+ (c) BYTE (ByteGPT) — the full bytegpt_serialize.py public API. Its top-level
33
+ `serialize(pt, bin)` is the .pt → .bin bridge; the ByteGPT-only helper _f32le.
34
+ (d) NAME-DISPATCH — the two backends each defined a top-level `serialize` with a
35
+ DIFFERENT signature (CLM `serialize(sd,L,E,out)` vs ByteGPT `serialize(pt,bin)`).
36
+ They cannot both bind the bare name. The LIVE importer contract (grep-verified)
37
+ is: cli/serialize.py + cli/train.py call `S.serialize_v3` (never bare CLM
38
+ `serialize`), and cli/train.py calls `BGS.serialize` (the ByteGPT bridge). So:
39
+ * top-level `serialize` = ByteGPT `serialize(pt, bin)` (satisfies BGS.serialize)
40
+ * `serialize_clm(sd,L,E,out)` = the CLM unified entry (was clm's bare `serialize`;
41
+ preserved under a distinct name, no live caller)
42
+ Both keep serialize_v2/serialize_v3/serialize_v3_bind (CLM) unchanged.
43
+ (e) `serialize_auto(...)` — a small target-extension dispatcher (.bin → ByteGPT,
44
+ else → CLM serialize_v3), convenience only; the existing entry points are
45
+ UNRENAMED.
46
+ """
47
+ from __future__ import annotations
48
+
49
+ # ⛔ ENGINE-INTERNAL — DO NOT RUN DIRECTLY (단일진입 우회 #2603). cli/ import만 허용.
50
+ # 가드는 `from __future__` 뒤에 둔다 — 앞에 두면 SyntaxError(from __future__ must be at top).
51
+ import sys as _anima_entry_guard
52
+ if __name__ == "__main__":
53
+ _anima_entry_guard.exit("⛔ core/serialize.py 직접 실행 금지 — cli/ 단일진입(anima train/serialize, canonical=hexa) 경유. #2603")
54
+
55
+ import struct
56
+ from typing import Dict, Any
57
+
58
+ try:
59
+ import numpy as np
60
+ except Exception: # pragma: no cover - numpy is effectively always present
61
+ np = None
62
+
63
+
64
+ # ════════════════════════════════════════════════════════════════════════
65
+ # (a) SHARED — CLM magic byte constants (used by the CONV serializer).
66
+ # ════════════════════════════════════════════════════════════════════════
67
+ MAGIC = bytes([67, 76, 77, 1]) # "CLM\x01"
68
+ CLMX = bytes([67, 76, 77, 88]) # "CLMX"
69
+ CLMB = bytes([67, 76, 77, 66]) # "CLMB" — bind-readout (Hadamard) extension
70
+ INT4_SYM_MAX = 7
71
+
72
+
73
+ # ════════════════════════════════════════════════════════════════════════
74
+ # H_9200 E1 — "SLW\x01" gated-write forward-slot trailer (CORE-owned codec in
75
+ # core/slw.py). Appended at the END of the trailer chain (after CLMX ext / CLMB),
76
+ # so an SLW model = a normal additive .clm + this trailer. Absent => byte-identical
77
+ # to today's additive .clm (the loaders passthrough on a short/absent read).
78
+ # ════════════════════════════════════════════════════════════════════════
79
+ def append_slw_trailer(out_path: str, slw_module) -> int:
80
+ """Append the SLW trailer to an already-written .clm (after serialize_v3). Reads
81
+ the trained torch SLWModule, packs it via core/slw.pack_slw, and appends the
82
+ bytes. Returns the number of trailer bytes written. No-op path: callers only
83
+ invoke this when model.slw is not None, so the additive .clm stays untouched."""
84
+ from slw import slw_weights_from_torch, pack_slw # core/slw.py (same core/ dir)
85
+ trailer = pack_slw(slw_weights_from_torch(slw_module))
86
+ with open(out_path, "ab") as f:
87
+ f.write(trailer)
88
+ return len(trailer)
89
+
90
+
91
+ # ════════════════════════════════════════════════════════════════════════
92
+ # fork-A "CLML" read-side context-pooling lane trailer (CORE-owned codec in
93
+ # core/clml.py). Appended at the END of the trailer chain (after CLMX / CLMB /
94
+ # SLW), so a lane model = a normal .clm + this trailer. Absent => byte-identical
95
+ # (loaders passthrough on short/absent read). Trains a frozen-trunk lane (H_9235).
96
+ # ════════════════════════════════════════════════════════════════════════
97
+ def append_clml_trailer(out_path: str, clml) -> int:
98
+ """Append the CLML lane trailer to an already-written .clm. `clml` = a trained torch
99
+ CLMLModule OR a ready numpy weight dict (W1,b1,W2,w_g,b_g,r,tau). Returns bytes written.
100
+ Callers only invoke this when the model actually has a fork-A lane."""
101
+ from clml import pack_clml, clml_weights_from_torch # core/clml.py (same core/ dir)
102
+ w = clml if isinstance(clml, dict) else clml_weights_from_torch(clml)
103
+ trailer = pack_clml(w)
104
+ with open(out_path, "ab") as f:
105
+ f.write(trailer)
106
+ return len(trailer)
107
+
108
+ # readout-type flag (CLMB byte[4]). 0 = additive Conv1d(d->V) (default, NO CLMB
109
+ # section); 1 = bind/Hadamard g=u*v ; 2 = bind_linear (param-matched add) g=u+v.
110
+ RO_ADDITIVE = 0
111
+ RO_BIND_HADAMARD = 1
112
+ RO_BIND_LINEAR = 2
113
+
114
+
115
+ # ════════════════════════════════════════════════════════════════════════
116
+ # (b) CONV (CLM) — verbatim port of clm_serialize_v2.py. serialize_v3 is the
117
+ # .clm byte-grammar SSOT core/decode.hexa's CONV mouth parses (golden
118
+ # reexport_d768_v2_fast.clm). DO NOT alter the byte layout.
119
+ # ════════════════════════════════════════════════════════════════════════
120
+
121
+ # state_dict key names of a CLMConvMoE configured E=2 / n_trunk_layers=1.
122
+ # (CLM/model/model.py: embed, embed_conv, trunk[0], moe.router, moe.experts[0/1],
123
+ # norm_out, readout). The decoder block order is ecW,tcW,e0W,e1W,rW,roW.
124
+ _KEYMAP = {
125
+ "ecW": "embed_conv.conv.weight",
126
+ "tcW": "trunk.0.conv.conv.weight",
127
+ "e0W": "moe.experts.0.conv.conv.weight",
128
+ "e1W": "moe.experts.1.conv.conv.weight",
129
+ "rW": "moe.router.weight",
130
+ "roW": "readout.weight",
131
+ # ext (CLMX) sources
132
+ "embed": "embed.weight",
133
+ "ecB": "embed_conv.conv.bias",
134
+ "tcB": "trunk.0.conv.conv.bias",
135
+ "e0B": "moe.experts.0.conv.conv.bias",
136
+ "e1B": "moe.experts.1.conv.conv.bias",
137
+ "rB": "moe.router.bias",
138
+ "roB": "readout.bias",
139
+ "tgG": "trunk.0.norm.weight",
140
+ "tgB": "trunk.0.norm.bias",
141
+ "noG": "norm_out.weight",
142
+ "noB": "norm_out.bias",
143
+ }
144
+
145
+ _BLOCK_ORDER = ["ecW", "tcW", "e0W", "e1W", "rW", "roW"]
146
+ _EXT_ORDER = ["embed", "ecB", "tcB", "e0B", "e1B", "rB", "roB",
147
+ "tgG", "tgB", "noG", "noB"]
148
+
149
+
150
+ # --------------------------------------------------------------------------- #
151
+ # v0.3 — GENERAL (n_trunk_layers L, n_experts E) block/ext ordering.
152
+ #
153
+ # The CLM\x01 format is ALREADY self-describing: nblk (byte[4]) + each block's
154
+ # (cout,rest) + n_ext + each ext count fully determine the layout. v0.2 only
155
+ # *hardcoded* the block-role assignment (L=1,E=2). v0.3 generalizes that
156
+ # assignment WITHOUT changing the byte grammar, so it is byte-exact backward
157
+ # compatible — a v0.3 file with L=1,E=2 is byte-identical to a v0.2 file.
158
+ #
159
+ # General block order (nblk = L + E + 3):
160
+ # ecW · tcW_0..tcW_{L-1} · e0W..e{E-1}W · rW(cout=E) · roW(cout=V)
161
+ # General ext order (n_ext = 2L + E + 6):
162
+ # embed · ecB · tcB_0..tcB_{L-1} · e0B..e{E-1}B · rB · roB ·
163
+ # tgG_0..tgG_{L-1} · tgB_0..tgB_{L-1} · noG · noB
164
+ #
165
+ # At L=1,E=2 this reduces EXACTLY to _BLOCK_ORDER / _EXT_ORDER above (the trunk
166
+ # bias tcB_0, then expert biases e0B,e1B, then rB,roB, then trunk GN tgG_0,tgB_0,
167
+ # then noG,noB) — byte-identical to v0.2.
168
+ #
169
+ # (L,E) recovery at decode time: E = cout of block[nblk-2] (router), V = cout of
170
+ # block[nblk-1] (readout), L = nblk - E - 3. No new bytes, no magic bump.
171
+ # --------------------------------------------------------------------------- #
172
+
173
+ # torch state_dict key templates for the general CLMConvMoE (model.py).
174
+ # embed.weight · embed_conv.conv.{weight,bias}
175
+ # trunk.{i}.conv.conv.{weight,bias} · trunk.{i}.norm.{weight,bias}
176
+ # moe.experts.{j}.conv.conv.{weight,bias} · moe.router.{weight,bias}
177
+ # norm_out.{weight,bias} · readout.{weight,bias}
178
+ def _general_block_keymap(L: int, E: int):
179
+ """logical slot -> torch key, for the L*E general block order."""
180
+ km = {"ecW": "embed_conv.conv.weight"}
181
+ for i in range(L):
182
+ km[f"tc{i}W"] = f"trunk.{i}.conv.conv.weight"
183
+ for j in range(E):
184
+ km[f"e{j}W"] = f"moe.experts.{j}.conv.conv.weight"
185
+ km["rW"] = "moe.router.weight"
186
+ km["roW"] = "readout.weight"
187
+ return km
188
+
189
+
190
+ def _general_ext_keymap(L: int, E: int):
191
+ km = {"embed": "embed.weight", "ecB": "embed_conv.conv.bias"}
192
+ for i in range(L):
193
+ km[f"tc{i}B"] = f"trunk.{i}.conv.conv.bias"
194
+ for j in range(E):
195
+ km[f"e{j}B"] = f"moe.experts.{j}.conv.conv.bias"
196
+ km["rB"] = "moe.router.bias"
197
+ km["roB"] = "readout.bias"
198
+ for i in range(L):
199
+ km[f"tg{i}G"] = f"trunk.{i}.norm.weight"
200
+ for i in range(L):
201
+ km[f"tg{i}B"] = f"trunk.{i}.norm.bias"
202
+ km["noG"] = "norm_out.weight"
203
+ km["noB"] = "norm_out.bias"
204
+ return km
205
+
206
+
207
+ def _general_block_order(L: int, E: int):
208
+ return (["ecW"] + [f"tc{i}W" for i in range(L)]
209
+ + [f"e{j}W" for j in range(E)] + ["rW", "roW"])
210
+
211
+
212
+ def _general_ext_order(L: int, E: int):
213
+ return (["embed", "ecB"]
214
+ + [f"tc{i}B" for i in range(L)]
215
+ + [f"e{j}B" for j in range(E)]
216
+ + ["rB", "roB"]
217
+ + [f"tg{i}G" for i in range(L)]
218
+ + [f"tg{i}B" for i in range(L)]
219
+ + ["noG", "noB"])
220
+
221
+
222
+ def _to_np(x) -> "np.ndarray":
223
+ """Coerce a torch.Tensor / numpy array / nested list to a float32 numpy array."""
224
+ if np is None:
225
+ raise RuntimeError("numpy is required for serialize_v2")
226
+ # torch tensor (duck-typed: has detach + cpu + numpy)
227
+ if hasattr(x, "detach"):
228
+ x = x.detach().cpu().float().numpy()
229
+ return np.asarray(x, dtype=np.float32)
230
+
231
+
232
+ def _conv_w_to_2d(w: "np.ndarray", name: str) -> "np.ndarray":
233
+ """Map a weight tensor to the (cout, rest=Cin*K) 2-D layout the decoder expects.
234
+
235
+ nn.Conv1d weight is (out, in, K). Decoder col-major within a block walks
236
+ j = ci*K + k (see clm_decode _clmd_conv1d xcol indexing: ci*K + k), and the
237
+ weight is read flat as w[co*rest + j] with j over Cin*K. nn.Conv1d's (out,in,K)
238
+ flattened C-order is exactly co, then (in, K) -> in*K + k = ci*K + k. So a
239
+ plain reshape(cout, -1) is byte-correct.
240
+ nn.Linear-style (router/readout are Conv1d k=1) -> (out, in, 1) -> (out, in).
241
+ """
242
+ w = np.asarray(w, dtype=np.float32)
243
+ if w.ndim == 3:
244
+ cout = w.shape[0]
245
+ return w.reshape(cout, -1)
246
+ if w.ndim == 2:
247
+ return w
248
+ raise ValueError(f"unexpected weight ndim {w.ndim} for {name}")
249
+
250
+
251
+ def _quant_block(w2d: "np.ndarray"):
252
+ """int4-sym per-output-channel quant. Returns (codes int8 [cout,rest], scale f32 [cout])."""
253
+ cout = w2d.shape[0]
254
+ amax = np.abs(w2d).max(axis=1)
255
+ scale = np.maximum(amax / INT4_SYM_MAX, 1e-12).astype(np.float32) # >0, never div0
256
+ codes = np.round(w2d / scale[:, None])
257
+ codes = np.clip(codes, -INT4_SYM_MAX, INT4_SYM_MAX).astype(np.int64)
258
+ return codes.reshape(-1), scale, cout, w2d.shape[1]
259
+
260
+
261
+ def _pack_nibbles(codes_flat: "np.ndarray") -> bytes:
262
+ """Pack flat int4 codes [-7,7] to bytes, 2 codes/byte, matching the decoder:
263
+ low nibble = even-index code (+8), high nibble = odd-index code (+8)."""
264
+ n = codes_flat.shape[0]
265
+ off = (codes_flat.astype(np.int64) + 8) & 0xF # 0..15
266
+ if n % 2 == 1:
267
+ off = np.concatenate([off, np.zeros(1, dtype=np.int64)])
268
+ lo = off[0::2] # even indices -> low nibble
269
+ hi = off[1::2] # odd indices -> high nibble
270
+ packed = ((hi << 4) | lo).astype(np.uint8)
271
+ return packed.tobytes()
272
+
273
+
274
+ def _pack_conv_block(w2d: "np.ndarray") -> bytes:
275
+ codes_flat, scale, cout, rest = _quant_block(w2d)
276
+ out = bytearray()
277
+ out += struct.pack("<I", cout)
278
+ out += struct.pack("<I", rest)
279
+ out += _pack_nibbles(codes_flat)
280
+ out += scale.astype("<f4").tobytes()
281
+ return bytes(out)
282
+
283
+
284
+ def _pack_ext(arr: "np.ndarray") -> bytes:
285
+ flat = np.asarray(arr, dtype=np.float32).reshape(-1)
286
+ return struct.pack("<I", flat.shape[0]) + flat.astype("<f4").tobytes()
287
+
288
+
289
+ def _resolve_state_dict(state_dict_or_ckpt, cfg) -> Dict[str, "np.ndarray"]:
290
+ """Return a {logical_or_torch_key: np.ndarray} mapping.
291
+
292
+ Accepts:
293
+ - a path (str) to a torch ckpt -> torch.load (lazy import)
294
+ - a torch state_dict (OrderedDict of tensors)
295
+ - a plain dict keyed by torch keys (e.g. 'embed.weight') OR by logical
296
+ slot names (e.g. 'embed','ecW',...) -> values numpy/list arrays.
297
+ """
298
+ sd = state_dict_or_ckpt
299
+ if isinstance(sd, str):
300
+ import torch # lazy
301
+ sd = torch.load(sd, map_location="cpu")
302
+ if isinstance(sd, dict) and "model" in sd and hasattr(sd.get("model"), "items"):
303
+ sd = sd["model"]
304
+ return sd
305
+
306
+
307
+ def _get(sd: Dict[str, Any], logical: str, keymap=None) -> "np.ndarray":
308
+ """Fetch a weight by logical slot name, accepting either logical keys or
309
+ torch state_dict keys in the source dict. `keymap` overrides _KEYMAP (used by
310
+ the general v0.3 path with per-(L,E) slot names)."""
311
+ km = keymap if keymap is not None else _KEYMAP
312
+ if logical in sd:
313
+ return _to_np(sd[logical])
314
+ tkey = km[logical]
315
+ if tkey in sd:
316
+ return _to_np(sd[tkey])
317
+ raise KeyError(
318
+ f"missing weight for slot '{logical}' (tried torch key '{tkey}'); "
319
+ f"available keys: {list(sd.keys())[:12]}..."
320
+ )
321
+
322
+
323
+ def _assert_e2_l1(cfg):
324
+ """The CORE decoder hardcodes E=2 experts + 1 trunk layer. Enforce it."""
325
+ if cfg is None:
326
+ return
327
+ n_e = getattr(cfg, "n_experts", None)
328
+ n_l = getattr(cfg, "n_trunk_layers", None)
329
+ if n_e is not None and n_e != 2:
330
+ raise ValueError(
331
+ f"core/decode.hexa CONV mouth is FIXED to E=2 experts; cfg.n_experts={n_e}. "
332
+ f"Train with a CLMConfig(n_experts=2, n_trunk_layers=1) preset."
333
+ )
334
+ if n_l is not None and n_l != 1:
335
+ raise ValueError(
336
+ f"core/decode.hexa CONV mouth is FIXED to 1 trunk layer; "
337
+ f"cfg.n_trunk_layers={n_l}. Train with n_trunk_layers=1."
338
+ )
339
+
340
+
341
+ def serialize_v2(state_dict_or_ckpt, cfg, out_path: str) -> str:
342
+ """Pack a CLMConvMoE (E=2 / 1-trunk) state_dict to a CLM\\x01 v0.2 .clm that
343
+ core/decode.hexa's CONV mouth loads. Returns out_path.
344
+
345
+ cfg may be a CLMConfig (asserted E=2/L1) or None (skip the assert — caller
346
+ vouches the dict already matches the E=2/L1 slot layout, e.g. synthetic test).
347
+ """
348
+ if np is None:
349
+ raise RuntimeError("numpy is required for serialize_v2")
350
+ _assert_e2_l1(cfg)
351
+ sd = _resolve_state_dict(state_dict_or_ckpt, cfg)
352
+
353
+ blob = bytearray()
354
+ blob += MAGIC
355
+ blob += struct.pack("<B", 6) # nblk = 6
356
+
357
+ for slot in _BLOCK_ORDER:
358
+ w = _get(sd, slot)
359
+ w2d = _conv_w_to_2d(w, slot)
360
+ blob += _pack_conv_block(w2d)
361
+
362
+ blob += CLMX
363
+ blob += struct.pack("<B", len(_EXT_ORDER)) # n_ext = 11
364
+ for slot in _EXT_ORDER:
365
+ blob += _pack_ext(_get(sd, slot))
366
+
367
+ with open(out_path, "wb") as f:
368
+ f.write(blob)
369
+ return out_path
370
+
371
+
372
+ def serialize_v3(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
373
+ out_path: str) -> str:
374
+ """Pack a GENERAL CLMConvMoE(n_trunk_layers=L, n_experts=E, d, K) to a
375
+ CLM\\x01 v0.3 .clm that core/decode.hexa's CONV mouth loads.
376
+
377
+ v0.3 == v0.2 byte grammar, generalized block/ext counts (see the v0.3 note
378
+ above). At L=1,E=2 the output is BYTE-IDENTICAL to serialize_v2 (verified by
379
+ the round-trip gate). d, K, V are read from the weight shapes — no width
380
+ hardcode. Returns out_path.
381
+ """
382
+ if np is None:
383
+ raise RuntimeError("numpy is required for serialize_v3")
384
+ L, E = int(n_trunk_layers), int(n_experts)
385
+ if L < 1 or E < 1:
386
+ raise ValueError(f"need L>=1 and E>=1, got L={L} E={E}")
387
+ sd = _resolve_state_dict(state_dict_or_ckpt, None)
388
+ blob = _pack_main_blob(sd, L, E)
389
+ with open(out_path, "wb") as f:
390
+ f.write(blob)
391
+ return out_path
392
+
393
+
394
+ def _pack_main_blob(sd: Dict[str, Any], L: int, E: int) -> bytearray:
395
+ """Pack the MAIN CLM\\x01 body (MAGIC + nblk + conv blocks + CLMX + ext arrays)
396
+ for a general (L,E) CLMConvMoE, returning a bytearray (no CLMB section). Shared
397
+ by serialize_v3 (additive) and serialize_v3_bind (which appends a CLMB section).
398
+ The readout slot roW (cout=block[nblk-1].cout=V, rest) is taken from
399
+ 'readout.weight' / roB from 'readout.bias': for a bind model the caller has
400
+ routed Wo -> readout.{weight,bias} so the block is (V, k) instead of (V, d)
401
+ (the byte grammar is self-describing — rest is read from the header at decode)."""
402
+ bkm = _general_block_keymap(L, E)
403
+ ekm = _general_ext_keymap(L, E)
404
+ border = _general_block_order(L, E)
405
+ eorder = _general_ext_order(L, E)
406
+ nblk = len(border) # = L + E + 3
407
+ n_ext = len(eorder) # = 2L + E + 6
408
+
409
+ blob = bytearray()
410
+ blob += MAGIC
411
+ blob += struct.pack("<B", nblk)
412
+ for slot in border:
413
+ w = _get(sd, slot, bkm)
414
+ w2d = _conv_w_to_2d(w, slot)
415
+ blob += _pack_conv_block(w2d)
416
+ blob += CLMX
417
+ blob += struct.pack("<B", n_ext)
418
+ for slot in eorder:
419
+ blob += _pack_ext(_get(sd, slot, ekm))
420
+ return blob
421
+
422
+
423
+ def _bget(sd: Dict[str, Any], names) -> "np.ndarray":
424
+ """Fetch the first present key among `names` from a (torch- or logical-keyed)
425
+ state dict, coerced to a float32 numpy array. Accepts a 'base.'-stripped dict."""
426
+ for nm in names:
427
+ if nm in sd:
428
+ return _to_np(sd[nm])
429
+ raise KeyError(f"none of {names} present (have: {list(sd.keys())[:16]}...)")
430
+
431
+
432
+ def serialize_v3_bind(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
433
+ readout_type: int, out_path: str) -> str:
434
+ """Pack a BIND-readout CLMConvMoE (the EXP-3 ARM-BIND architecture:
435
+ BindCLM = production trunk + Hadamard byte readout u=Wa(x), v=Wb(x),
436
+ g=u*v (readout_type=1) or u+v (readout_type=2), logits=Wo(g)) to a CLM\\x01
437
+ .clm that core/decode.hexa's CONV mouth loads.
438
+
439
+ BYTE LAYOUT (backward-compatible, in-place extension — no magic bump):
440
+ · MAIN body == serialize_v3, EXCEPT the readout slot roW carries **Wo**
441
+ (cout=V, rest=k) and roB carries **Wo.bias** (V). All trunk/embed/MoE
442
+ blocks+ext are IDENTICAL to the additive ctrl arm (only the readout
443
+ differs — the EXP-3 design intent). The self-describing (d,E,V,L,K)
444
+ recovery is UNCHANGED (E=block[nblk-2].cout, V=block[nblk-1].cout, the
445
+ readout block's `rest` field = k).
446
+ · a CLMB trailer is appended AFTER the CLMX ext arrays:
447
+ "CLMB" (67,76,77,66)
448
+ readout_type:u8 (1=Hadamard u*v, 2=linear u+v)
449
+ Wa conv block (cout=k, rest=d, int4-sym + per-channel fp32 scale)
450
+ Wb conv block (cout=k, rest=d, int4-sym + per-channel fp32 scale)
451
+ Wa_bias ext (u32 k + k*f32)
452
+ Wb_bias ext (u32 k + k*f32)
453
+ An additive .clm has NO CLMB section, so the decoder defaults
454
+ readout_type=0 (a_engine_native_learning backward-compat: existing
455
+ additive .clm decode byte-identically).
456
+
457
+ Accepts a torch state_dict (BindCLM.state_dict(): base.<trunk...> + Wa/Wb/Wo
458
+ .{weight,bias}) OR a logical dict (Wa,WaB,Wb,WbB,Wo,WoB + v3 slot names).
459
+ Returns out_path."""
460
+ if np is None:
461
+ raise RuntimeError("numpy is required for serialize_v3_bind")
462
+ L, E = int(n_trunk_layers), int(n_experts)
463
+ rt = int(readout_type)
464
+ if L < 1 or E < 1:
465
+ raise ValueError(f"need L>=1 and E>=1, got L={L} E={E}")
466
+ if rt not in (RO_BIND_HADAMARD, RO_BIND_LINEAR):
467
+ raise ValueError(f"readout_type must be 1 (hadamard) or 2 (linear), got {rt}")
468
+ sd = _resolve_state_dict(state_dict_or_ckpt, None)
469
+
470
+ # normalize: strip BindCLM's 'base.' trunk prefix into bare CLMConvMoE keys.
471
+ norm: Dict[str, Any] = {}
472
+ for k, v in sd.items():
473
+ nk = k[5:] if k.startswith("base.") else k
474
+ norm[nk] = v
475
+
476
+ # bind readout weights (Wa/Wb -> CLMB ; Wo -> routed into the roW/roB slots).
477
+ Wa = _bget(norm, ["Wa.weight", "Wa"])
478
+ WaB = _bget(norm, ["Wa.bias", "WaB"])
479
+ Wb = _bget(norm, ["Wb.weight", "Wb"])
480
+ WbB = _bget(norm, ["Wb.bias", "WbB"])
481
+ Wo = _bget(norm, ["Wo.weight", "Wo"])
482
+ WoB = _bget(norm, ["Wo.bias", "WoB"])
483
+ norm["readout.weight"] = Wo # (V, k, 1) -> roW block (cout=V, rest=k)
484
+ norm["readout.bias"] = WoB # (V,) -> roB ext
485
+
486
+ blob = _pack_main_blob(norm, L, E)
487
+ blob += CLMB
488
+ blob += struct.pack("<B", rt)
489
+ blob += _pack_conv_block(_conv_w_to_2d(Wa, "Wa")) # (k, d)
490
+ blob += _pack_conv_block(_conv_w_to_2d(Wb, "Wb")) # (k, d)
491
+ blob += _pack_ext(WaB) # (k,)
492
+ blob += _pack_ext(WbB) # (k,)
493
+
494
+ with open(out_path, "wb") as f:
495
+ f.write(blob)
496
+ return out_path
497
+
498
+
499
+ def serialize_clm(state_dict_or_ckpt, n_trunk_layers: int, n_experts: int,
500
+ out_path: str) -> str:
501
+ """Unified CLM entry: routes to serialize_v3 (general). For L=1,E=2 the v3 path
502
+ is byte-identical to serialize_v2, so this is the single CLM serializer for ANY
503
+ (L,E,d). cfg-free — caller states (L,E) explicitly.
504
+
505
+ NAME NOTE (union dispatch): in the standalone clm_serialize_v2.py this was the
506
+ bare `serialize`; on the unified module the bare `serialize` binds the ByteGPT
507
+ .pt→.bin bridge (BGS.serialize contract), so the CLM unified entry lives here
508
+ as `serialize_clm`. No live importer called the CLM bare `serialize` (grep-
509
+ verified); all live callers use `serialize_v3` directly."""
510
+ return serialize_v3(state_dict_or_ckpt, n_trunk_layers, n_experts, out_path)
511
+
512
+
513
+ # ════════════════════════════════════════════════════════════════════════
514
+ # (c) BYTE (ByteGPT) — verbatim port of tool/bytegpt_serialize.py. torch .pt →
515
+ # engine .bin (5×u32 header) BRIDGE. Ground-truth layout = core/decode.hexa
516
+ # BYTE mouth (bg_load / bytegpt_forward_last).
517
+ #
518
+ # header 5x u32 little-endian: [vocab, d, n_layer, n_head, block]
519
+ # tok[vocab*d] pos[block*d]
520
+ # per layer: ln1.w[d] ln1.b[d] in_proj.w[3d*d] in_proj.b[3d]
521
+ # out_proj.w[d*d] out_proj.b[d] ln2.w[d] ln2.b[d]
522
+ # mlp0.w[4d*d] mlp0.b[4d] mlp2.w[d*4d] mlp2.b[d]
523
+ # ln_f.w[d] ln_f.b[d] head[vocab*d] (all little-endian float32.)
524
+ #
525
+ # WEIGHT ORIENTATION: torch nn.Linear.weight is [out,in] row-major; the engine's
526
+ # _bg_linear ALSO stores W as [Co,Ci] row-major and transposes at load, so torch's
527
+ # native [out,in] is written VERBATIM (NO transpose here). in_proj_weight [3d,d] is
528
+ # rows Q|K|V — exactly what _bg_mha expects (NO reordering).
529
+ # ════════════════════════════════════════════════════════════════════════
530
+
531
+ def _f32le(t) -> bytes:
532
+ """Flatten a torch tensor ROW-MAJOR (C-contiguous) to little-endian float32 bytes."""
533
+ import torch # training family — torch OK here (.pt bridge)
534
+ a = t.detach().cpu().to(torch.float32).contiguous().numpy()
535
+ a = np.ascontiguousarray(a, dtype="<f4") # little-endian float32, C order
536
+ return a.tobytes()
537
+
538
+
539
+ def serialize(pt_path: str, bin_path: str) -> None:
540
+ """ByteGPT .pt (cfg+state_dict) → engine .bin BRIDGE (BGS.serialize contract).
541
+
542
+ torch is imported here (the .pt bridge is LEARNING-side, a_clm_gen_pipeline) — this
543
+ is NOT the verdict scorer; the verdict is the engine `anima evaluate <bin>` (generator
544
+ L3 → core/decode.hexa BYTE mouth)."""
545
+ import torch # training family — torch OK here (.pt bridge)
546
+ ck = torch.load(pt_path, map_location="cpu", weights_only=False)
547
+ sd = ck["model"]
548
+ cfg = ck["config"]
549
+ vocab, d, n_layer, n_head, block = (
550
+ int(cfg["vocab"]), int(cfg["d"]), int(cfg["n_layer"]),
551
+ int(cfg["n_head"]), int(cfg["block"]),
552
+ )
553
+ print(f"[cfg] vocab={vocab} d={d} n_layer={n_layer} n_head={n_head} block={block}", flush=True)
554
+ print(f"[cfg] val_ce={ck.get('val_ce')} step={ck.get('step')} nparam={ck.get('nparam')}", flush=True)
555
+
556
+ def W(key, shape):
557
+ if key not in sd:
558
+ raise KeyError(f"missing state_dict key: {key}")
559
+ t = sd[key]
560
+ if tuple(t.shape) != tuple(shape):
561
+ raise ValueError(f"{key} shape {tuple(t.shape)} != expected {tuple(shape)}")
562
+ return _f32le(t)
563
+
564
+ out = bytearray()
565
+ # header
566
+ out += struct.pack("<5I", vocab, d, n_layer, n_head, block)
567
+ # embeddings
568
+ out += W("tok.weight", (vocab, d))
569
+ out += W("pos.weight", (block, d))
570
+ # per layer
571
+ for i in range(n_layer):
572
+ p = f"blocks.{i}."
573
+ out += W(p + "ln1.weight", (d,))
574
+ out += W(p + "ln1.bias", (d,))
575
+ out += W(p + "attn.in_proj_weight", (3 * d, d))
576
+ out += W(p + "attn.in_proj_bias", (3 * d,))
577
+ out += W(p + "attn.out_proj.weight", (d, d))
578
+ out += W(p + "attn.out_proj.bias", (d,))
579
+ out += W(p + "ln2.weight", (d,))
580
+ out += W(p + "ln2.bias", (d,))
581
+ out += W(p + "mlp.0.weight", (4 * d, d))
582
+ out += W(p + "mlp.0.bias", (4 * d,))
583
+ out += W(p + "mlp.2.weight", (d, 4 * d))
584
+ out += W(p + "mlp.2.bias", (d,))
585
+ # final norm + tied head
586
+ out += W("ln_f.weight", (d,))
587
+ out += W("ln_f.bias", (d,))
588
+ head_key = "head.weight" if "head.weight" in sd else "tok.weight" # tied
589
+ out += W(head_key, (vocab, d))
590
+
591
+ # expected size check
592
+ per_layer = (d + d) + (3 * d * d + 3 * d) + (d * d + d) + (d + d) + \
593
+ (4 * d * d + 4 * d) + (d * 4 * d + d)
594
+ expected = 20 + (vocab * d + block * d) * 4 + n_layer * per_layer * 4 + \
595
+ (d + d + vocab * d) * 4
596
+ if len(out) != expected:
597
+ raise AssertionError(f"size mismatch: wrote {len(out)} expected {expected}")
598
+
599
+ with open(bin_path, "wb") as f:
600
+ f.write(out)
601
+ print(f"[ok] wrote {bin_path} bytes={len(out)} (expected {expected})", flush=True)
602
+
603
+
604
+ # alias — the ByteGPT bridge under a mouth-explicit name (parallels serialize_clm).
605
+ bytegpt_serialize = serialize
606
+
607
+
608
+ # ════════════════════════════════════════════════════════════════════════
609
+ # (c2) BYTE injected-bind (BGB) — base ByteGPT .bin (verbatim) + N appended
610
+ # GATED transformer blocks -> engine .bin with a "BGB\x01" trailer. The
611
+ # ByteGPT analogue of the CONV "CLMB" bind-readout extension: the base
612
+ # bytes are copied UNCHANGED and the trailer is appended after `head`, so a
613
+ # gate=0 file decodes byte-identically to its base (core/decode.py bg_load
614
+ # reads the optional trailer; W["bind"]=[] when absent -> zero regression).
615
+ #
616
+ # BGB trailer (after head): magic 66,71,66,1 ; u32 n_bind ;
617
+ # per bind block: the SAME 12 param tensors as a base layer in the SAME
618
+ # order/layout as `serialize` above (ln1.w[d] ln1.b[d] in_proj.w[3d,d]
619
+ # in_proj.b[3d] out_proj.w[d,d] out_proj.b[d] ln2.w[d] ln2.b[d]
620
+ # mlp0.w[4d,d] mlp0.b[4d] mlp2.w[d,4d] mlp2.b[d], all LE f32 VERBATIM —
621
+ # torch native [out,in], NO transpose) ; then one LE f32 `gate`.
622
+ #
623
+ # Reading the injected torch .pt (unpickle) is the only torch touch (training
624
+ # family, a_clm_gen_pipeline / a_engine_native_learning: serializer may use torch).
625
+ # The injected .pt carries a standard transformer Block state_dict + a scalar gate
626
+ # per appended block (BindAttnByteGPT: self.bind=Block(...), self.gate).
627
+ # ════════════════════════════════════════════════════════════════════════
628
+
629
+ BGB = bytes([66, 71, 66, 1]) # "BGB\x01" — ByteGPT injected-bind trailer
630
+
631
+
632
+ def _bfind(bsd, suffix):
633
+ """Resolve a Block state_dict tensor by key SUFFIX (robust to a 'bind.'/module
634
+ prefix). Exact match first, else the unique / shortest suffix match."""
635
+ if suffix in bsd:
636
+ return bsd[suffix]
637
+ cands = [k for k in bsd if k.endswith(suffix)]
638
+ if not cands:
639
+ raise KeyError("bind block missing key *" + suffix + " (keys=" + ",".join(sorted(bsd)) + ")")
640
+ cands.sort(key=len)
641
+ return bsd[cands[0]]
642
+
643
+
644
+ def _bind_block_bytes(bsd, d):
645
+ """Map ONE torch Block state_dict -> BGB block bytes (12 tensors, bg_load order,
646
+ VERBATIM f32le — same orientation as `serialize`'s per-layer write, no transpose)."""
647
+ def w(name, shape):
648
+ t = _bfind(bsd, name)
649
+ if tuple(t.shape) != tuple(shape):
650
+ raise ValueError(f"bind {name} shape {tuple(t.shape)} != expected {tuple(shape)}")
651
+ return _f32le(t)
652
+ out = bytearray()
653
+ out += w("ln1.weight", (d,))
654
+ out += w("ln1.bias", (d,))
655
+ out += w("attn.in_proj_weight", (3 * d, d))
656
+ out += w("attn.in_proj_bias", (3 * d,))
657
+ out += w("attn.out_proj.weight", (d, d))
658
+ out += w("attn.out_proj.bias", (d,))
659
+ out += w("ln2.weight", (d,))
660
+ out += w("ln2.bias", (d,))
661
+ out += w("mlp.0.weight", (4 * d, d))
662
+ out += w("mlp.0.bias", (4 * d,))
663
+ out += w("mlp.2.weight", (d, 4 * d))
664
+ out += w("mlp.2.bias", (d,))
665
+ return bytes(out)
666
+
667
+
668
+ def _is_block_sd(x):
669
+ """True if x is a single Block state_dict (has an in_proj_weight-suffixed key)."""
670
+ return isinstance(x, dict) and any(str(k).endswith("in_proj_weight") for k in x)
671
+
672
+
673
+ def _scalar(g):
674
+ """Coerce a gate (python number / 0-d or 1-elem torch tensor / list) -> float."""
675
+ if hasattr(g, "detach"):
676
+ g = g.detach().cpu().reshape(-1)
677
+ return float(g[0])
678
+ if isinstance(g, (list, tuple)):
679
+ return float(g[0])
680
+ return float(g)
681
+
682
+
683
+ def _normalize_bind_list(binds, gates):
684
+ """Return an ordered list of (block_state_dict, gate_float). Accepts:
685
+ * single Block state_dict + scalar gate -> [(sd, g)]
686
+ * list of Block state_dicts + list|scalar of gates -> zipped
687
+ * dict{idx: Block state_dict} + dict|list|scalar gates -> index-ordered."""
688
+ if _is_block_sd(binds):
689
+ return [(binds, _scalar(gates))]
690
+ if isinstance(binds, (list, tuple)):
691
+ n = len(binds)
692
+ gl = gates if isinstance(gates, (list, tuple)) else [gates] * n
693
+ return [(binds[i], _scalar(gl[i])) for i in range(n)]
694
+ if isinstance(binds, dict):
695
+ keys = sorted(binds, key=lambda k: (int(k) if str(k).isdigit() else 1 << 30, str(k)))
696
+ out = []
697
+ for i, k in enumerate(keys):
698
+ if isinstance(gates, dict):
699
+ g = gates[k]
700
+ elif isinstance(gates, (list, tuple)):
701
+ g = gates[i]
702
+ else:
703
+ g = gates
704
+ out.append((binds[k], _scalar(g)))
705
+ return out
706
+ raise TypeError("unrecognized 'bind' payload type: " + str(type(binds)))
707
+
708
+
709
+ def serialize_bind(base_bin: str, injected_pt: str, out_bin: str) -> str:
710
+ """base ByteGPT .bin (bytes VERBATIM) + injected torch .pt -> engine .bin + BGB
711
+ trailer. injected_pt = {"bind": <Block state_dict | list | dict>, "gate":
712
+ <scalar | list | dict>, "config": {...}}. Supports N>=1 appended blocks.
713
+
714
+ torch is imported ONLY to unpickle the .pt (irreducible); the byte layout is
715
+ reference-matched to `serialize` (per-layer write) + core/decode.py bg_load."""
716
+ import torch # training family — torch OK here (.pt bridge)
717
+ base = open(base_bin, "rb").read()
718
+ if len(base) < 20:
719
+ raise ValueError("base .bin too small (no 5xu32 header): " + base_bin)
720
+ vocab, d, n_layer, n_head, block = struct.unpack("<5I", base[:20])
721
+ # sanity: reject a CLM base (CLM\x01 magic) — BGB rides ByteGPT only.
722
+ if base[0] == 67 and base[1] == 76 and base[2] == 77 and base[3] == 1:
723
+ raise ValueError("base is a CLM .clm, not a ByteGPT .bin: " + base_bin)
724
+
725
+ ck = torch.load(injected_pt, map_location="cpu", weights_only=False)
726
+ if not (isinstance(ck, dict) and "bind" in ck and "gate" in ck):
727
+ raise KeyError("injected .pt must have keys {'bind','gate'}; got " + str(list(ck)[:8]))
728
+ blocks = _normalize_bind_list(ck["bind"], ck["gate"])
729
+ if len(blocks) < 1:
730
+ raise ValueError("injected .pt carried 0 bind blocks")
731
+
732
+ trailer = bytearray()
733
+ trailer += BGB
734
+ trailer += struct.pack("<I", len(blocks))
735
+ for bsd, gate in blocks:
736
+ bsd = {k: (v.detach().cpu() if hasattr(v, "detach") else v) for k, v in bsd.items()}
737
+ trailer += _bind_block_bytes(bsd, d)
738
+ trailer += struct.pack("<f", float(gate))
739
+
740
+ with open(out_bin, "wb") as f:
741
+ f.write(base)
742
+ f.write(bytes(trailer))
743
+ print(f"[ok] wrote {out_bin} base={len(base)} + BGB(n_bind={len(blocks)},d={d})"
744
+ f" trailer={len(trailer)} = {len(base) + len(trailer)} bytes"
745
+ f" gates={[round(g, 6) for _, g in blocks]}", flush=True)
746
+ return out_bin
747
+
748
+
749
+ # ════════════════════════════════════════════════════════════════════════
750
+ # (d.inv) ByteGPT .bin → torch state_dict INVERSE — warm-start reader.
751
+ # The exact byte-for-byte inverse of serialize() above (same 5×u32 header +
752
+ # flat little-endian float32 tensor order). Used by `anima-python train --init
753
+ # <base.bin>` to warm-start a fresh ByteGPT from a trained engine .bin. The
754
+ # byte grammar SSOT stays in THIS file (mirror of serialize's write order).
755
+ # ════════════════════════════════════════════════════════════════════════
756
+
757
+ def deserialize_bytegpt(bin_path: str) -> "Tuple[Dict[str, Any], Dict[str, int]]":
758
+ """Read a ByteGPT engine `.bin` (5×u32 header) back into a torch state_dict.
759
+
760
+ Returns (state_dict, cfg) where state_dict has the EXACT keys ByteGPT.state_dict()
761
+ emits (tok/pos/blocks.{i}.*/ln_f/head — head==tok since the head is tied) and cfg =
762
+ {vocab,d,n_layer,n_head,block}. This is the byte-inverse of serialize(): it walks the
763
+ same tensor order and reshapes each little-endian float32 slice to torch's native
764
+ [out,in] orientation (serialize wrote torch's layout VERBATIM, so no transpose)."""
765
+ import torch # training/warm-start family — torch OK here (.bin bridge, a_clm_gen_pipeline)
766
+ rb = open(bin_path, "rb").read() if not isinstance(bin_path, (bytes, bytearray)) else bin_path
767
+ if len(rb) < 20:
768
+ raise ValueError(f"deserialize_bytegpt: {bin_path} too short ({len(rb)}B) for a 5×u32 header")
769
+ vocab, d, n_layer, n_head, block = struct.unpack_from("<5I", rb, 0)
770
+ off = 20
771
+
772
+ def rd(shape) -> "torch.Tensor":
773
+ nonlocal off
774
+ n = 1
775
+ for s in shape:
776
+ n *= s
777
+ arr = np.frombuffer(rb, dtype="<f4", count=n, offset=off).astype(np.float32)
778
+ off += 4 * n
779
+ return torch.from_numpy(np.ascontiguousarray(arr).reshape(shape))
780
+
781
+ sd: Dict[str, Any] = {}
782
+ sd["tok.weight"] = rd((vocab, d))
783
+ sd["pos.weight"] = rd((block, d))
784
+ for i in range(n_layer):
785
+ p = f"blocks.{i}."
786
+ sd[p + "ln1.weight"] = rd((d,))
787
+ sd[p + "ln1.bias"] = rd((d,))
788
+ sd[p + "attn.in_proj_weight"] = rd((3 * d, d))
789
+ sd[p + "attn.in_proj_bias"] = rd((3 * d,))
790
+ sd[p + "attn.out_proj.weight"] = rd((d, d))
791
+ sd[p + "attn.out_proj.bias"] = rd((d,))
792
+ sd[p + "ln2.weight"] = rd((d,))
793
+ sd[p + "ln2.bias"] = rd((d,))
794
+ sd[p + "mlp.0.weight"] = rd((4 * d, d))
795
+ sd[p + "mlp.0.bias"] = rd((4 * d,))
796
+ sd[p + "mlp.2.weight"] = rd((d, 4 * d))
797
+ sd[p + "mlp.2.bias"] = rd((d,))
798
+ sd["ln_f.weight"] = rd((d,))
799
+ sd["ln_f.bias"] = rd((d,))
800
+ sd["head.weight"] = rd((vocab, d)) # tied — equals tok.weight in a serialized model
801
+ if off != len(rb):
802
+ raise AssertionError(
803
+ f"deserialize_bytegpt: consumed {off}B but file is {len(rb)}B "
804
+ f"(header cfg vocab={vocab} d={d} n_layer={n_layer} n_head={n_head} block={block} "
805
+ f"— corrupt or wrong-arch .bin)")
806
+ cfg = {"vocab": vocab, "d": d, "n_layer": n_layer, "n_head": n_head, "block": block}
807
+ return sd, cfg
808
+
809
+
810
+ # ════════════════════════════════════════════════════════════════════════
811
+ # (e) FORMAT DISPATCH — pick the mouth serializer by target extension. Convenience
812
+ # only; the existing entry points (serialize_v3 / serialize) are UNRENAMED.
813
+ # ════════════════════════════════════════════════════════════════════════
814
+
815
+ def serialize_auto(src, out_path: str, *, n_trunk_layers: int = None,
816
+ n_experts: int = None):
817
+ """Dispatch by target extension: '.bin' → ByteGPT bridge (src=.pt path,
818
+ serialize(pt,bin)); else → CLM serialize_v3 (src=state_dict/ckpt, needs
819
+ n_trunk_layers/n_experts). The two mouth serializers have different input
820
+ contracts (ByteGPT reads a .pt PATH; CLM takes a state_dict + (L,E)), so this
821
+ only routes — it does not unify the signatures."""
822
+ if str(out_path).endswith(".bin"):
823
+ return serialize(src, out_path)
824
+ if n_trunk_layers is None or n_experts is None:
825
+ raise ValueError("CLM serialize_auto needs n_trunk_layers + n_experts")
826
+ return serialize_v3(src, n_trunk_layers, n_experts, out_path)