Spaces:
Sleeping
Sleeping
File size: 16,011 Bytes
0c05ae5 7be1033 0c05ae5 fbb0463 0c05ae5 7be1033 0c05ae5 7be1033 0c05ae5 7be1033 0c05ae5 7be1033 0c05ae5 fbb0463 0c05ae5 7be1033 2ca9f6f 7be1033 0c05ae5 9e2f7c1 7be1033 8dddcba 7be1033 2ca9f6f 7be1033 2ca9f6f 7be1033 2ca9f6f 7be1033 0c05ae5 7be1033 fbb0463 0c05ae5 7be1033 0c05ae5 7be1033 2ca9f6f 9e2f7c1 0c05ae5 7be1033 0c05ae5 8dddcba 0c05ae5 1bde9ee 9e2f7c1 c10c824 9e2f7c1 1bde9ee 9e2f7c1 0c05ae5 7be1033 0c05ae5 8dddcba 7be1033 0c05ae5 | 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 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | """First report of the text rule contract on a TTT chat checkpoint: one JSON per mode, a README table, a manifest.
PYTHONPATH=<abs deps> python -m scripts.experiments.text_contract_report --checkpoint <dir> --out <dir> \
[--device mps] [--rule-set decorate] [--modes frozen,continued,in_context,replay_verify] [--seed 0]
Every mode runs the same contract on the same fixed-seed material; the learner is reloaded from the checkpoint
between modes so no lasting update leaks. The lasting update visits the stream in full passes by default; the archived
reports up to a8afb4b drew 20 episodes with replacement and 6 verification episodes, reproduced with
``--sampling draws --steps 20 --verify-episodes 6 --no-sequential-clean --choice-episodes 0``. Held-out chat NLL (SmolTalk test rows, pinned revision) is measured
inside each contract measurement when --chat-rows > 0 and is also the replay_verify mode's third check.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import time
from plastic.eval.text_contract import TextContractSpec, contract_split, run_text_contract
from plastic.eval.text_learner import MODES, TextRuleLearner
def _git_head() -> str:
"""The execution commit. A run launched from an exported snapshot of a commit (``git archive``, no repository)
names it through PLASTIC_CODE_COMMIT, so a long queue is not affected by later edits in the checkout."""
if os.environ.get("PLASTIC_CODE_COMMIT"):
return os.environ["PLASTIC_CODE_COMMIT"] + " (exported snapshot)"
try:
sha = subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, check=True).stdout.strip()
return sha + ("+dirty" if _dirty_paths() else "")
except Exception: # noqa: BLE001
return "unknown"
def _dirty_paths() -> list[str]:
"""Tracked files that differ from HEAD at launch, so '+dirty' says which files (a teammate's instruction edit is
not an experiment-code change). An exported snapshot has none by construction."""
if os.environ.get("PLASTIC_CODE_COMMIT"):
return []
try:
out = subprocess.run(["git", "status", "--porcelain", "--untracked-files=no"], capture_output=True, text=True, check=True).stdout
return sorted(line[3:] for line in out.splitlines() if line.strip())
except Exception: # noqa: BLE001
return []
def build_parser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--checkpoint", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--device", default="mps")
ap.add_argument("--rule-set", default="decorate", choices=["transform", "decorate"])
ap.add_argument("--modes", default="frozen,continued,in_context,replay_verify", help=f"comma-separated, from {', '.join(MODES)}")
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--n-heldout", type=int, default=8)
ap.add_argument("--min-reversed-heldout", type=int, default=6, help="the contract default; every archived decorate report measures on the seed-0 split with 6")
ap.add_argument("--situations", type=int, default=8)
ap.add_argument("--stream-episodes", type=int, default=17, help="one lesson per training composition by default (5 singles + 12 pairs)")
ap.add_argument("--eval-episodes-per-composition", type=int, default=2)
ap.add_argument("--poison-operator", default="#P")
ap.add_argument("--target", default="w0", choices=["w0", "all"])
ap.add_argument("--lr", type=float, default=1e-4)
ap.add_argument("--steps", type=int, default=20, help="lasting-update steps under --sampling draws (ignored under passes)")
ap.add_argument("--sampling", default="passes", choices=["passes", "draws"],
help="passes: every stream episode once per pass, shuffled; draws: --steps draws with replacement (the archived runs)")
ap.add_argument("--passes", type=int, default=1, help="full passes over the stream under --sampling passes")
ap.add_argument("--no-sequential-clean", action="store_true", help="skip the clean-again control for the sequential poison arm")
ap.add_argument("--name-permuted-control", action="store_true", help="also consume the content null: each name's answers computed by another composition of its arity")
ap.add_argument("--choice-episodes", type=int, default=2, help="first-situation choice items per composition (0 disables)")
ap.add_argument("--chat-rows", type=int, default=8, help="held-out SmolTalk rows for the chat-NLL forgetting measure (0 disables)")
ap.add_argument("--verifier", default="v2", choices=["v1", "v2"], help="v1: held-in checks scored with the fast path frozen; v2: adapting, plus the first-situation exact")
ap.add_argument("--no-sequential-poison", action="store_true", help="skip the poison-on-top-of-accepted-clean arm")
ap.add_argument("--verify-episodes", type=int, default=34, help="held-in verification episodes (3 situations each, cycling over the training compositions) for replay_verify; the first-situation check is over this many items")
ap.add_argument("--unstated-rules", action="store_true", help="no definitions in any preface; the rules come from the worked examples only")
ap.add_argument("--poison-kind", default="consistent", choices=["consistent", "inconsistent"],
help="consistent: the false definition is stated with the false answers; inconsistent: true definitions stated, false answers")
return ap
def spec_from_args(args: argparse.Namespace) -> TextContractSpec:
"""The one place the command line becomes a contract spec; the learner's held-in material and the logged split
both come from ``contract_split`` of this spec (FABLE-41B-211)."""
return TextContractSpec(n_heldout=args.n_heldout, split_seed=args.seed, situations_per_episode=args.situations,
eval_episodes_per_composition=args.eval_episodes_per_composition, stream_episodes=args.stream_episodes,
probe_situations=args.situations, poison_operator=args.poison_operator, rule_set=args.rule_set,
sequential_poison=not args.no_sequential_poison, stated_rules=not args.unstated_rules, poison_kind=args.poison_kind,
min_reversed_heldout=args.min_reversed_heldout, sequential_clean=not args.no_sequential_clean,
choice_episodes_per_composition=args.choice_episodes, name_permuted_control=args.name_permuted_control)
def _fmt(x: float | None, spec: str = "+.2f") -> str:
return "n/a" if x is None else format(x, spec)
def detail_row(mode: str, rep: dict) -> str:
"""Coverage of the clean lasting update, the first-situation choice, and the sequential poison beside its clean-again control."""
rc = (rep.get("consume_records") or {}).get("clean") or {}
cov = "n/a" if "compositions_trained" not in rc else f"{rc['compositions_trained']}/{rc['compositions_in_stream']} ({rc['steps']}, {rc['poisoned_steps']})"
ch = rep.get("choice") or {}
def acc(key: str, part: str) -> str:
v = (ch.get(key) or {}).get(part)
return "n/a" if not v else f"{v['accuracy']:.2f}"
def arm(block: dict | None) -> str:
if not block:
return "n/a"
changes = block.get("verify_first_changes")
moved = "n/a" if changes is None else (", ".join(f"{c['ops']} {int(c['before'])}β{int(c['after'])}" for c in changes) or "none")
return f"{block.get('accepted')}, {moved}"
sq, sc = rep.get("sequential_poison"), rep.get("sequential_control")
vs = "n/a"
if sq and sc and sq.get("choice_heldout_delta") is not None and sc.get("choice_heldout_delta") is not None:
vs = f"{sq['choice_heldout_delta'] - sc['choice_heldout_delta']:+.2f} / {sq['choice_train_delta'] - sc['choice_train_delta']:+.2f}"
nov = ch.get("heldout_novel_accuracy") or {}
nov_cell = "" if not nov else f" ({_fmt(nov.get('before'), '.2f')} β {_fmt(nov.get('after'), '.2f')})"
tb = rep.get("template_baseline") or {}
tpl = "n/a" if not tb else f"{tb['second_situation']:.2f} / {tb['exact_after_first']:.2f}"
return (f"| {mode} | {tpl} | {cov} | {acc('before', 'heldout')} β {acc('after', 'heldout')}{nov_cell} | {acc('before', 'train')} β {acc('after', 'train')} "
f"| {arm(sc)} | {arm(sq)} | {vs} |")
def paired_rows(mode: str, rep: dict) -> list[str]:
paired = (rep.get("choice") or {}).get("paired") or {}
labels = {"after_vs_before": "clean update vs before", "clean_again_vs_after": "clean again vs after clean",
"poison_sequential_vs_after": "poison on top vs after clean", "poison_vs_before": "poison from snapshot vs before",
"format_vs_before": "format-only vs before", "names_vs_before": "name-permuted (content null) vs before"}
rows = []
for key, label in labels.items():
block = paired.get(key)
if not block:
continue
def cell(g: str) -> str:
v = block.get(g)
return "n/a" if not v else f"{v['mean_change']:+.2f} ({v['improved']}/{v['worsened']} of {v['n']})"
rows.append(f"| {mode} | {label} | {cell('train')} | {cell('heldout_novel')} | {cell('heldout_duplicate')} |")
return rows
def main() -> None:
args = build_parser().parse_args()
from plastic.backends.ttt_lm.backend import TTTBackend
from plastic.sleep.ttt import heldout_nll, load_replay_conversations
os.makedirs(args.out, exist_ok=True)
log_path = os.path.join(args.out, "log.txt")
def log(msg: str) -> None:
print(msg, flush=True)
with open(log_path, "a", encoding="utf-8") as f:
f.write(msg + "\n")
t0 = time.time()
spec = spec_from_args(args)
train, heldout = contract_split(spec) # the same split the contract measures on; the learner's held-in material comes from it
log(f"[setup] rule set {spec.rule_set}: {len(train)} training compositions, {len(heldout)} held-out pairs {[' '.join(c) for c in heldout]}")
chat_rows = load_replay_conversations("everyday-conversations", "test", args.chat_rows, args.seed + 1, log) if args.chat_rows > 0 else []
manifest = {"checkpoint": os.path.abspath(args.checkpoint), "device": args.device, "code_commit": _git_head(), "dirty_paths": _dirty_paths(), "started_at_unix": int(t0),
"rule_set": spec.rule_set, "spec": spec.__dict__ | {"n_words": list(spec.n_words)}, "modes": {}, "chat_rows": len(chat_rows)}
rows3 = ["| Mode | Comparison | Training compositions | Held-out, novel | Held-out, repeats a trained answer |", "| --- | --- | --- | --- | --- |"]
rows2 = ["| Mode | Untrained template copier, held-out (second situation / all later) | Clean stream: compositions trained (steps, poisoned steps) | First-situation choice, held-out: before β after (novel pairs only) | First-situation choice, training: before β after | Clean again: accepted, verify first-situation items changed | Poison on top: accepted, verify first-situation items changed | Poison on top vs clean again: choice held-out / training Ξ |",
"| --- | --- | --- | --- | --- | --- | --- | --- |"]
rows = ["| Mode | Held-out exact (adapt) before β after | of which first situation Ξ / later Ξ | Held-out nll (adapt) before β after | Held-out nll (no adapt) Ξ | Speed area before β after | Forgetting nll Ξ | Poison harm (nll) | Sequential poison: accepted, harm (nll) | Correction residual | Format-only gain exact (true) | Revert | Accepted good / refused bad | Verifier |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"]
for mode in [m.strip() for m in args.modes.split(",") if m.strip()]:
log(f"[mode] {mode}")
be = TTTBackend.load(args.checkpoint, device=args.device)
manifest["checkpoint_digest"] = be.checkpoint_digest
chat = (lambda: heldout_nll(be, chat_rows, 512)) if chat_rows else None
learner = TextRuleLearner(be, mode=mode, target=args.target, lr=args.lr, steps=args.steps, chat_nll=chat, verify_adapt=args.verifier == "v2",
verify_episodes=args.verify_episodes, sampling=args.sampling, passes=args.passes, log=log)
learner.train_compositions = train
learner.rule_set = spec.rule_set
learner.stated_rules = spec.stated_rules
rep = run_text_contract(learner, spec, seed=args.seed, chat_nll=chat)
rep["mode"] = mode
with open(os.path.join(args.out, f"contract_{mode}.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, indent=1)
tr, sp, fg, co, rv, acc, fo = rep["transfer"], rep["speed"], rep["forgetting"], rep["correction"], rep["revert"], rep["acceptance"], rep["format_only"]
sq = rep.get("sequential_poison")
seq_cell = "n/a" if sq is None else f"{sq['accepted']}, {sq['harm_nll']:+.3f}"
rows.append(f"| {mode} | {tr['before']['adapt']['exact']:.2f} β {tr['after']['adapt']['exact']:.2f} | {tr.get('delta_exact_first', float('nan')):+.2f} / {tr.get('delta_exact_after_first', float('nan')):+.2f} | {tr['before']['adapt']['nll']:.3f} β {tr['after']['adapt']['nll']:.3f} "
f"| {tr['delta_nll_no_adapt']:+.3f} | {sp['before']['area']:.2f} β {sp['after']['area']:.2f} | {fg['delta_nll']:+.3f} | {co['harm_nll']:+.3f} | {seq_cell} | {co['residual_nll']:+.3f} "
f"| {fo['gain_exact']:+.2f} ({fo['true_stream_gain_exact']:+.2f}) "
f"| {'ok' if rv['ok'] else 'GAP ' + format(rv['gap'], '.2e')} | {acc['accepted_good']} / {acc['refused_bad']} (n {acc['n_good']}/{acc['n_bad']}) | {rep.get('verifier') or 'n/a'} |")
manifest["modes"][mode] = {"seconds": rep["compute"]["wall_clock_s"], "decisions": rep["decisions"]}
log(rows[-1])
rows2.append(detail_row(mode, rep))
log(rows2[-1])
rows3.extend(paired_rows(mode, rep))
del learner, be
if args.device == "mps":
import torch
torch.mps.empty_cache()
manifest["seconds"] = round(time.time() - t0, 1)
with open(os.path.join(args.out, "manifest.json"), "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=1)
readme = (f"# Text rule contract report\n\nCode {manifest['code_commit']}, checkpoint digest `{manifest.get('checkpoint_digest', '?')[:12]}`, device {args.device}, "
f"rule set `{spec.rule_set}`, seed {args.seed}, {len(train)} training compositions, {len(heldout)} held-out pairs, {spec.situations_per_episode} situations per episode, "
f"stream {spec.stream_episodes} episodes, lasting update target {args.target}, lr {args.lr}, {args.steps} steps, poison operator `{spec.poison_operator}` ({spec.poison_kind}), "
f"rules {'stated' if spec.stated_rules else 'UNSTATED'} in every preface, {args.verify_episodes} verify episodes, "
f"{len(chat_rows)} held-out chat rows, lasting-update sampling {args.sampling}"
+ (f" ({args.passes} pass{'es' if args.passes != 1 else ''})" if args.sampling == "passes" else f" ({args.steps} draws)")
+ f", {spec.choice_episodes_per_composition} choice items per composition. Exact is teacher-forced (`exact_tf`). Every mode reloads the checkpoint.\n\n"
+ "\n".join(rows) + "\n\nCoverage, first-situation choice (chance is 1/candidates: 23 distinct outputs on the decoration set) and the sequential arms:\n\n" + "\n".join(rows2)
+ "\n\nPaired first-situation margin change per item (mean nats; improved / worsened of n), by group:\n\n" + "\n".join(rows3) + "\n")
with open(os.path.join(args.out, "README.md"), "w", encoding="utf-8") as f:
f.write(readme)
log(f"[done] {manifest['seconds']} s -> {args.out}")
if __name__ == "__main__":
main()
|