tracegrpo / scripts /analyze_scale_parser_failures.py
Dhruv1000's picture
Archive complete TraceGRPO project results and documentation (2026-10-09)
646ccfc verified
Raw History Blame Contribute Delete
3.55 kB
"""Read-only prompt-cluster audit of a stopped scale run's parser failures."""
from __future__ import annotations
from collections import Counter
import json
from pathlib import Path
import sys
import numpy as np
import torch
def audit(run_dir: Path, *, seed: int = 42, draws: int = 20_000) -> dict:
manifest = json.loads((run_dir / "run_manifest.json").read_text())
completed = json.loads((run_dir / "curves.json").read_text())
pending = json.loads((run_dir / "pending_step.json").read_text())
steps = len(completed) + 1
assert pending["optimizer_step"] == steps
selection = manifest["train_selection"]
order = torch.randperm(len(selection), generator=torch.Generator().manual_seed(
manifest["seed"])).tolist()[:steps * 64]
chosen = [selection[i] for i in order]
assert len(chosen) == steps * 64
labels = {row["prompt_id"]: int(row["target"]) for row in chosen}
assert len(labels) == len(chosen), "a prompt recurred during the first epoch"
failures = [json.loads(line) for line in (run_dir / "rollout_failures.jsonl").open()]
assert all(row["prompt_id"] in labels for row in failures)
assert len(failures) == sum(row["verifier_failures"] for row in completed) + pending["verifier_failures"]
by_prompt = Counter(row["prompt_id"] for row in failures)
counts = np.asarray([by_prompt[row["prompt_id"]] for row in chosen])
classes = np.asarray([int(row["target"]) for row in chosen])
rates = {name: float(counts[classes == label].sum() / (8 * (classes == label).sum()))
for name, label in (("SAFE", 0), ("VULNERABLE", 1))}
observed = rates["VULNERABLE"] - rates["SAFE"]
# The eight rollouts sharing a prompt are correlated. Resample/permute
# entire prompt groups rather than treating 7,680 rollouts as IID.
rng = np.random.default_rng(seed)
safe = counts[classes == 0]
vuln = counts[classes == 1]
bootstrap = np.empty(draws)
permutation = np.empty(draws)
for i in range(draws):
bootstrap[i] = (rng.choice(vuln, len(vuln), replace=True).mean() -
rng.choice(safe, len(safe), replace=True).mean()) / 8
perm = rng.permutation(counts)
permutation[i] = (perm[:len(vuln)].mean() - perm[len(vuln):].mean()) / 8
return {
"run_dir": str(run_dir), "seed": seed, "draws": draws,
"steps_including_pending": steps, "prompts": len(chosen),
"rollouts": len(chosen) * 8, "failures": len(failures),
"class_counts": {
name: {"prompts": int((classes == label).sum()),
"rollouts": int(8 * (classes == label).sum()),
"failures": int(counts[classes == label].sum()),
"prompts_with_failure": int((counts[classes == label] > 0).sum()),
"failure_rate": rates[name]}
for name, label in (("SAFE", 0), ("VULNERABLE", 1))},
"vulnerable_minus_safe_failure_rate": observed,
"prompt_cluster_bootstrap_95_interval": np.quantile(bootstrap, [.025, .975]).tolist(),
"prompt_label_permutation_two_sided_p": float(np.mean(np.abs(permutation) >= abs(observed))),
"prompts_with_multiple_failures": int((counts >= 2).sum()),
"failures_on_multiple_failure_prompts": int(counts[counts >= 2].sum()),
"max_failures_on_one_prompt": int(counts.max()),
"note": "Exploratory prompt-cluster comparison; no causal or equal-rate claim.",
}
if __name__ == "__main__":
print(json.dumps(audit(Path(sys.argv[1])), indent=2))