File size: 12,094 Bytes
7d3eb8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
"""RAW-text judge inputs, scoring and a channel check for the fix arms on the bug-fixed base
(runs/CSEL/FIX_TRAINING_PREREG.md amendments 3-4): job 184 (FIX 21-23, prefix jr, pools FXR) and job 185 (FIX 10/5/7
of jobs 173/175/176, prefix jq, pools FXQ; replaces job 177's ๆ-written pools).

The judge sees the poem exactly as the model wrote it: NO ๆ write-back (runs/CSEL/FIXED_BASE_PREREG.md amendment 1 -
fixbase_pipe.judged_text contracts any two equal adjacent syllables, which turned model stutters into plausible
reduplication for 6.6% of candidates). scripts/fixon.py (rawjudge/rawpools) still writes ๆ back and is not used here.

  rawpools OUT A-B PFX ARM=DIR...  judge inputs, seeds A..B: the control (170's FIXED_b0 candidates) + <= 3 arms; per
                                seed and topic half (fixbase_pipe.halves, 25/24 topics) OUT/{PFX}_{fixbase,ARM...}_s{seed}h{h}.jsonl
                                = ALL candidates (form-failing ones too, so every arm has the same count per pool and
                                build_pool's balance assert holds by construction; form enters the endpoint through
                                the candidate's all_pass), id TOPIC_sSEED, poem = the raw candidate text (asserted 4 lines)
  score OUT A-B PREFIX ARM...   after judging runs/{PREFIX}_{CLEAN,ALIGN}{seed}{half}/run1.json: per arm vs the control,
                                paired by (topic, seed) over ALL candidates (every candidate is judged; the endpoint
                                counts a form-failing candidate as not clean / not aligned; judged-only rates reported): clean, aligned, all-three, topic-clustered 95% CI, decision
                                (pre-registered rule); a pool whose human anchors are < 70% clean is void -> exit 3
  phonuse CTL_MODEL ARM_MODEL   row norms of token / rime / tone embeddings and the standard valid loss with rime+tone
                                zeroed, for both models (reported by job 179; not a gate)
  test                          CPU tests of the pure parts
"""
import os, sys, json, collections
sys.path.insert(0, "."); sys.path.insert(0, "scripts")

FB = "runs/FIXBASE"


def rows(p): return [json.loads(l) for l in open(p, encoding="utf-8") if l.strip()]


def wrows(p, R):
    with open(p + ".tmp", "w", encoding="utf-8") as f:
        for r in R: f.write(json.dumps(r, ensure_ascii=False) + "\n")
    os.replace(p + ".tmp", p)


def halves(ids):   # = fixbase_pipe.halves (copied so the pools do not depend on fixbase_pipe)
    ids = sorted(ids); h = (len(ids) + 1) // 2
    return [ids[:h], ids[h:]]


def arms_of(specs):
    A = [("fixbase", f"{FB}/FIXED_b0/cands")] + [tuple(x.split("=", 1)) for x in specs]
    assert len({n for n, _ in A}) == len(A) and len(A) <= 4, "arm names must be unique; at most control + 3 arms per pool"
    return A


def rawpools(out, spec, pfx, *arms):
    assert pfx in ("jr", "jq"), f"prefix {pfx}: jr (job 184) or jq (job 185)"
    a, b = map(int, spec.split("-"))
    A = arms_of(arms)
    os.makedirs(out, exist_ok=True); n = nf = 0
    for s in range(a, b + 1):
        R = {name: {r["id"]: r for r in rows(f"{d}/c{s}.jsonl")} for name, d in A}
        ids = sorted(R["fixbase"])
        assert all(set(R[k]) == set(ids) for k in R), f"seed {s}: arms cover different topics"
        assert all(R[k][i]["prompt"] == R["fixbase"][i]["prompt"] for k in R for i in ids), f"seed {s}: prompts differ"
        for h, part in enumerate(halves(ids)):
            for name, _ in A:
                J = [{"id": f"{i}_s{s}", "prompt": R[name][i]["prompt"], "poem": R[name][i]["poem"]} for i in part]
                bad = [r["id"] for r in J if len(r["poem"].split("\n")) != 4]
                assert not bad, f"{name} seed {s}: poems without exactly 4 lines (build_pool would drop them silently): {bad}"
                wrows(f"{out}/{pfx}_{name}_s{s}h{h}.jsonl", J); n += len(J); nf += sum(not R[name][i]["all_pass"] for i in part)
    print(f"  raw pools (no ๆ write-back): arms {[x for x, _ in A]}, seeds {a}-{b}, {n} poems ({nf} form-failing, judged too) in {2 * (b - a + 1)} pool pairs")


