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,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
|