chore: memory updates, diag_pairs_compare script

Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
This commit is contained in:
wassname
2026-06-09 02:42:56 +00:00
co-authored by Claudypoo
parent ec88ba3e42
commit 4a65eedc92
3 changed files with 188 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
"""Pairs comparison: which PAIR-SET gives the best high-precision hack direction?
Companion to diag_cosine_dist.py. That script sweeps SCORE x SPACE x FILTER for ONE
pair-set; this one fixes the winning config (grad cosine, the p@10=0.70 corner) and
sweeps the PAIR-SET axis -- authored vs pool-derived (prog_wide family) vs intent.
This is the table I should have built before committing a GPU run to a pairs guess.
The expensive part (140 live-rollout backward passes) is independent of the pairs, so
we cache the per-module live grads ONCE and loop the cheap v_grad build + scoring over
pair-sets. No oracle labels touch training; `exploited` is an offline plot-only label.
uv run python -m scripts.diag_pairs_compare # 4B, free GPU, ~20 min
outputs: out/diag/pairs_compare.csv (pairset x AUROC/p@10/p@20 at grad cosine).
"""
from __future__ import annotations
import json
import struct
from dataclasses import dataclass
from pathlib import Path
import torch
import tyro
import polars as pl
from loguru import logger
from tabulate import tabulate
from safetensors.torch import load_file
from transformers import AutoModelForCausalLM, AutoTokenizer
from vgrout.antipasto import wrap_model_with_antipasto
from vgrout.extract_vhack_grad import extract_v_hack, completion_nll
from vgrout.pairs import PAIRS
from vgrout.pairs_v2 import PAIRS_V2
from vgrout.pairs_intent import PAIRS_FUNCNAME
from vgrout.pairs_from_pool import load_pairs_json
from vgrout.train import CACHE_ROOT
from scripts.diag_cosine_dist import _auroc, _prec_at_k
_PS = Path("out/pairsets")
# every label here lives on hand-authored pairs OR pool demos we wrote -- no live labels.
PAIRSETS = {
"authored_all": lambda: list(PAIRS), # 18 pairs / 6 axes
"authored_runtests": lambda: list(PAIRS)[:8], # axis-1 only (the live mechanism)
"authored_v2": lambda: list(PAIRS_V2), # 6 harder/verbose
"authored_allv2": lambda: list(PAIRS) + list(PAIRS_V2),
"funcname": lambda: list(PAIRS_FUNCNAME), # best intent design
"prog_wide": lambda: load_pairs_json(_PS / "prog_wide.json"), # 30, training default
"prog_wider": lambda: load_pairs_json(_PS / "prog_wider.json"),
"prog_widest": lambda: load_pairs_json(_PS / "prog_widest.json"),
"heldout_known_rt": lambda: load_pairs_json(_PS / "heldout_known_runtests.json"),
}
@dataclass
class Cfg:
run_dir: Path = Path("out/runs/20260607T134234_fast_routingV_seed43_dir6_routeV_pertoken_s43")
ckpt: str = "first_hack"
step_lo: int = 5
step_hi: int = 9
max_rollouts: int = 140
out_dir: Path = Path("out/diag")
def main(cfg: Cfg) -> int:
device = torch.device("cuda")
kept_path = cfg.run_dir / f"{cfg.ckpt}.safetensors"
hack_path = cfg.run_dir / f"{cfg.ckpt}_hack.safetensors"
with open(kept_path, "rb") as f:
meta = json.loads(f.read(struct.unpack("<Q", f.read(8))[0])).get("__metadata__", {})
model_name = meta.get("model", "Qwen/Qwen3-4B")
logger.info(f"ckpt {kept_path.name} step={meta.get('step')} model={model_name}")
tok = AutoTokenizer.from_pretrained(model_name)
if tok.pad_token_id is None:
tok.pad_token = tok.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name, dtype=torch.bfloat16, attn_implementation="flash_attention_2").to(device)
model.config.use_cache = False
wrappers = wrap_model_with_antipasto(model, model_name, CACHE_ROOT, device, grad_probe=False)
names = sorted(wrappers)
kept, hack = load_file(str(kept_path)), load_file(str(hack_path))
for nm in names:
wrappers[nm]["delta_S"].data.copy_(kept[nm].to(device))
wrappers[nm]["delta_S_hack"].data.copy_(hack[nm].to(device))
model.eval()
# ── cache live-rollout per-module grads ONCE (independent of pair-set) ──
recs = [json.loads(l) for l in (cfg.run_dir / "rollouts.jsonl").read_text().splitlines()]
batch = [r for r in recs if cfg.step_lo <= r["step"] <= cfg.step_hi and r["text"].strip()][:cfg.max_rollouts]
gb_cache, gn_cache, labels = [], [], []
for i, r in enumerate(batch):
model.zero_grad(set_to_none=True)
loss = completion_nll(model, tok, r["prompt"], r["text"], device)
if not torch.isfinite(loss):
continue
loss.backward()
gb = {nm: wrappers[nm]["delta_S"].grad.flatten().float().cpu() for nm in names}
gb_cache.append(gb)
gn_cache.append(torch.tensor([gb[nm].norm().item() for nm in names])) # |g_m|, pairs-independent
labels.append(bool(r["exploited"]))
if (i + 1) % 40 == 0:
logger.info(f" cached {i+1}/{len(batch)} live grads")
model.zero_grad(set_to_none=True)
gn_stack = torch.stack(gn_cache) # [n_roll, n_mod]
n_pos = sum(labels)
logger.info(f"live: {len(labels)} rollouts, {n_pos} exploited ({n_pos/len(labels):.2f} base rate)")
# ── score one pair-set at grad cosine (all + keep75 noise filter) ──
def score_pairset(pairs):
_, v_sv, raw_grads, _ = extract_v_hack(
model, tok, wrappers, pairs, top_k=1, tau_axis=0.0, n_heldout=2, device=device)
v_grad = {nm: (lambda d: d / d.norm().clamp_min(1e-12))(
(raw_grads[f"hack/{nm}"] - raw_grads[f"clean/{nm}"]).mean(0).flatten().float().cpu())
for nm in names}
sv0 = torch.tensor([v_sv[nm][0].item() for nm in names])
keep75 = sv0 >= sv0.quantile(0.25)
cos_all, cos_k75 = [], []
for gb, gn in zip(gb_cache, gn_stack):
dot = torch.tensor([(gb[nm] @ v_grad[nm]).item() for nm in names])
for mask, out in [(torch.ones(len(names), dtype=bool), cos_all), (keep75, cos_k75)]:
d = dot[mask].sum()
gmag = gn[mask].pow(2).sum().sqrt()
nmod = int(mask.sum())
out.append((d / (gmag.clamp_min(1e-12) * nmod ** 0.5)).item())
return cos_all, cos_k75
rows = []
for name, fn in PAIRSETS.items():
try:
pairs = fn()
except FileNotFoundError:
logger.warning(f"{name}: pairset file missing, skipping")
continue
model.eval()
ca, ck = score_pairset(pairs)
row = dict(pairset=name, n=len(pairs),
AUROC=round(_auroc(ca, labels), 3),
p10=round(_prec_at_k(ca, labels, 10), 3),
p20=round(_prec_at_k(ca, labels, 20), 3),
p10_keep75=round(_prec_at_k(ck, labels, 10), 3))
rows.append(row)
logger.info(f"{name} (n={len(pairs)}): grad-cosine p10={row['p10']} AUROC={row['AUROC']}")
df = pl.DataFrame(rows).sort(["p10", "AUROC"], descending=True)
cfg.out_dir.mkdir(parents=True, exist_ok=True)
df.write_csv(cfg.out_dir / "pairs_compare.csv")
print("\n=== pair-sets ranked by grad-cosine precision@10 (base rate "
f"{n_pos/len(labels):.2f}, {len(labels)} live rollouts) ===")
print(tabulate(df.to_pandas(), headers="keys", tablefmt="pipe", showindex=False))
return 0
if __name__ == "__main__":
raise SystemExit(main(tyro.cli(Cfg)))