anima-python 0.13.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anima_py/README.md +39 -0
- anima_py/__init__.py +28 -0
- anima_py/__main__.py +7 -0
- anima_py/cli/__init__.py +1 -0
- anima_py/cli/anima.py +271 -0
- anima_py/cli/corpus.py +150 -0
- anima_py/cli/evaluate.py +1350 -0
- anima_py/cli/rho_axon.py +801 -0
- anima_py/cli/serialize.py +174 -0
- anima_py/cli/serialize_bind.py +100 -0
- anima_py/cli/sweep.py +509 -0
- anima_py/cli/train.py +1667 -0
- anima_py/core/__init__.py +1 -0
- anima_py/core/brain.py +601 -0
- anima_py/core/clm_serialize_v2.py +486 -0
- anima_py/core/clml.py +155 -0
- anima_py/core/decode.py +1386 -0
- anima_py/core/engine_cli.py +10307 -0
- anima_py/core/engine_g.py +129 -0
- anima_py/core/generator.py +318 -0
- anima_py/core/hippo_lane.py +133 -0
- anima_py/core/model.py +462 -0
- anima_py/core/pure_field.py +334 -0
- anima_py/core/rho_fan.py +442 -0
- anima_py/core/serialize.py +826 -0
- anima_py/core/serialize_standalone.py +186 -0
- anima_py/core/slw.py +211 -0
- anima_py/core/verify_clm_v2.py +844 -0
- anima_python-0.13.1.dist-info/METADATA +52 -0
- anima_python-0.13.1.dist-info/RECORD +34 -0
- anima_python-0.13.1.dist-info/WHEEL +5 -0
- anima_python-0.13.1.dist-info/entry_points.txt +2 -0
- anima_python-0.13.1.dist-info/licenses/LICENSE +21 -0
- anima_python-0.13.1.dist-info/top_level.txt +1 -0
anima_py/cli/sweep.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cli/sweep.py — the CANONICAL anima multi-GPU lever-sweep orchestrator (`anima sweep`).
|
|
3
|
+
|
|
4
|
+
>>> This file promotes the ad-hoc scratch shell orchestration (fire_4gpu_big.sh and
|
|
5
|
+
>>> friends) into a proper, reproducible single-entry subcommand, SYMMETRIC to
|
|
6
|
+
>>> cli/train.py (LEARNING) and cli/evaluate.py (MEASUREMENT). `anima sweep <args>`
|
|
7
|
+
>>> dispatches HERE. No scratch fire_*.sh sprawl.
|
|
8
|
+
>>>
|
|
9
|
+
>>> WHAT IT DOES: run a ρ·weave-lever sweep (recombination wall · frozen bar = former G1 ·
|
|
10
|
+
>>> H_1129) over the matrix ARMS × OBJECTIVES. Each
|
|
11
|
+
>>> (arm, objective) pair is one "cell". Cells are pinned round-robin to the GPUs in
|
|
12
|
+
>>> --gpus and run concurrently (max-concurrent = number of GPUs). Each cell:
|
|
13
|
+
>>> (1) TRAINS a 303M CLMConvMoE via `python3 cli/train.py …` (CUDA_VISIBLE_DEVICES
|
|
14
|
+
>>> pinned to its GPU), serializing <out-dir>/<tag>.clm.
|
|
15
|
+
>>> (2) if --measure, ρ-AXON reach-MEASURES (former G0-G6) the resulting .clm via `python3 cli/evaluate.py
|
|
16
|
+
>>> <clm> --corpus … --gen N` (CPU, torch-free numpy) -> <out-dir>/<tag>.meas.log.
|
|
17
|
+
>>> (3) touches a per-cell done flag.
|
|
18
|
+
>>> After every cell finishes, the orchestrator PARSES each <tag>.meas.log for the
|
|
19
|
+
>>> reach-bar lines (ρ·form/weave/leap/fan, parsed by their frozen-bar G-labels) and prints
|
|
20
|
+
>>> + writes a summary TABLE (SWEEP_SUMMARY.md),
|
|
21
|
+
>>> flagging any ρ·weave-PASS candidate (former G1 · best_distinct >= 2 AND > max_single) and any
|
|
22
|
+
>>> overfit-collapse INVALID cell (ρ·form/G0 FAIL + collapsed train CE).
|
|
23
|
+
>>>
|
|
24
|
+
>>> DESIGN INVARIANT (single-entry discipline, a_engine_native_learning): sweep is
|
|
25
|
+
>>> ONLY an orchestrator. It NEVER imports torch / the model / the scorers — it shells
|
|
26
|
+
>>> out to the two canonical engines (cli/train.py, cli/evaluate.py) as SUBPROCESSES,
|
|
27
|
+
>>> exactly as the hexa anima_train_mode / anima_evaluate_mode dispatch shells out.
|
|
28
|
+
>>> This keeps sweep torch-free (pure orchestration + subprocess + text parsing) and
|
|
29
|
+
>>> makes the trainer/evaluator the single sources of truth. The engine-native TERMINAL
|
|
30
|
+
>>> verdict is the .clm re-measure via cli/evaluate.py (already the measure step here);
|
|
31
|
+
>>> the torch-side training CE is DIRECTIONAL only (a_engine_native_learning).
|
|
32
|
+
>>>
|
|
33
|
+
>>> bf16 vs fp32 (convergence lesson): objective==constructive_bind uses torch.fft
|
|
34
|
+
>>> (HRR circular convolution), which has NO bf16 kernel — bf16 there silently dies
|
|
35
|
+
>>> (H_1823). So the orchestrator AUTO-DROPS --bf16 for constructive_bind (fp32) and
|
|
36
|
+
>>> keeps --bf16 for every other objective. This mirrors fire_4gpu_big.sh (B_CBIND ran
|
|
37
|
+
>>> with an empty bf16 flag).
|
|
38
|
+
|
|
39
|
+
USAGE (installed `anima` PATH command after `hx install anima`):
|
|
40
|
+
anima sweep --arms ctrl --objectives ce_marginal,composed_nce,infonce,constructive_bind \\
|
|
41
|
+
--steps 8000 --gpus 0,1,2,3 \\
|
|
42
|
+
--corpus dancinlab/anima-corpus-5lang-unified-v2 dancinlab/anima-corpus-ko-general \\
|
|
43
|
+
--cell-label general-5lang ko-general --measure
|
|
44
|
+
"""
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
import argparse
|
|
47
|
+
import os
|
|
48
|
+
import re
|
|
49
|
+
import subprocess
|
|
50
|
+
import glob
|
|
51
|
+
import sys
|
|
52
|
+
import threading
|
|
53
|
+
import time
|
|
54
|
+
|
|
55
|
+
# ── canonical engine paths (resolved relative to THIS file so it works installed) ──
|
|
56
|
+
_HERE = os.path.dirname(os.path.abspath(__file__)) # …/cli
|
|
57
|
+
_REPO = os.environ.get("ANIMA_SRC") or os.path.dirname(_HERE) # repo root (parent of cli/)
|
|
58
|
+
_TRAIN_PY = os.path.join(_HERE, "train.py") # canonical trainer
|
|
59
|
+
_EVAL_PY = os.path.join(_HERE, "evaluate.py") # canonical evaluator
|
|
60
|
+
|
|
61
|
+
# objectives whose loss uses torch.fft (HRR bind) → NO bf16 kernel → force fp32.
|
|
62
|
+
_FP32_ONLY_OBJECTIVES = {"constructive_bind"}
|
|
63
|
+
|
|
64
|
+
# overfit-collapse heuristic (convergence train-py-3): a memorized/collapsed run drives
|
|
65
|
+
# the TRAIN CE toward ~0 while the model is garbage (held-out never descends, ρ·form/G0 fails).
|
|
66
|
+
# So ρ·form/G0 FAIL + a collapsed final train CE => the ρ-AXON reach measurement (former G0-G6) is INVALID (not a real
|
|
67
|
+
# capability floor, just corpus starvation / memorization). Threshold is frozen, documented.
|
|
68
|
+
_OVERFIT_LOSSF_THRESH = 0.5
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
# GPU discovery
|
|
73
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
def _default_gpus() -> list[str]:
|
|
75
|
+
"""Default GPU index list = all visible. Honor CUDA_VISIBLE_DEVICES if set, else
|
|
76
|
+
probe `nvidia-smi -L`, else fall back to a single ['0']."""
|
|
77
|
+
cvd = os.environ.get("CUDA_VISIBLE_DEVICES")
|
|
78
|
+
if cvd:
|
|
79
|
+
# pass the visible physical indices through as-is — each becomes a child's
|
|
80
|
+
# CUDA_VISIBLE_DEVICES (do NOT re-index, or a user's pre-restriction is lost).
|
|
81
|
+
vis = [g.strip() for g in cvd.split(",") if g.strip()]
|
|
82
|
+
if vis:
|
|
83
|
+
return vis
|
|
84
|
+
try:
|
|
85
|
+
out = subprocess.run(["nvidia-smi", "-L"], capture_output=True, text=True, timeout=15)
|
|
86
|
+
n = len([ln for ln in out.stdout.splitlines() if ln.strip().startswith("GPU ")])
|
|
87
|
+
if n > 0:
|
|
88
|
+
return [str(i) for i in range(n)]
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
return ["0"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
95
|
+
# one cell = one (arm, objective) train (+ optional measure)
|
|
96
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
97
|
+
def _cell_tag(arm: str, objective: str, seed: int) -> str:
|
|
98
|
+
return f"{arm}_{objective}_s{seed}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_train_cmd(a, arm: str, objective: str, tag: str, out_dir: str) -> list[str]:
|
|
102
|
+
clm = os.path.join(out_dir, tag + ".clm")
|
|
103
|
+
pt = os.path.join(out_dir, tag + ".pt")
|
|
104
|
+
gj = os.path.join(out_dir, tag + ".json")
|
|
105
|
+
cmd = [sys.executable, _TRAIN_PY,
|
|
106
|
+
"--arm", arm, "--objective", objective,
|
|
107
|
+
"--tlora-rank", str(a.tlora_rank),
|
|
108
|
+
"--seed", str(a.seed),
|
|
109
|
+
"--steps", str(a.steps),
|
|
110
|
+
"--sample", a.sample,
|
|
111
|
+
"--val-frac", str(a.val_frac),
|
|
112
|
+
"--val-every", str(a.val_every),
|
|
113
|
+
"--dbes-every", str(a.dbes_every),
|
|
114
|
+
"--out", clm, "--ckpt-out", pt, "--gauges-out", gj]
|
|
115
|
+
if a.canon:
|
|
116
|
+
cmd.append("--canon")
|
|
117
|
+
if a.corpus:
|
|
118
|
+
cmd += ["--corpus", *a.corpus]
|
|
119
|
+
if a.cell_label:
|
|
120
|
+
cmd += ["--cell-label", *a.cell_label]
|
|
121
|
+
# bf16 EXCEPT for fft-based objectives (constructive_bind → fp32, H_1823 lesson).
|
|
122
|
+
if a.bf16 and objective not in _FP32_ONLY_OBJECTIVES:
|
|
123
|
+
cmd.append("--bf16")
|
|
124
|
+
# N6 regularization-floor passthrough: force a CONSTANT dropout/weight-decay
|
|
125
|
+
# (>=0 overrides the savant decay schedule) — needed to escape the 8000-step
|
|
126
|
+
# generation-collapse (train-py-4) by holding inhibition at a floor.
|
|
127
|
+
if a.dropout_floor >= 0.0:
|
|
128
|
+
cmd += ["--dropout-floor", str(a.dropout_floor)]
|
|
129
|
+
if a.wd_floor >= 0.0:
|
|
130
|
+
cmd += ["--wd-floor", str(a.wd_floor)]
|
|
131
|
+
# step-window multiplex: train.py dumps <clm>.step<N>.clm every N steps so ONE run
|
|
132
|
+
# yields the 2000/4000/… checkpoints (train-py-4 confound isolation, no re-train).
|
|
133
|
+
if a.ckpt_every > 0:
|
|
134
|
+
cmd += ["--ckpt-every", str(a.ckpt_every)]
|
|
135
|
+
return cmd
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _ckpt_step(path: str):
|
|
139
|
+
"""Extract N from a `<clm>.step<N>.clm` intermediate-checkpoint path (else None)."""
|
|
140
|
+
m = re.search(r"\.step(\d+)\.clm$", path)
|
|
141
|
+
return int(m.group(1)) if m else None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _build_measure_cmd(a, tag: str, out_dir: str) -> list[str]:
|
|
145
|
+
clm = os.path.join(out_dir, tag + ".clm")
|
|
146
|
+
cmd = [sys.executable, _EVAL_PY, clm, "--gen", str(a.gen)]
|
|
147
|
+
if a.corpus:
|
|
148
|
+
cmd += ["--corpus", *a.corpus]
|
|
149
|
+
return cmd
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def run_cell(a, arm: str, objective: str, gpu: str, out_dir: str, log_lock: threading.Lock):
|
|
153
|
+
"""Train (GPU-pinned) then optionally measure (CPU) one (arm,objective) cell."""
|
|
154
|
+
tag = _cell_tag(arm, objective, a.seed)
|
|
155
|
+
clm = os.path.join(out_dir, tag + ".clm")
|
|
156
|
+
train_log = os.path.join(out_dir, tag + ".log")
|
|
157
|
+
meas_log = os.path.join(out_dir, tag + ".meas.log")
|
|
158
|
+
bf = "fp32" if objective in _FP32_ONLY_OBJECTIVES else ("bf16" if a.bf16 else "fp32")
|
|
159
|
+
|
|
160
|
+
def _say(msg):
|
|
161
|
+
with log_lock:
|
|
162
|
+
print(f"[{time.strftime('%H:%M:%S')}] [gpu{gpu}] {tag}: {msg}", flush=True)
|
|
163
|
+
|
|
164
|
+
# ── (1) TRAIN (CUDA_VISIBLE_DEVICES pinned to this cell's GPU) ────────────
|
|
165
|
+
env = os.environ.copy()
|
|
166
|
+
env["CUDA_VISIBLE_DEVICES"] = str(gpu)
|
|
167
|
+
_say(f"TRAIN start ({arm}/{objective}, {bf}) -> {os.path.basename(clm)}")
|
|
168
|
+
with open(train_log, "w") as lf:
|
|
169
|
+
rc = subprocess.run(_build_train_cmd(a, arm, objective, tag, out_dir),
|
|
170
|
+
cwd=_REPO, env=env, stdout=lf, stderr=subprocess.STDOUT).returncode
|
|
171
|
+
have_clm = os.path.exists(clm) and os.path.getsize(clm) > 0
|
|
172
|
+
_say(f"TRAIN done rc={rc} clm={'OK' if have_clm else 'MISSING'}")
|
|
173
|
+
open(os.path.join(out_dir, tag + ".done"), "w").close() # per-cell train done flag
|
|
174
|
+
|
|
175
|
+
# ── (2) MEASURE (CPU, torch-free numpy evaluate — CUDA hidden) ────────────
|
|
176
|
+
# step-window: measure the FINAL <tag>.clm AND every intermediate --ckpt-every
|
|
177
|
+
# checkpoint (<tag>.clm.step<N>.clm), each into its own <label>.meas.log so the
|
|
178
|
+
# aggregate can plot ρ·form/ρ·weave (former G0/G1) across steps (train-py-4 confound: where does ρ·form/G0 break?).
|
|
179
|
+
if a.measure:
|
|
180
|
+
menv = os.environ.copy()
|
|
181
|
+
menv["CUDA_VISIBLE_DEVICES"] = "" # force CPU numpy path
|
|
182
|
+
menv.setdefault("OMP_NUM_THREADS", "8")
|
|
183
|
+
targets = []
|
|
184
|
+
for cp in sorted((p for p in glob.glob(clm + ".step*.clm")), key=_ckpt_step):
|
|
185
|
+
n = _ckpt_step(cp)
|
|
186
|
+
targets.append((cp, f"{tag}__s{n}"))
|
|
187
|
+
if have_clm:
|
|
188
|
+
targets.append((clm, tag)) # final full-run (bare tag)
|
|
189
|
+
if not targets:
|
|
190
|
+
with open(meas_log, "w") as lf:
|
|
191
|
+
lf.write("NO CKPT — train produced no .clm; measure skipped\n")
|
|
192
|
+
for cpath, label in targets:
|
|
193
|
+
ml = os.path.join(out_dir, label + ".meas.log")
|
|
194
|
+
with open(ml, "w") as lf:
|
|
195
|
+
lf.write(f"=== [{label}] ρ-AXON reach measure · former G0-G6 ({time.strftime('%H:%M:%S')}) ===\n")
|
|
196
|
+
lf.flush()
|
|
197
|
+
mcmd = [sys.executable, _EVAL_PY, cpath, "--gen", str(a.gen)]
|
|
198
|
+
if a.corpus:
|
|
199
|
+
mcmd += ["--corpus", *a.corpus]
|
|
200
|
+
mrc = subprocess.run(mcmd, cwd=_REPO, env=menv, stdout=lf,
|
|
201
|
+
stderr=subprocess.STDOUT).returncode
|
|
202
|
+
lf.write(f"\n=== [{label}] measure rc={mrc} ===\n")
|
|
203
|
+
open(os.path.join(out_dir, tag + ".meas.done"), "w").close() # per-cell measure flag
|
|
204
|
+
_say(f"MEASURE done ({len(targets)} checkpoint(s))")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
208
|
+
# aggregate — parse each <tag>.meas.log (+ <tag>.log) into a summary table
|
|
209
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
210
|
+
_RE_G1 = re.compile(r"best_distinct=(\d+)\s*>\s*max_single=(\d+)")
|
|
211
|
+
_RE_G2 = re.compile(r"novel=(\d+)")
|
|
212
|
+
_RE_G6 = re.compile(r"distinct=(\d+)\s*\(need>=5\).*?falsifiable=(\d+)")
|
|
213
|
+
_RE_G0N = re.compile(r"on\s+(\d+)/5")
|
|
214
|
+
_RE_LOSSF = re.compile(r"lossF=([0-9.]+)")
|
|
215
|
+
_RE_VALPOOL = re.compile(r"FINAL val_CE\(pooled\)=(\S+)")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _gate_pass(line: str) -> bool:
|
|
219
|
+
# evaluate.py prints 🟢 for PASS, 🔴 for FAIL on G0/G1/G2/G6.
|
|
220
|
+
return "🟢" in line
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def parse_cell(tag: str, out_dir: str) -> dict:
|
|
224
|
+
"""Parse one cell's measure log (+ train log) into a result dict."""
|
|
225
|
+
meas_log = os.path.join(out_dir, tag + ".meas.log")
|
|
226
|
+
# a checkpoint label is "<base>__s<N>"; its train log is the base cell's <base>.log
|
|
227
|
+
base_tag = tag.split("__s")[0]
|
|
228
|
+
train_log = os.path.join(out_dir, base_tag + ".log")
|
|
229
|
+
r = {"tag": tag, "form": None, "form_n": None, "weave": None, "weave_bd": None,
|
|
230
|
+
"weave_ms": None, "leap": None, "leap_novel": None, "fan": None, "fan_dist": None,
|
|
231
|
+
"fan_fals": None, "closure": None, "lossf": None, "val_pool": None,
|
|
232
|
+
"status": "?", "note": ""}
|
|
233
|
+
|
|
234
|
+
# train log → collapse signals
|
|
235
|
+
if os.path.exists(train_log):
|
|
236
|
+
tl = open(train_log, encoding="utf-8", errors="replace").read()
|
|
237
|
+
m = _RE_LOSSF.search(tl)
|
|
238
|
+
if m:
|
|
239
|
+
r["lossf"] = float(m.group(1))
|
|
240
|
+
m = _RE_VALPOOL.search(tl)
|
|
241
|
+
if m and m.group(1) not in ("None", "nan"):
|
|
242
|
+
try:
|
|
243
|
+
r["val_pool"] = float(m.group(1))
|
|
244
|
+
except ValueError:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
if not os.path.exists(meas_log):
|
|
248
|
+
r["status"] = "NO-MEAS"
|
|
249
|
+
r["note"] = "no measure log"
|
|
250
|
+
return r
|
|
251
|
+
txt = open(meas_log, encoding="utf-8", errors="replace").read()
|
|
252
|
+
if "NO CKPT" in txt:
|
|
253
|
+
r["status"] = "NO-CKPT"
|
|
254
|
+
r["note"] = "train produced no .clm"
|
|
255
|
+
return r
|
|
256
|
+
|
|
257
|
+
for line in txt.splitlines():
|
|
258
|
+
if "ρ·form COHERENCE" in line:
|
|
259
|
+
r["form"] = _gate_pass(line)
|
|
260
|
+
mm = _RE_G0N.search(line)
|
|
261
|
+
if mm:
|
|
262
|
+
r["form_n"] = int(mm.group(1))
|
|
263
|
+
elif "ρ·weave RECOMBINATION" in line:
|
|
264
|
+
r["weave"] = _gate_pass(line)
|
|
265
|
+
mm = _RE_G1.search(line)
|
|
266
|
+
if mm:
|
|
267
|
+
r["weave_bd"] = int(mm.group(1)); r["weave_ms"] = int(mm.group(2))
|
|
268
|
+
elif "ρ·leap NOVELTY" in line:
|
|
269
|
+
r["leap"] = _gate_pass(line)
|
|
270
|
+
mm = _RE_G2.search(line)
|
|
271
|
+
if mm:
|
|
272
|
+
r["leap_novel"] = int(mm.group(1))
|
|
273
|
+
elif "ρ·fan IDEATION" in line:
|
|
274
|
+
r["fan"] = _gate_pass(line)
|
|
275
|
+
mm = _RE_G6.search(line)
|
|
276
|
+
if mm:
|
|
277
|
+
r["fan_dist"] = int(mm.group(1)); r["fan_fals"] = int(mm.group(2))
|
|
278
|
+
elif line.strip().startswith("CLOSURE"):
|
|
279
|
+
r["closure"] = _gate_pass(line)
|
|
280
|
+
|
|
281
|
+
# status classification
|
|
282
|
+
# overfit-INVALID applies to the FINAL run only (lossf is the end-of-run CE; an
|
|
283
|
+
# intermediate step-window checkpoint keeps its own ρ·form/ρ·weave · former G0/G1 row, not an overfit verdict).
|
|
284
|
+
overfit = ("__s" not in tag and r["form"] is False and r["lossf"] is not None
|
|
285
|
+
and r["lossf"] < _OVERFIT_LOSSF_THRESH)
|
|
286
|
+
weave_cand = (r["weave_bd"] is not None and r["weave_ms"] is not None
|
|
287
|
+
and r["weave_bd"] >= 2 and r["weave_bd"] > r["weave_ms"])
|
|
288
|
+
if r["form"] is None:
|
|
289
|
+
r["status"] = "INCOMPLETE"
|
|
290
|
+
r["note"] = "measure did not finish (no gate lines)"
|
|
291
|
+
elif overfit:
|
|
292
|
+
r["status"] = "INVALID"
|
|
293
|
+
r["note"] = (f"overfit collapse — G0 FAIL + train CE→{r['lossf']:.3f} "
|
|
294
|
+
f"(<{_OVERFIT_LOSSF_THRESH}); measurement invalid (corpus starvation)")
|
|
295
|
+
elif weave_cand:
|
|
296
|
+
r["status"] = "ρ·weave-PASS?"
|
|
297
|
+
r["note"] = (f"ρ·weave-PASS candidate: best_distinct={r['weave_bd']} > "
|
|
298
|
+
f"max_single={r['weave_ms']}")
|
|
299
|
+
elif r["closure"]:
|
|
300
|
+
r["status"] = "CLOSURE"
|
|
301
|
+
else:
|
|
302
|
+
r["status"] = "OK"
|
|
303
|
+
return r
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _fmt(v, dash="—"):
|
|
307
|
+
return dash if v is None else str(v)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _pf(v):
|
|
311
|
+
if v is None:
|
|
312
|
+
return "—"
|
|
313
|
+
return "🟢" if v else "🔴"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _row_sortkey(lbl: str):
|
|
317
|
+
base = lbl.split("__s")[0]
|
|
318
|
+
st = lbl.split("__s")[1] if "__s" in lbl else None
|
|
319
|
+
return (base, int(st) if (st and st.isdigit()) else 10 ** 9) # final (no __s) sorts last
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def aggregate(a, cells, out_dir: str):
|
|
323
|
+
# one row per measured checkpoint: glob every <label>.meas.log (final <tag> +
|
|
324
|
+
# step-window <tag>__s<N>), so --ckpt-every runs expand into per-step rows.
|
|
325
|
+
labels = sorted((os.path.basename(p)[:-len(".meas.log")]
|
|
326
|
+
for p in glob.glob(os.path.join(out_dir, "*.meas.log"))),
|
|
327
|
+
key=_row_sortkey)
|
|
328
|
+
if labels:
|
|
329
|
+
rows = [parse_cell(lbl, out_dir) for lbl in labels]
|
|
330
|
+
else:
|
|
331
|
+
rows = [parse_cell(_cell_tag(arm, obj, a.seed), out_dir) for (arm, obj) in cells]
|
|
332
|
+
|
|
333
|
+
# console + markdown table
|
|
334
|
+
header = ("| tag | ρ·form | ρ·weave bd/ms | ρ·leap novel | ρ·fan dist/fals | closure | status |")
|
|
335
|
+
sep = ("|---|---|---|---|---|---|---|")
|
|
336
|
+
lines = [header, sep]
|
|
337
|
+
for r in rows:
|
|
338
|
+
weave = f"{_fmt(r['weave_bd'])}/{_fmt(r['weave_ms'])}"
|
|
339
|
+
fan = f"{_fmt(r['fan_dist'])}/{_fmt(r['fan_fals'])}"
|
|
340
|
+
lines.append(
|
|
341
|
+
f"| {r['tag']} | {_pf(r['form'])} | {weave} | {_fmt(r['leap_novel'])} | "
|
|
342
|
+
f"{fan} | {_pf(r['closure'])} | {r['status']} |")
|
|
343
|
+
|
|
344
|
+
weave_cands = [r for r in rows if r["status"] == "ρ·weave-PASS?"]
|
|
345
|
+
invalids = [r for r in rows if r["status"] == "INVALID"]
|
|
346
|
+
|
|
347
|
+
md = []
|
|
348
|
+
md.append(f"# anima sweep summary — {time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
349
|
+
md.append("")
|
|
350
|
+
md.append(f"- matrix: arms={a.arms} × objectives={a.objectives} "
|
|
351
|
+
f"(seed={a.seed}, steps={a.steps}, gen={a.gen})")
|
|
352
|
+
md.append(f"- cells: {len(rows)} · out-dir: `{out_dir}`")
|
|
353
|
+
md.append(f"- engines: `cli/train.py` (GPU) + `cli/evaluate.py` (CPU numpy, torch-free)")
|
|
354
|
+
md.append("")
|
|
355
|
+
md += lines
|
|
356
|
+
md.append("")
|
|
357
|
+
md.append("## flags")
|
|
358
|
+
if weave_cands:
|
|
359
|
+
md.append("**🟢 ρ·weave-PASS candidate(s)** (best_distinct ≥ 2 AND > max_single — "
|
|
360
|
+
"engine-native recombination signal, verify byte-exact before cementing):")
|
|
361
|
+
for r in weave_cands:
|
|
362
|
+
md.append(f"- `{r['tag']}` — {r['note']}")
|
|
363
|
+
else:
|
|
364
|
+
md.append("- ρ·weave-PASS candidates: none (all cells floored best_distinct ≤ max_single).")
|
|
365
|
+
if invalids:
|
|
366
|
+
md.append("")
|
|
367
|
+
md.append("**⚠️ INVALID (overfit collapse — measurement not trustworthy):**")
|
|
368
|
+
for r in invalids:
|
|
369
|
+
md.append(f"- `{r['tag']}` — {r['note']}")
|
|
370
|
+
md.append("")
|
|
371
|
+
md.append("> HONESTY (a_engine_native_learning / c9): the ρ-AXON reach measure (former G0-G6) here IS the "
|
|
372
|
+
"engine-native `.clm` re-measure (cli/evaluate.py numpy mirror = terminal-eligible). "
|
|
373
|
+
"The torch-side training CE (in each `<tag>.log`) is DIRECTIONAL only. A ρ·weave-PASS (former G1) "
|
|
374
|
+
"candidate still needs the frozen-bar verdict cemented from THIS measure output "
|
|
375
|
+
"verbatim (no tune-to-green).")
|
|
376
|
+
md_text = "\n".join(md) + "\n"
|
|
377
|
+
|
|
378
|
+
summary_path = os.path.join(out_dir, "SWEEP_SUMMARY.md")
|
|
379
|
+
with open(summary_path, "w", encoding="utf-8") as f:
|
|
380
|
+
f.write(md_text)
|
|
381
|
+
|
|
382
|
+
print("")
|
|
383
|
+
print("════════════════════════════════════════════════════════════════")
|
|
384
|
+
print(f" anima sweep aggregate ({len(rows)} cells)")
|
|
385
|
+
print("════════════════════════════════════════════════════════════════")
|
|
386
|
+
for ln in lines:
|
|
387
|
+
print(" " + ln)
|
|
388
|
+
print("")
|
|
389
|
+
if weave_cands:
|
|
390
|
+
print(" 🟢 ρ·weave-PASS candidate(s):")
|
|
391
|
+
for r in weave_cands:
|
|
392
|
+
print(f" {r['tag']} — {r['note']}")
|
|
393
|
+
else:
|
|
394
|
+
print(" ρ·weave-PASS candidates: none.")
|
|
395
|
+
if invalids:
|
|
396
|
+
print(" ⚠️ INVALID (overfit collapse):")
|
|
397
|
+
for r in invalids:
|
|
398
|
+
print(f" {r['tag']} — {r['note']}")
|
|
399
|
+
print("")
|
|
400
|
+
print(f" summary -> {summary_path}")
|
|
401
|
+
return rows
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ════════════════════════════════════════════════════════════════════════════
|
|
405
|
+
def _csv(s: str) -> list[str]:
|
|
406
|
+
return [x.strip() for x in s.split(",") if x.strip()]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def main(argv=None):
|
|
410
|
+
ap = argparse.ArgumentParser(
|
|
411
|
+
prog="anima sweep",
|
|
412
|
+
description="canonical multi-GPU lever-sweep orchestrator — the arms×objectives "
|
|
413
|
+
"matrix, GPU-pinned round-robin, subprocess-shells cli/train.py + "
|
|
414
|
+
"cli/evaluate.py, aggregates the ρ-AXON reach bars · former G0-G6 (replaces scratch fire_*.sh).")
|
|
415
|
+
ap.add_argument("--arms", default="ctrl",
|
|
416
|
+
help="comma list of train.py --arm values (default ctrl)")
|
|
417
|
+
ap.add_argument("--objectives", default="ce_marginal,composed_nce,infonce,constructive_bind",
|
|
418
|
+
help="comma list of train.py --objective values (matrix = arms × objectives)")
|
|
419
|
+
ap.add_argument("--steps", type=int, default=8000)
|
|
420
|
+
ap.add_argument("--ckpt-every", type=int, default=0,
|
|
421
|
+
help="0=final .clm only; N=also dump+measure a checkpoint every N steps "
|
|
422
|
+
"(step-window: one run → ρ·form/ρ·weave · former G0/G1 across 2000/4000/… — train-py-4 isolation)")
|
|
423
|
+
ap.add_argument("--seed", type=int, default=7)
|
|
424
|
+
ap.add_argument("--corpus", nargs="*", default=[],
|
|
425
|
+
help="corpus paths / HF ids passed through to train.py + evaluate.py")
|
|
426
|
+
ap.add_argument("--cell-label", nargs="*", default=[],
|
|
427
|
+
help="per-corpus register labels passed through to train.py")
|
|
428
|
+
ap.add_argument("--gpus", default="",
|
|
429
|
+
help="comma list of GPU indices to pin round-robin (default = all visible)")
|
|
430
|
+
ap.add_argument("--out-dir", default="./sweeprun",
|
|
431
|
+
help="dir for .clm/.pt/.log/.meas.log + SWEEP_SUMMARY.md")
|
|
432
|
+
ap.add_argument("--gen", type=int, default=80, help="measure decode budget (tokens)")
|
|
433
|
+
ap.add_argument("--sample", choices=["roundrobin", "proportional"], default="proportional")
|
|
434
|
+
ap.add_argument("--val-frac", type=float, default=0.02)
|
|
435
|
+
ap.add_argument("--val-every", type=int, default=500)
|
|
436
|
+
ap.add_argument("--dbes-every", type=int, default=100000)
|
|
437
|
+
ap.add_argument("--tlora-rank", type=int, default=8)
|
|
438
|
+
# N6 regularization floor (savant decay override) — forwarded to train.py only when >=0.
|
|
439
|
+
ap.add_argument("--dropout-floor", type=float, default=-1.0,
|
|
440
|
+
help="force constant dropout (>=0 overrides savant decay; escapes 8000-step collapse)")
|
|
441
|
+
ap.add_argument("--wd-floor", type=float, default=-1.0,
|
|
442
|
+
help="force constant weight-decay (>=0 overrides savant decay)")
|
|
443
|
+
# bf16 default ON (train.py toy default is fp32, but a 303M GPU sweep wants bf16);
|
|
444
|
+
# constructive_bind is auto-forced fp32 regardless (fft has no bf16 kernel).
|
|
445
|
+
bf = ap.add_mutually_exclusive_group()
|
|
446
|
+
bf.add_argument("--bf16", dest="bf16", action="store_true", default=True)
|
|
447
|
+
bf.add_argument("--no-bf16", dest="bf16", action="store_false")
|
|
448
|
+
ap.add_argument("--canon", dest="canon", action="store_true", default=True,
|
|
449
|
+
help="303M CLMConvMoE (d3784·L4) — default ON")
|
|
450
|
+
ap.add_argument("--no-canon", dest="canon", action="store_false",
|
|
451
|
+
help="toy shape (d64·L2) for a CPU smoke of the orchestration")
|
|
452
|
+
ms = ap.add_mutually_exclusive_group()
|
|
453
|
+
ms.add_argument("--measure", dest="measure", action="store_true", default=True,
|
|
454
|
+
help="after each train, ρ-AXON reach-measure · former G0-G6 the .clm (default)")
|
|
455
|
+
ms.add_argument("--no-measure", dest="measure", action="store_false",
|
|
456
|
+
help="train only, skip measurement")
|
|
457
|
+
a = ap.parse_args(argv)
|
|
458
|
+
|
|
459
|
+
a.arms = _csv(a.arms)
|
|
460
|
+
a.objectives = _csv(a.objectives)
|
|
461
|
+
gpus = _csv(a.gpus) if a.gpus else _default_gpus()
|
|
462
|
+
out_dir = os.path.abspath(a.out_dir)
|
|
463
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
464
|
+
|
|
465
|
+
# matrix = arms × objectives (each a cell)
|
|
466
|
+
cells = [(arm, obj) for arm in a.arms for obj in a.objectives]
|
|
467
|
+
|
|
468
|
+
print("=== anima sweep (canonical multi-GPU lever-sweep orchestrator) ===", flush=True)
|
|
469
|
+
print(f" engines : train={_TRAIN_PY}", flush=True)
|
|
470
|
+
print(f" eval ={_EVAL_PY}", flush=True)
|
|
471
|
+
print(f" matrix : arms={a.arms} × objectives={a.objectives} = {len(cells)} cells", flush=True)
|
|
472
|
+
print(f" gpus : {gpus} (max-concurrent = {len(gpus)})", flush=True)
|
|
473
|
+
print(f" cfg : steps={a.steps} seed={a.seed} gen={a.gen} bf16={a.bf16} "
|
|
474
|
+
f"canon={a.canon} measure={a.measure} sample={a.sample}", flush=True)
|
|
475
|
+
print(f" out-dir : {out_dir}", flush=True)
|
|
476
|
+
for i, (arm, obj) in enumerate(cells):
|
|
477
|
+
g = gpus[i % len(gpus)]
|
|
478
|
+
bfnote = "fp32(fft)" if obj in _FP32_ONLY_OBJECTIVES else ("bf16" if a.bf16 else "fp32")
|
|
479
|
+
print(f" cell[{i}] {_cell_tag(arm, obj, a.seed):<32s} -> gpu{g} [{bfnote}]", flush=True)
|
|
480
|
+
print("", flush=True)
|
|
481
|
+
|
|
482
|
+
# ── round-robin assign cells to GPUs; one worker thread per GPU runs its cells
|
|
483
|
+
# sequentially (train then measure). max-concurrent = number of GPUs. ────────
|
|
484
|
+
per_gpu: dict[str, list] = {g: [] for g in gpus}
|
|
485
|
+
for i, cell in enumerate(cells):
|
|
486
|
+
per_gpu[gpus[i % len(gpus)]].append(cell)
|
|
487
|
+
|
|
488
|
+
log_lock = threading.Lock()
|
|
489
|
+
|
|
490
|
+
def gpu_worker(gpu: str, assigned: list):
|
|
491
|
+
for (arm, obj) in assigned:
|
|
492
|
+
run_cell(a, arm, obj, gpu, out_dir, log_lock)
|
|
493
|
+
|
|
494
|
+
threads = [threading.Thread(target=gpu_worker, args=(g, per_gpu[g]), daemon=True)
|
|
495
|
+
for g in gpus if per_gpu[g]]
|
|
496
|
+
for t in threads:
|
|
497
|
+
t.start()
|
|
498
|
+
for t in threads:
|
|
499
|
+
t.join()
|
|
500
|
+
|
|
501
|
+
if a.measure:
|
|
502
|
+
aggregate(a, cells, out_dir)
|
|
503
|
+
else:
|
|
504
|
+
print(" --no-measure: trained only, no ρ-AXON reach aggregate (former G0-G6).", flush=True)
|
|
505
|
+
return 0
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if __name__ == "__main__":
|
|
509
|
+
sys.exit(main() or 0)
|