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,186 @@
1
+ # ==========================================================================
2
+ # ⛔ ENGINE-INTERNAL TOOL — agent 가 직접 타이핑 금지. 학습/직렬화는 cli/ 단일진입만:
3
+ # anima train | anima serialize (canonical=hexa cli/{train,serialize}.hexa).
4
+ # 단, cli/serialize.hexa 가 torch .pt→.clm v0.3 bridge 를 `python3 … serialize_standalone.py …` 로
5
+ # SUBPROCESS 호출한다(=canonical 경로의 정당한 내부 shell-out) — 그래서 __main__ hard-exit 가드는
6
+ # 두지 않는다(그건 canonical serialize 를 깨뜨림). agent 직접실행 차단은 .harness/enforcement.json
7
+ # H-ANIMA-SINGLE-ENTRY pre_bash 룰이 담당(hexa 내부 subprocess 는 안 건드림). #2603
8
+ # ==========================================================================
9
+ #!/usr/bin/env python3
10
+ # serialize.py — anima STANDALONE re-serialize entry (.pt → .clm v0.3 + held-out gate).
11
+ #
12
+ # WHY THIS FILE (recovery/re-export · a_clm_gen_pipeline): `anima train` ALREADY
13
+ # auto-serializes its trained weights to .clm v0.3 and runs the held-out mirror-DESCENT
14
+ # gate at the end of a run (cli/train.py / cli/train.hexa post-serialize block). This
15
+ # standalone command exists for the SEPARATE case: re-exporting an ALREADY-TRAINED torch
16
+ # .pt checkpoint (recovery, re-export, re-verification) without re-training.
17
+ #
18
+ # REFERENCE-MATCH (NOT a reimplementation): the byte layout comes from the GROUND-TRUTH
19
+ # unified serializer core/serialize.py::serialize_v3 (the SAME byte grammar core/decode.hexa
20
+ # parses and the golden reexport_d768_v2_fast.clm uses), and the held-out gate is the KEPT
21
+ # torch-interop sibling verify_clm_v2.py descent (math.log mirror, dt_ln-immune). This file
22
+ # only loads the .pt, derives L/E from the state-dict keys, and calls those two backends —
23
+ # it invents no new layout and no new gate.
24
+ #
25
+ # TORCH IS ALLOWED HERE (training family): unlike cli/evaluate.py (torch-free
26
+ # measurement) this is the LEARNING-side re-export and must read a torch .pt. The gate
27
+ # enforcer's torch grep targets the measurement surfaces (evaluate / anima dispatch), not
28
+ # the trainer/serializer family (a_engine_native_learning: train/ may use torch).
29
+ #
30
+ # USAGE (installed `anima` PATH command after `hx install anima`)
31
+ # anima serialize <ckpt.pt> <out.clm> [--heldout <path>] [--train <path>] [--nwin N]
32
+ # <ckpt.pt> torch state_dict (or {"state_dict": ...}/{"model": ...}) checkpoint
33
+ # <out.clm> output .clm v0.3 path (engine-loadable, CLM magic)
34
+ # --heldout held-out byte corpus for the post-serialize mirror-DESCENT gate
35
+ # (a_clm_gen_pipeline: structure round-trip is NOT enough)
36
+ # --train optional train corpus (gap-vs-heldout overfit advisory)
37
+ # --nwin N descent window length (default 64)
38
+
39
+ import os
40
+ import sys
41
+
42
+ _HERE = os.path.dirname(os.path.abspath(__file__))
43
+ # verify_clm_v2.py (held-out DESCENT gate) is a KEPT torch-interop SIBLING in this same
44
+ # train/clm/model/ dir. The serializer itself is now the UNIFIED core/serialize.py (CLM
45
+ # serialize_v3 + ByteGPT bridge, parallel to core/decode.py) — add core/ so `import
46
+ # serialize` resolves. core/ = <repo>/core; from …/archive/train/clm/model/ walk up 4 to
47
+ # the repo root (…/archive/train/clm/model → …/archive/train/clm → …/archive/train →
48
+ # …/archive → repo). If that core/ is absent (staged pod), fall back to _HERE.
49
+ sys.path.insert(0, _HERE)
50
+ _REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(_HERE))))
51
+ _CORE = os.path.join(_REPO, "core")
52
+ if os.path.isdir(_CORE) and _CORE not in sys.path:
53
+ sys.path.insert(0, _CORE)
54
+
55
+
56
+ def serialize_usage():
57
+ """Print the canonical usage banner (installed `anima serialize` command form)."""
58
+ print("anima serialize — re-export a torch .pt to .clm v0.3 + held-out DESCENT gate.")
59
+ print("")
60
+ print("usage:")
61
+ print(" anima serialize <ckpt.pt> <out.clm> [--heldout <path>] [--train <path>] [--nwin N]")
62
+ print("")
63
+ print(" re-serialize an ALREADY-TRAINED torch checkpoint to an engine-loadable .clm")
64
+ print(" (ground-truth bridge serialize_v3) and verify it models held-out text")
65
+ print(" (verify_clm_v2 mirror-DESCENT gate). `anima train` does this automatically;")
66
+ print(" this command is for recovery / re-export of an existing .pt.")
67
+
68
+
69
+ def _strval(argv, flag, dflt):
70
+ i = 0
71
+ while i < len(argv):
72
+ if argv[i] == flag and i + 1 < len(argv):
73
+ return argv[i + 1]
74
+ i += 1
75
+ return dflt
76
+
77
+
78
+ def _intval(argv, flag, dflt):
79
+ v = _strval(argv, flag, None)
80
+ return int(v) if v is not None else dflt
81
+
82
+
83
+ def _derive_LE(sd):
84
+ """Derive (L trunk layers, E experts) from the state-dict keys (reference-match
85
+ of serialize_v2._general_block_keymap naming: trunk.{i}.* and moe.experts.{j}.*)."""
86
+ L = 0
87
+ E = 0
88
+ for k in sd.keys():
89
+ if k.startswith("trunk.") and k.endswith(".conv.conv.weight"):
90
+ try:
91
+ L = max(L, int(k.split(".")[1]) + 1)
92
+ except (ValueError, IndexError):
93
+ pass
94
+ if k.startswith("moe.experts.") and k.endswith(".conv.conv.weight"):
95
+ try:
96
+ E = max(E, int(k.split(".")[2]) + 1)
97
+ except (ValueError, IndexError):
98
+ pass
99
+ return L, E
100
+
101
+
102
+ def serialize_run(argv):
103
+ if len(argv) < 2:
104
+ serialize_usage()
105
+ return 2
106
+
107
+ pt = argv[0]
108
+ out = argv[1]
109
+ heldout = _strval(argv[2:], "--heldout", None)
110
+ train_corpus = _strval(argv[2:], "--train", None)
111
+ nwin = _intval(argv[2:], "--nwin", 64)
112
+
113
+ if not os.path.exists(pt):
114
+ print("ERROR: torch checkpoint not found: " + pt)
115
+ return 2
116
+
117
+ import torch # training family — torch OK here
118
+ import serialize as S # core/serialize.py — serialize_v3 = bridge SSOT
119
+ import verify_clm_v2 as VC # clm_decodable / descent (this dir)
120
+
121
+ print("=== anima serialize — .pt → .clm v0.3 (ground-truth bridge) ===")
122
+ print("pt: " + pt)
123
+ print("out: " + out)
124
+
125
+ raw = torch.load(pt, map_location="cpu")
126
+ # normalize to a flat state_dict (mirror serialize_v2._resolve_state_dict inputs).
127
+ if isinstance(raw, dict) and "state_dict" in raw and isinstance(raw["state_dict"], dict):
128
+ sd = raw["state_dict"]
129
+ elif isinstance(raw, dict) and "model" in raw and isinstance(raw["model"], dict):
130
+ sd = raw["model"]
131
+ else:
132
+ sd = raw
133
+ sd = {k: (v.detach().cpu() if hasattr(v, "detach") else v) for k, v in sd.items()}
134
+
135
+ L, E = _derive_LE(sd)
136
+ if L < 1 or E < 1:
137
+ print("ERROR: could not derive L/E from state-dict keys "
138
+ "(expected trunk.{i}.conv.conv.weight / moe.experts.{j}.conv.conv.weight). "
139
+ "Is this an anima CLMConvMoE checkpoint?")
140
+ return 2
141
+ print("derived: L(trunk)=" + str(L) + " E(experts)=" + str(E))
142
+
143
+ S.serialize_v3(sd, n_trunk_layers=L, n_experts=E, out_path=out)
144
+ nbytes = os.path.getsize(out)
145
+ print("WRITTEN " + str(nbytes) + " bytes -> " + out)
146
+
147
+ # ── CORE-loadable self-check (mirror of core/clm_decode.hexa) ────────────
148
+ rb = open(out, "rb").read()
149
+ decodable = VC.clm_decodable(rb)
150
+ print("")
151
+ print("=== CORE-loadable self-check (mirror of core/clm_decode.hexa) ===")
152
+ print(" clm_decodable = " + str(decodable))
153
+ if not decodable:
154
+ print("FAIL — serialized .clm is NOT CORE-loadable (see above).")
155
+ return 1
156
+
157
+ # ── held-out mirror-DESCENT gate (a_clm_gen_pipeline) ────────────────────
158
+ if heldout:
159
+ if not os.path.exists(heldout):
160
+ print("ERROR: --heldout corpus not found: " + heldout)
161
+ return 2
162
+ print("")
163
+ print("post-serialize held-out DESCENT gate (verify_clm_v2 mirror, dt_ln-immune):")
164
+ rc = VC.run_descent_gate_cli(out, heldout, train_corpus, nwin)
165
+ if rc != 0:
166
+ print(" ⛔ DESCENT GATE FAIL — serialized .clm broken/overfit; do NOT mark "
167
+ "done / HF-upload (a_clm_gen_pipeline).")
168
+ return 1
169
+ print(" ✅ DESCENT GATE PASS — serialized .clm models held-out text.")
170
+ else:
171
+ print("")
172
+ print("NOTE: no --heldout corpus given — structure round-trip only. The .clm is "
173
+ "CORE-loadable but its held-out predictive descent is UNVERIFIED "
174
+ "(a_clm_gen_pipeline: decodable != generalizing). Pass --heldout to gate it.")
175
+ return 0
176
+
177
+
178
+ def main(argv):
179
+ if len(argv) >= 1 and argv[0] in ("-h", "--help"):
180
+ serialize_usage()
181
+ return 0
182
+ return serialize_run(argv)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ sys.exit(main(sys.argv[1:]) or 0)
anima_py/core/slw.py ADDED
@@ -0,0 +1,211 @@
1
+ """core/slw.py — H_9200 E1 gated-write forward-slot (SLW), CORE-owned SSOT.
2
+
3
+ anima's next-byte = fn( (a)CE-trained · (b)feed-forward · (c)single-trunk ). The
4
+ additive combination operator `cbind` (order-BLIND sum of two concept reps) is the
5
+ proven ρ·weave recombination wall (was G1 · H_1129): two concepts farther apart than the conv receptive
6
+ field are mathematically independent -> cannot recombine. SLW replaces the additive
7
+ cbind with a content-addressed slot memory written/read causally on the post-norm
8
+ penultimate before readout -- an ASYMMETRIC write(address)/read(key) that attacks
9
+ premise (b). Because the slot state S lives OUTSIDE the token stream, a write at
10
+ position i is read at any j>i with zero dependence on j-i, so it reaches across the
11
+ D>RF independence wall. See state/9200_e1_303m/E1_SLW_module_spec.md.
12
+
13
+ CORE-owned (owner directive: core-related lives in core/). ONE file holds all three
14
+ faces of the SLW so nothing drifts:
15
+ * slot_apply() -- torch-free NUMPY inference mirror. This is the byte-parity
16
+ partner of the engine `core/decode.hexa` _slot_apply, and the
17
+ `anima-python evaluate` (a_eval_py_canonical) TERMINAL path calls
18
+ it. Import stays torch-free so the inference pod needs no torch.
19
+ * pack_slw() / read_slw() -- the "SLW\\x01" trailer codec (used by
20
+ core/serialize.py on write, by the numpy + hexa loaders on read).
21
+ * SLWModule -- the torch training module (DIRECTIONAL). Defined only when torch
22
+ is importable, so `import slw` for inference never pulls torch.
23
+
24
+ γ=0 => exact passthrough (clean slot-ablation control). shuffle_perm permutes the
25
+ WRITE address only (reads unpermuted) => shuffle-bind control. Both are pure
26
+ eval-time switches on serialized state (no retraining -> no tune-to-green surface).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import struct
32
+ import numpy as np
33
+
34
+ # ── "SLW\x01" trailer magic (mirrors the BGB/CLMB trailer convention) ──────────
35
+ SLW_MAGIC = bytes([83, 76, 87, 1]) # "SLW\x01"
36
+
37
+
38
+ # --------------------------------------------------------------------------- #
39
+ # (a) numpy inference mirror — torch-free, byte-parity with core/decode.hexa
40
+ # --------------------------------------------------------------------------- #
41
+ def _softmax(z: np.ndarray) -> np.ndarray:
42
+ z = z - z.max()
43
+ e = np.exp(z)
44
+ return e / e.sum()
45
+
46
+
47
+ def _sigmoid(x: float) -> float:
48
+ return 1.0 / (1.0 + np.exp(-x))
49
+
50
+
51
+ def slot_apply(x, slw, gamma=None, shuffle_perm=None):
52
+ """Apply the SLW forward-slot to a penultimate sequence.
53
+
54
+ x: (T, d) float32 post-norm penultimate (per sequence; caller loops batch).
55
+ slw: dict from read_slw (K_slots, W_r/b_r, W_q/b_q, W_v/b_v, W_o/b_o, w_g/b_g,
56
+ gamma, n_slot, k, d_s), all numpy float32.
57
+ gamma: override the stored γ (pass 0.0 for the --slot-off ablation control).
58
+ shuffle_perm: length-n_slot int permutation applied to the WRITE address only
59
+ (reads unpermuted) for the --slot-shuffle control; None = identity.
60
+
61
+ Returns (T, d): x + γ·(W_o·m_t + b_o) per position. Op order is IDENTICAL to the
62
+ torch SLWModule.forward and to core/decode.hexa _slot_apply (2-production parity).
63
+ """
64
+ x = np.asarray(x)
65
+ dt = x.dtype # keep the caller's precision (decode.py
66
+ T, d = x.shape # is float64 for determinism; f32 weights
67
+ n_slot, k, d_s = slw["n_slot"], slw["k"], slw["d_s"] # promote in mixed ops).
68
+ g_scalar = float(slw["gamma"]) if gamma is None else float(gamma)
69
+ if g_scalar == 0.0: # passthrough (bit-exact base trunk)
70
+ return x
71
+ scale = 1.0 / np.sqrt(k)
72
+ Kt = slw["K_slots"].T # (k, n_slot)
73
+ Wr, br = slw["W_r"], slw["b_r"]
74
+ Wq, bq = slw["W_q"], slw["b_q"]
75
+ Wv, bv = slw["W_v"], slw["b_v"]
76
+ Wo, bo = slw["W_o"], slw["b_o"]
77
+ wg, bg = slw["w_g"], slw["b_g"]
78
+ S = np.zeros((n_slot, d_s), dtype=dt)
79
+ out = np.empty((T, d), dtype=dt)
80
+ for t in range(T):
81
+ h = x[t] # (d,)
82
+ a = _softmax((Wr @ h + br) @ Kt * scale) # (n_slot,) write addr
83
+ v = Wv @ h + bv # (d_s,) filler
84
+ g = _sigmoid(float(wg @ h + bg)) # scalar write gate
85
+ ga = g * a # (n_slot,)
86
+ wa = ga[shuffle_perm] if shuffle_perm is not None else ga
87
+ S = (1.0 - wa)[:, None] * S + wa[:, None] * v[None, :] # erase-then-write
88
+ b = _softmax((Wq @ h + bq) @ Kt * scale) # (n_slot,) read addr
89
+ m = (b[:, None] * S).sum(axis=0) # (d_s,) read-after-write
90
+ out[t] = h + g_scalar * (Wo @ m + bo)
91
+ return out
92
+
93
+
94
+ # --------------------------------------------------------------------------- #
95
+ # (b) "SLW\x01" trailer codec — write (serialize) + read (loaders)
96
+ # --------------------------------------------------------------------------- #
97
+ # Fixed tensor order (spec §2), all row-major [out, in], LE f32:
98
+ # K_slots[n_slot·k], W_r[k·d] b_r[k], W_q[k·d] b_q[k],
99
+ # W_v[d_s·d] b_v[d_s], W_o[d·d_s] b_o[d], w_g[d] b_g[1], gamma[1]
100
+ _ARR_ORDER = ("K_slots", "W_r", "b_r", "W_q", "b_q",
101
+ "W_v", "b_v", "W_o", "b_o", "w_g", "b_g", "gamma")
102
+
103
+
104
+ def pack_slw(w: dict) -> bytes:
105
+ """Pack an SLW weight dict into the appended trailer bytes. `w` carries numpy
106
+ arrays under _ARR_ORDER plus ints n_slot/k/d_s. Absent trailer <=> byte-identical
107
+ additive model, so a writer only calls this when the model actually has SLW."""
108
+ out = bytearray()
109
+ out += SLW_MAGIC
110
+ out += struct.pack("<III", int(w["n_slot"]), int(w["k"]), int(w["d_s"]))
111
+ for name in _ARR_ORDER:
112
+ out += np.asarray(w[name], dtype="<f4").reshape(-1).tobytes()
113
+ return bytes(out)
114
+
115
+
116
+ def read_slw(buf: bytes, off: int):
117
+ """Read an SLW trailer at byte offset `off` in `buf`. Returns (slw_dict, new_off)
118
+ or (None, off) if absent/short (passthrough-safe, same guard idiom as the BGB
119
+ trailer reader). `buf` is the whole file bytes; the loader passes the offset the
120
+ previous trailer reader returned."""
121
+ if off < 0 or off + 16 > len(buf) or buf[off:off + 4] != SLW_MAGIC:
122
+ return None, off
123
+ p = off + 4
124
+ n_slot, k, d_s = struct.unpack_from("<III", buf, p); p += 12
125
+ # d (model width) == d_s by construction (d_s = d_model default); projections are
126
+ # [·, d]. K_slots is (n_slot, k); all projections/biases follow in _ARR_ORDER.
127
+ slw = {"n_slot": n_slot, "k": k, "d_s": d_s}
128
+ kslots_n = n_slot * k
129
+ slw["K_slots"] = np.frombuffer(buf, "<f4", kslots_n, p).reshape(n_slot, k).copy(); p += kslots_n * 4
130
+ d = d_s
131
+ def take(rows, cols):
132
+ nonlocal p
133
+ n = rows * cols
134
+ arr = np.frombuffer(buf, "<f4", n, p).reshape(rows, cols).copy() if cols > 1 \
135
+ else np.frombuffer(buf, "<f4", rows, p).reshape(rows).copy()
136
+ p += (rows * cols) * 4
137
+ return arr
138
+ slw["W_r"] = take(k, d); slw["b_r"] = take(k, 1)
139
+ slw["W_q"] = take(k, d); slw["b_q"] = take(k, 1)
140
+ slw["W_v"] = take(d_s, d); slw["b_v"] = take(d_s, 1)
141
+ slw["W_o"] = take(d, d_s); slw["b_o"] = take(d, 1)
142
+ slw["w_g"] = take(d, 1); slw["b_g"] = float(np.frombuffer(buf, "<f4", 1, p)[0]); p += 4
143
+ slw["gamma"] = float(np.frombuffer(buf, "<f4", 1, p)[0]); p += 4
144
+ return slw, p
145
+
146
+
147
+ def slw_weights_from_torch(mod) -> dict:
148
+ """Extract the SLW weight dict (numpy) from a trained torch SLWModule, in the
149
+ exact names pack_slw / slot_apply expect. Called by core/serialize.py."""
150
+ def n(t):
151
+ return t.detach().cpu().numpy().astype("<f4")
152
+ return {
153
+ "n_slot": mod.n_slot, "k": mod.k, "d_s": mod.d_s,
154
+ "K_slots": n(mod.K_slots),
155
+ "W_r": n(mod.W_r.weight), "b_r": n(mod.W_r.bias),
156
+ "W_q": n(mod.W_q.weight), "b_q": n(mod.W_q.bias),
157
+ "W_v": n(mod.W_v.weight), "b_v": n(mod.W_v.bias),
158
+ "W_o": n(mod.W_o.weight), "b_o": n(mod.W_o.bias),
159
+ "w_g": n(mod.w_g.weight).reshape(-1), "b_g": n(mod.w_g.bias),
160
+ "gamma": n(mod.gamma).reshape(1),
161
+ }
162
+
163
+
164
+ # --------------------------------------------------------------------------- #
165
+ # (c) torch training module (DIRECTIONAL) — defined only when torch is present so
166
+ # the inference import (slot_apply / read_slw) stays torch-free (pod-clean).
167
+ # --------------------------------------------------------------------------- #
168
+ try:
169
+ import math as _math
170
+ import torch as _torch
171
+ import torch.nn as _nn
172
+ _HAS_TORCH = True
173
+ except Exception: # pragma: no cover - inference pod has no torch
174
+ _HAS_TORCH = False
175
+
176
+ if _HAS_TORCH:
177
+ class SLWModule(_nn.Module):
178
+ """Learnable gated-write forward-slot. forward(x:(B,d,T)) -> (B,d,T). Op order
179
+ MIRRORS core/slw.slot_apply exactly for 2-production parity. γ=0 passthrough."""
180
+
181
+ def __init__(self, d, n_slot=8, k=64, d_s=None):
182
+ super().__init__()
183
+ d_s = d_s or d
184
+ self.d, self.n_slot, self.k, self.d_s = d, n_slot, k, d_s
185
+ self.scale = 1.0 / _math.sqrt(k)
186
+ self.K_slots = _nn.Parameter(_torch.randn(n_slot, k) * (1.0 / _math.sqrt(k)))
187
+ self.W_r = _nn.Linear(d, k)
188
+ self.W_q = _nn.Linear(d, k)
189
+ self.W_v = _nn.Linear(d, d_s)
190
+ self.W_o = _nn.Linear(d_s, d)
191
+ self.w_g = _nn.Linear(d, 1)
192
+ self.gamma = _nn.Parameter(_torch.tensor(1.0))
193
+
194
+ def forward(self, x):
195
+ B, C, T = x.shape
196
+ xt = x.transpose(1, 2) # (B, T, d)
197
+ S = x.new_zeros(B, self.n_slot, self.d_s)
198
+ Kt = self.K_slots.t()
199
+ outs = []
200
+ for t in range(T):
201
+ h = xt[:, t, :] # (B, d)
202
+ a = _torch.softmax((self.W_r(h) @ Kt) * self.scale, dim=-1)
203
+ v = self.W_v(h)
204
+ g = _torch.sigmoid(self.w_g(h))
205
+ ga = (g * a).unsqueeze(-1)
206
+ S = (1.0 - ga) * S + ga * v.unsqueeze(1)
207
+ b = _torch.softmax((self.W_q(h) @ Kt) * self.scale, dim=-1)
208
+ m = (b.unsqueeze(-1) * S).sum(dim=1)
209
+ outs.append(self.W_o(m))
210
+ o = _torch.stack(outs, dim=1).transpose(1, 2) # (B, d, T)
211
+ return x + self.gamma * o