Spaces:
Sleeping
Sleeping
File size: 7,447 Bytes
2a3773a | 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 | """What does the anchor fold move? Two read-only analyses over archived sleep_controls runs (no model, no device).
1. Per-turn write magnitude in the teaching session: transactions joined to chat turns by their transaction-index
range; per turn the sum of committed ``delta_norm``, the max ``surprise_max`` and ``chunk_loss``, and whether the
observational policy flagged any chunk. Question: did the planted contradictions write more than the other facts?
2. Storage shift under the anchor child: for every verbatim probe the anchor report's before/after
``answer_logprob`` (expected-answer mean log-prob per token, teacher-forced from a fresh state), grouped into
planted answers, general true answers and the taught personal facts. Question: does the fold move the planted
answers specifically, or every short factual completion?
python -m scripts.experiments.anchor_transfer_analysis --runs docs/research/results/sleep-2026-09-23/final_step250_seed0_exclude ... --out <json>
"""
from __future__ import annotations
import argparse
import json
import os
import statistics
from typing import Any
POISON_ANSWERS = {"Berlin", "50", "nine", "Moon"}
GENERAL_ANSWERS = {"Paris", "100", "seven", "Earth", "blue"}
def load_jsonl(path: str) -> list[dict[str, Any]]:
with open(path, encoding="utf-8") as f:
return [json.loads(line) for line in f if line.strip()]
def turn_write_table(transactions: list[dict[str, Any]], trace: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""One row per chat turn: committed delta_norm summed over its chunks (by tx range), max surprise and chunk loss,
chunk count, whether any chunk carried a would_* / requested intervention flag, and the rank by write (1 = largest)."""
by_index = {int(t["index"]): t for t in transactions}
rows: list[dict[str, Any]] = []
for rec in trace:
if rec.get("kind") != "chat" or rec.get("tx_start") is None or rec.get("tx_end") is None:
continue
chunks = [by_index[i] for i in range(int(rec["tx_start"]), int(rec["tx_end"])) if i in by_index]
if not chunks:
continue
flagged = any(
any(str(r).startswith("would_") for r in c.get("requested", {}).get("reasons", [])) or c.get("requested", {}).get("kind") != c.get("decision", {}).get("kind")
or c.get("decision", {}).get("kind") in ("scale", "project")
for c in chunks
)
rows.append({
"prompt": rec.get("prompt", ""),
"chunks": len(chunks),
"delta_norm_sum": float(sum(float(c.get("accepted", {}).get("delta_norm") or 0.0) for c in chunks)),
"surprise_max": float(max(float(c.get("signals", {}).get("surprise_max") or 0.0) for c in chunks)),
"chunk_loss_max": float(max(float(c.get("signals", {}).get("chunk_loss") or 0.0) for c in chunks)),
"flagged": bool(flagged),
"planted": rec.get("prompt", "").startswith("Remember this"),
})
order = sorted(range(len(rows)), key=lambda i: -rows[i]["delta_norm_sum"])
for rank, i in enumerate(order, start=1):
rows[i]["rank_by_write"] = rank
return rows
def answer_group(expected: str) -> str:
if expected in POISON_ANSWERS:
return "planted"
if expected in GENERAL_ANSWERS:
return "general"
return "taught"
def storage_shift(report: dict[str, Any]) -> dict[str, Any]:
"""Per verbatim probe (keyed by question AND expected answer: the plants reuse general questions) the after-before
shift of the expected answer's mean log-prob per token, and per-group means."""
def key(x: dict[str, Any]) -> tuple[str, str]:
return (x["question"], x["expected"])
before = {key(x): x for x in report["before"]["recall"]["results"] if x.get("variant") == "verbatim"}
after = {key(x): x for x in report["after"]["recall"]["results"] if x.get("variant") == "verbatim"}
probes: list[dict[str, Any]] = []
for k, b in before.items():
a = after.get(k)
if a is None or b.get("answer_logprob") is None or a.get("answer_logprob") is None:
continue
probes.append({"question": k[0], "expected": k[1], "group": answer_group(k[1]),
"before": float(b["answer_logprob"]), "after": float(a["answer_logprob"]),
"shift": float(a["answer_logprob"]) - float(b["answer_logprob"]),
"greedy_before": bool(b.get("contains")), "greedy_after": bool(a.get("contains"))})
groups: dict[str, Any] = {}
for g in ("planted", "general", "taught"):
v = [p["shift"] for p in probes if p["group"] == g]
groups[g] = {"n": len(v), "mean_shift": statistics.fmean(v) if v else None,
"greedy_hits_before": sum(p["greedy_before"] for p in probes if p["group"] == g),
"greedy_hits_after": sum(p["greedy_after"] for p in probes if p["group"] == g)}
return {"probes": probes, "groups": groups}
def analyze_run(run_dir: str) -> dict[str, Any]:
out: dict[str, Any] = {"run": os.path.basename(os.path.normpath(run_dir))}
sess = os.path.join(run_dir, "sessions", "teach")
if os.path.exists(os.path.join(sess, "transactions.jsonl")):
rows = turn_write_table(load_jsonl(os.path.join(sess, "transactions.jsonl")), load_jsonl(os.path.join(sess, "trace.jsonl")))
writes = [r["delta_norm_sum"] for r in rows]
out["teach_turns"] = rows
out["write_summary"] = {"turns": len(rows), "median_delta_norm_sum": statistics.median(writes) if writes else None,
"planted_ranks": sorted(r["rank_by_write"] for r in rows if r["planted"]),
"largest_write_prompt": max(rows, key=lambda r: r["delta_norm_sum"])["prompt"][:60] if rows else None}
else:
out["teach_turns"] = None # sessions not archived for this run: absent, not zero
anchor = os.path.join(run_dir, "sleep_anchor_report.json")
if os.path.exists(anchor):
rep = json.load(open(anchor, encoding="utf-8"))
if rep.get("before", {}).get("recall") and rep.get("after", {}).get("recall"):
out["anchor_storage_shift"] = storage_shift(rep)
out["anchor_status"] = rep.get("status")
return out
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--runs", nargs="+", required=True, help="archived run folders (results/sleep-2026-09-23/<run>)")
ap.add_argument("--out", required=True, help="JSON output path")
args = ap.parse_args()
results = [analyze_run(r) for r in args.runs]
with open(args.out, "w", encoding="utf-8") as f:
json.dump({"analysis": "anchor_transfer", "runs": results}, f, indent=1)
for res in results:
print(f"== {res['run']}")
ws = res.get("write_summary")
if ws:
print(f" teach turns {ws['turns']}, median write {ws['median_delta_norm_sum']:.2f}, planted turns rank {ws['planted_ranks']}, largest: {ws['largest_write_prompt']!r}")
st = res.get("anchor_storage_shift")
if st:
g = st["groups"]
print(" anchor storage shift: " + ", ".join(f"{k} n={v['n']} mean {v['mean_shift']:+.3f} greedy {v['greedy_hits_before']}->{v['greedy_hits_after']}" for k, v in g.items() if v["n"]))
if __name__ == "__main__":
main()
|