def decide(clean, aligned):
    """Pre-registered (FIX_TRAINING_PREREG.md): CONTINUE if clean >= +5pp with the lower 95% bound > 0 and aligned
    >= -3pp; KILL if clean < +2pp; otherwise GREY."""
    if clean["diff"] >= 5 and clean["ci"][0] > 0 and aligned["diff"] >= -3: return "CONTINUE"
    if clean["diff"] < 2: return "KILL"
    return "GREY"


def labels(prefix, a, b, root_pools="runs"):
    """{(src, topic, seed): {clean, aligned}} for judged system poems; anchor clean share per clean pool"""
    lab = {"clean": {}, "aligned": {}}; anchors = {}
    for s in range(a, b + 1):
        for h in (0, 1):
            for kind, inst in (("clean", "CLEAN"), ("aligned", "ALIGN")):
                d = f"{root_pools}/{prefix}_{inst}{s}{h}"
                km = json.load(open(f"{d}/keymap.json", encoding="utf-8"))
                res = {r["item"]: r for r in json.load(open(f"{d}/run1.json", encoding="utf-8"))}
                assert set(res) == set(km), f"{d}: judge output does not cover the pool"
                hum = []
                for k, v in km.items():
                    r = res[k]
                    val = (not any(e.get("severity") in ("critical", "major") for e in r["errors"])) if kind == "clean" else int(r["align"]) >= 4
                    if v["src"] == "human": hum.append(val); continue
                    t, ss = v["id"].rsplit("_s", 1); assert int(ss) == s, f"{d}: {v} is not seed {s}"
                    assert (v["src"], t, s) not in lab[kind], f"{v} judged twice"
                    lab[kind][(v["src"], t, s)] = val
                if kind == "clean": anchors[d] = sum(hum) / max(len(hum), 1)
    return lab, anchors


def score(out, spec, prefix, *arm_specs, root_pools="runs"):
    import fixbase_pipe
    a, b = map(int, spec.split("-"))
    A = arms_of(arm_specs)
    form = {}
    for name, d in A:
        for s in range(a, b + 1):
            for r in rows(f"{d}/c{s}.jsonl"): form[(name, r["id"], s)] = bool(r["all_pass"])
    lab, anchors = labels(prefix, a, b, root_pools)
    void = {d: v for d, v in anchors.items() if v < 0.70}
    for kind in ("clean", "aligned"):
        assert set(lab[kind]) == set(form), f"{kind}: judged poems differ from the candidates"
    full = {kind: {k: form[k] and lab[kind][k] for k in form} for kind in ("clean", "aligned")}
    full["all-three"] = {k: full["clean"][k] and full["aligned"][k] for k in form}
    full["form"] = dict(form); full["judged-clean"] = dict(lab["clean"]); full["judged-aligned"] = dict(lab["aligned"])
    R = {"anchor_clean_share": anchors, "void_pools": sorted(void)}
    for name, _ in A[1:]:
        R[name] = {}
        for kind, L in full.items():
            c = {k[1:]: v for k, v in L.items() if k[0] == "fixbase"}; t = {k[1:]: v for k, v in L.items() if k[0] == name}
            st = R[name][kind] = fixbase_pipe.paired_stats(c, t)
            print(f"  {name:8s} {kind:9s} control {st['a']:5.1f}%  arm {st['b']:5.1f}%  diff {st['diff']:+.1f}pp "
                  f"({st['up']} up / {st['down']} down, n={st['n']})  95% CI [{st['ci'][0]:+.1f}, {st['ci'][1]:+.1f}]")
        R[name]["decision"] = decide(R[name]["clean"], R[name]["aligned"])
        print(f"  {name}: DECISION (pre-registered) {R[name]['decision']}")
    os.makedirs(out, exist_ok=True)
    json.dump(R, open(f"{out}/RESULT_{prefix}.json", "w"), indent=1)
    if void:
        print(f"  VOID pools (anchors < 70% clean, judge_frozen §2.2): {void} - re-judge those pools before any decision"); sys.exit(3)


def phonuse(ctl, arm):
    import torch
    import canvas_bot as B, fixtrain_gates as G
    G.RATIOS = (1.0,)          # only the standard condition is reported (and r=1.0, which valid_losses needs)
    real = B.load_model
    def zeroed(path):
        m, V = real(path)
        with torch.no_grad(): m.rime.weight.zero_(); m.tone.weight.zero_()
        return m, V
    for d in (ctl, arm):
        m, _ = real(d); n = lambda w: float(w.detach().float().norm(dim=1).mean())
        print(f"  {d}: row norms token {n(m.tok.weight):.3f}, rime {n(m.rime.weight):.3f}, tone {n(m.tone.weight):.3f}")
        del m
        full = G.valid_losses(d)["standard"]
        B.load_model = zeroed
        try: z = G.valid_losses(d)["standard"]
        finally: B.load_model = real
        print(f"  {d}: standard valid loss {full:.4f}; rime+tone zeroed {z:.4f} (use = {z - full:+.4f} nats)")


