javadtaghia's picture
Code release: unlearning-via-weight (history squashed before publication)
788274d
Raw History Blame Contribute Delete
7.28 kB
"""`uvw` command line: environment, weight hashing and frozen-config generation."""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from uvw import RUNS
def _cmd_env(args: argparse.Namespace) -> int:
from uvw.provenance import environment
print(json.dumps(environment(), indent=2))
return 0
def _cmd_hash(args: argparse.Namespace) -> int:
from uvw.models import load_pipeline, load_registry, weight_hashes
variant = load_registry()[args.variant]
print(json.dumps({"variant": variant.name, **weight_hashes(load_pipeline(variant))}, indent=2))
return 0
def _cmd_generate(args: argparse.Namespace) -> int:
"""Generate for a JSONL file of {prompt_id, prompt, seed}; save images and a records file."""
from uvw.generate import GenItem, generate, load_inference_config
from uvw.models import load_pipeline, load_registry, weight_hashes
from uvw.provenance import RunRecord, config_hash
cfg = load_inference_config()
if args.steps:
cfg["num_inference_steps"] = args.steps
variant = load_registry()[args.variant]
rows = [json.loads(line) for line in Path(args.prompts).read_text().splitlines() if line.strip()]
if args.limit:
rows = rows[: args.limit]
record = RunRecord(job="generate", variants=[variant.name], configs={"inference": config_hash(cfg)})
out = Path(args.out or RUNS / record.run_id)
(out / "images").mkdir(parents=True, exist_ok=True)
pipe = load_pipeline(variant)
record.weights = {f"{variant.name}/{k}": v for k, v in weight_hashes(pipe).items()}
items = [GenItem(r["prompt_id"], r.get("text") or r["prompt"], int(r["seed"])) for r in rows]
with open(out / "generations.jsonl", "w") as f:
for g in generate(pipe, items, cfg, record.cost):
name = f"{g.item.prompt_id}_s{g.item.seed}.png"
if not args.no_images:
g.image.save(out / "images" / name)
f.write(json.dumps({**asdict(g.item), "image": name, "image_sha256": g.image_sha256,
"config_hash": g.config_hash, "variant": variant.name}) + "\n")
record.outputs = [str(out / "generations.jsonl")]
record.finish()
print(record.write(out))
return 0
def _cmd_splits(args: argparse.Namespace) -> int:
"""Build sources → template-level splits → leakage report → data/splits/."""
from uvw import ROOT
from uvw.data import leakage
from uvw.data.sources import build_all
from uvw.data.splits import assign, write_splits
out = Path(args.out or ROOT / "data" / "splits")
out.mkdir(parents=True, exist_ok=True)
df, excluded = build_all()
emb = leakage.clip_text_embeddings(df.text.tolist())
df["group_id"] = leakage.group_templates(df, emb, threshold=args.threshold).values
df["_row"] = range(len(df))
df = assign(df)
emb, df = emb[df._row.to_numpy()], df.drop(columns="_row")
overlap = leakage.template_overlap(df)
exact = leakage.exact_duplicates(df)
near = leakage.near_duplicates(df, emb, threshold=args.threshold)
near.to_csv(out / "near_duplicates.csv", index=False)
report = {"rows": len(df), "excluded": len(excluded), "template_overlap": len(overlap),
"exact_cross_split_duplicates": len(exact), "near_cross_split_duplicates": len(near),
"threshold": args.threshold}
if len(exact) or len(near):
raise SystemExit(f"Cross-split duplicates remain: {report}")
if len(overlap):
print(overlap.head())
raise SystemExit("Template overlap across splits: refusing to write.")
manifest = write_splits(df, out)
(out / "excluded.jsonl").write_text("".join(json.dumps(e) + "\n" for e in excluded))
exact.to_csv(out / "exact_duplicates.csv", index=False)
(out / "leakage_report.json").write_text(json.dumps(report, indent=2))
print(json.dumps(report, indent=2))
for name, info in sorted(manifest["files"].items()):
print(f"{name:28s} rows={info['rows']:6d} templates={info['templates']:5d} {info['by_role']}")
return 0
def _cmd_evaluate(args: argparse.Namespace) -> int:
from uvw.evaluate import evaluate
for concept in args.concepts:
out = evaluate(args.variant, concept, args.split, n_seeds=args.seeds, limit=args.limit,
out=Path(args.out) / f"{args.variant}-{concept}-{args.split}" if args.out else None,
keep_images=args.keep_images, edits=Path(args.edits) if args.edits else None)
print(out)
return 0
def _cmd_edit_agent(args: argparse.Namespace) -> int:
"""Run the editor agent locally (ChatGPT plan via atbot) against a remote tool server."""
from uvw import RUNS
from uvw.harness.agent import run_editor
from uvw.harness.server import RemoteSession
session = RemoteSession(args.url, RUNS / "edits-local" / args.run_id)
summary = run_editor(session, editor_model=args.editor, model_name=args.model_name)
print(json.dumps(summary, indent=2))
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="uvw")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("env", help="Print the environment record")
p = sub.add_parser("hash", help="Hash a variant's weights")
p.add_argument("variant")
p = sub.add_parser("generate", help="Generate images with the frozen inference config")
p.add_argument("variant")
p.add_argument("prompts", help="JSONL with prompt_id, text (or prompt), seed")
p.add_argument("--out")
p.add_argument("--limit", type=int)
p.add_argument("--steps", type=int, help="Override steps (smoke tests only)")
p.add_argument("--no-images", action="store_true", help="Keep hashes and scores only")
p = sub.add_parser("splits", help="Build frozen dev/val/test prompt splits")
p.add_argument("--out")
p.add_argument("--threshold", type=float, default=0.95)
p = sub.add_parser("evaluate", help="Generate + score a variant on concept splits")
p.add_argument("variant")
p.add_argument("concepts", nargs="+", choices=["nudity", "vangogh", "church", "coco"])
p.add_argument("--split", default="dev", choices=["dev", "val"]) # test: evaluator only (003)
p.add_argument("--seeds", type=int, default=1)
p.add_argument("--limit", type=int, help="Max prompts per role (pilots)")
p.add_argument("--out")
p.add_argument("--keep-images", default="none", choices=["none", "benign", "all"])
p.add_argument("--edits", help="edited_tensors.safetensors from a committed candidate")
p = sub.add_parser("edit-agent", help="Run the editor agent against a tool server")
p.add_argument("--url", default="http://127.0.0.1:8765")
p.add_argument("--run-id", required=True)
p.add_argument("--editor", default="gpt-6-astra")
p.add_argument("--model-name", default="Stable Diffusion v1.4")
args = parser.parse_args(argv)
commands = {"edit-agent": _cmd_edit_agent, "evaluate": _cmd_evaluate, "env": _cmd_env, "hash": _cmd_hash, "generate": _cmd_generate, "splits": _cmd_splits}
return commands[args.command](args)
if __name__ == "__main__":
sys.exit(main())