def _test():
    import tempfile
    global FB
    t = tempfile.mkdtemp(); fb0, FB = FB, t
    os.makedirs(f"{t}/FIXED_b0/cands"); os.makedirs(f"{t}/x")
    stut = "ห้องห้องนี้\nฝนตก\nลมพัด\nใจเย็น"   # a model stutter: must reach the judge as written (no ๆ)
    for d, bad in ((f"{t}/FIXED_b0/cands", 3), (f"{t}/x", 1)):
        wrows(f"{d}/c0.jsonl", [{"id": f"T{i:02d}", "prompt": "ฝน", "poem": stut if i == 0 else "ฝนตก\nลม\nพัด\nมา", "all_pass": i != bad} for i in range(5)])
    rawpools(f"{t}/p", "0-0", "jr", f"fix21={t}/x")
    h0c, h0a, h1a = rows(f"{t}/p/jr_fixbase_s0h0.jsonl"), rows(f"{t}/p/jr_fix21_s0h0.jsonl"), rows(f"{t}/p/jr_fix21_s0h1.jsonl")
    # ALL candidates (form-failing ones too): equal counts per arm and pool by construction
    assert [r["id"] for r in h0c] == [r["id"] for r in h0a] == ["T00_s0", "T01_s0", "T02_s0"], (h0c, h0a)
    assert [r["id"] for r in h1a] == ["T03_s0", "T04_s0"] and h0c[0]["poem"] == stut and "ๆ" not in h0a[0]["poem"]
    # a candidate that is not 4 lines stops the export (build_pool would drop it silently)
    os.makedirs(f"{t}/y"); wrows(f"{t}/y/c0.jsonl", [dict(r, poem="ฝน\nตก\nลม") if r["id"] == "T02" else r for r in rows(f"{t}/x/c0.jsonl")])
    try: rawpools(f"{t}/q", "0-0", "jq", f"fix5={t}/y"); raise AssertionError("3-line poem not caught")
    except AssertionError as e: assert "4 lines" in str(e), e
    # scoring: fake judged pools; fix21 clean on everything it passed, control fails T00
    for inst in ("CLEAN", "ALIGN"):
        for h in (0, 1):
            d = f"{t}/runs/FXT_{inst}0{h}"; os.makedirs(d)
            km, res = {}, []
            for name in ("fixbase", "fix21"):
                for r in rows(f"{t}/p/jr_{name}_s0h{h}.jsonl"):
                    k = f"{name}{r['id']}"; km[k] = {"src": name, "id": r["id"]}
                    bad = name == "fixbase" and r["id"] == "T00_s0"
                    res.append({"item": k, "errors": [{"severity": "major"}] if bad else [], "align": 2 if bad else 5})
            for j in range(4):
                km[f"h{j}"] = {"src": "human", "id": f"H{j}"}
                res.append({"item": f"h{j}", "errors": [{"severity": "major"}] if (h == 1 and inst == "CLEAN" and j < 2) else [], "align": 5})
            json.dump(km, open(f"{d}/keymap.json", "w")); json.dump(res, open(f"{d}/run1.json", "w"))
    try:
        score(f"{t}/o", "0-0", "FXT", f"fix21={t}/x", root_pools=f"{t}/runs"); raise AssertionError("void pool not caught")
    except SystemExit as e:
        assert e.code == 3
    R = json.load(open(f"{t}/o/RESULT_FXT.json"))
    assert R["void_pools"] == [f"{t}/runs/FXT_CLEAN01"], R["void_pools"]
    c = R["fix21"]["clean"]   # control clean: T01,T02,T04 (T00 judged bad, T03 form-fail); arm: T00,T02,T03,T04 (T01 form-fail)
    assert R["fix21"]["judged-clean"]["a"] == 80 and R["fix21"]["judged-clean"]["b"] == 100 and R["fix21"]["form"]["b"] == 80
    assert c["n"] == 5 and abs(c["a"] - 60) < 1e-9 and abs(c["b"] - 80) < 1e-9 and c["up"] == 2 and c["down"] == 1, c
    assert decide({"diff": 6, "ci": (1, 11)}, {"diff": -2}) == "CONTINUE" and decide({"diff": 6, "ci": (1, 11)}, {"diff": -4}) == "GREY"
    assert decide({"diff": 1.9, "ci": (-3, 7)}, {"diff": 0}) == "KILL" and decide({"diff": 3, "ci": (-3, 9)}, {"diff": 0}) == "GREY"
    FB = fb0
    print("fixon_raw: tests pass")


if __name__ == "__main__":
    cmd, args = sys.argv[1], sys.argv[2:]
    {"rawpools": rawpools, "score": score, "phonuse": phonuse, "test": _test}[cmd](*args)