import argparse, json, os, re, sys, time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import torch from tqdm import tqdm from configs import get_config from configs.paths import LOG_DIR, dim_paths, ensure_dirs from src.detectors import BehaviorDetector from src.interventions import generate_with_alpha from src.utils import build_chat_prompt, get_device, load_model_and_tokenizer, read_json, read_jsonl, setup_logger, write_json def extract_boxed_letter(text): if not text: return None matches, idx = [], 0 while True: i = text.find("\\boxed", idx) if i < 0: break j = text.find("{", i) if j < 0: break depth, end = 0, -1 for k in range(j, len(text)): if text[k] == "{": depth += 1 elif text[k] == "}": depth -= 1 if depth == 0: end = k break if end > j: matches.append(text[j + 1:end].strip()) idx = end + 1 else: break if not matches: return None last = matches[-1].strip().upper() m = re.match(r"\(?\s*([ABCD])", last) return m.group(1) if m else None def repetition_score(text, tail_chars=400, ngram=30): tail = text[-tail_chars:] if len(text) > tail_chars else text if len(tail) < ngram * 2: return 0.0 seen, repeated, total = {}, 0, 0 for i in range(len(tail) - ngram): chunk = tail[i:i + ngram] total += 1 if chunk in seen: repeated += 1 else: seen[chunk] = 1 return repeated / total if total else 0.0 def think_tokens(tok, cot): seg = cot.split("")[0] if "" in cot else cot return len(tok(seg, add_special_tokens=False)["input_ids"]) def load_alpha1_baseline(results_dir, seed): candidates = [ os.path.join(results_dir, f"run_gpqa_d_gpqa_d_s{seed}.jsonl"), os.path.join(results_dir, "run_gpqa_d_gpqa_d_s64.jsonl"), ] baseline = {} for path in candidates: if not os.path.exists(path): continue for line in open(path, encoding="utf-8"): line = line.strip() if not line: continue try: r = json.loads(line) except Exception: continue if abs(float(r.get("alpha", -999)) - 1.0) < 1e-6: baseline[int(r["problem_idx"])] = r if baseline: return baseline, path return baseline, None def main(): ap = argparse.ArgumentParser() ap.add_argument("--dimension", default="monitoring") ap.add_argument("--alpha", type=float, default=0.7) ap.add_argument("--seed", type=int, default=64) ap.add_argument("--sel-suffix", default="_allmonoV2") ap.add_argument("--gen-max-tokens", type=int, default=None) ap.add_argument("--data-path", default=None) ap.add_argument("--out-suffix", default="_alpha07_s64") ap.add_argument("--force", action="store_true") args = ap.parse_args() ensure_dirs(args.dimension) cfg = get_config(args.dimension) p = dim_paths(args.dimension) gen_max = args.gen_max_tokens or cfg.GEN_MAX_NEW_TOKENS temperature = getattr(cfg, "DEFAULT_TEMPERATURE", 0.6) top_p = getattr(cfg, "DEFAULT_TOP_P", 0.95) data_path = args.data_path or os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "gpqa_d.jsonl" ) log = setup_logger( "gpqa_layer_ablation_alpha07", os.path.join(LOG_DIR, f"run_gpqa_d_layer_ablation{args.out_suffix}.log"), ) log.info("=" * 72) log.info("GPQA-D layer ablation: selected vs all candidate layers") log.info(f"alpha={args.alpha} seed={args.seed} gen_max={gen_max} temp={temperature} top_p={top_p}") log.info(f"data_path={data_path}") log.info("=" * 72) if not os.path.exists(p.DIRECTIONS): log.error(f"missing {p.DIRECTIONS}") sys.exit(1) dblob = torch.load(p.DIRECTIONS, map_location="cpu", weights_only=False) directions_all = {int(L): v for L, v in dblob["directions"].items()} all_layers = sorted(directions_all.keys()) base, ext = os.path.splitext(p.SELECTED_LAYERS) sel_path = f"{base}{args.sel_suffix}{ext}" if not os.path.exists(sel_path): log.error(f"missing selected layer file: {sel_path}") sys.exit(2) selected_json = read_json(sel_path) selected_layers = [int(L) for L in selected_json["selected_layers"] if int(L) in directions_all] all_candidate_path = f"{base}_allcandidate{ext}" write_json( { "dimension": args.dimension, "selected_layers": all_layers, "n_selected": len(all_layers), "policy": "all_candidate_layers_no_monotonic_filter_for_gpqa_ablation", "note": "Auto-written by run_gpqa_d_layer_ablation_alpha07.py; uses every layer with a learned direction.", }, all_candidate_path, ) conditions = [ {"name": "selected_alpha07", "layers": selected_layers}, {"name": "allcandidate_alpha07", "layers": all_layers}, ] for cond in conditions: log.info(f"{cond['name']} layers({len(cond['layers'])})={cond['layers']}") out_path = os.path.join(p.RESULTS_DIR, f"run_gpqa_d_layer_ablation{args.out_suffix}.jsonl") sum_path = os.path.join(p.RESULTS_DIR, f"run_gpqa_d_layer_ablation{args.out_suffix}_summary.json") items = read_jsonl(data_path) gt = {i: it["answer"].strip().upper() for i, it in enumerate(items)} baseline, baseline_path = load_alpha1_baseline(p.RESULTS_DIR, args.seed) log.info(f"alpha=1 baseline records for token-rise comparison: {len(baseline)} from {baseline_path}") if args.force and os.path.exists(out_path): os.remove(out_path) seen = set() if os.path.exists(out_path): for line in open(out_path, encoding="utf-8"): line = line.strip() if line: try: seen.add(json.loads(line)["_key"]) except Exception: pass log.info(f"[resume] {len(seen)} records cached") todo = [] for pi, it in enumerate(items): for cond in conditions: key = f"P{pi}_{cond['name']}" if key not in seen: todo.append((pi, it["problem"], cond, key)) log.info(f"records to compute: {len(todo)} / {len(items) * len(conditions)}") detector = BehaviorDetector(cfg) device = get_device() model = tokenizer = None if todo: log.info("Loading model...") model, tokenizer = load_model_and_tokenizer(device=device) fh = open(out_path, "a", encoding="utf-8") if todo else None for pi, problem, cond, key in tqdm(todo, desc="gpqa_layer_ablation", dynamic_ncols=True, mininterval=10): prompt = build_chat_prompt(tokenizer, problem, enable_thinking=True, system="") cond_dirs = {L: directions_all[L] for L in cond["layers"]} eff = {L: float(args.alpha) for L in cond_dirs} gen_seed = args.seed * 1000 + pi t0 = time.time() cot = generate_with_alpha( model, tokenizer, prompt, cond_dirs, eff, device, max_new_tokens=gen_max, do_sample=True, temperature=temperature, top_p=top_p, seed=gen_seed, ) elapsed = time.time() - t0 pred = extract_boxed_letter(cot) det = detector.detect(cot) rep = repetition_score(cot) ttok = think_tokens(tokenizer, cot) b = baseline.get(pi) b_tok = b.get("think_tokens") if b else None delta = (ttok - b_tok) if b_tok is not None else None rec = { "_key": key, "problem_idx": pi, "seed": args.seed, "alpha": float(args.alpha), "condition": cond["name"], "n_layers": len(cond["layers"]), "layers": cond["layers"], "problem": problem, "cot": cot, "pred": pred, "gt": gt.get(pi), "correct": (pred == gt.get(pi)) if pred and gt.get(pi) else False, "has_boxed": pred is not None, "has_think_end": "" in cot, "think_tokens": ttok, "n_chars": len(cot), "mon_total": det["total"], "repetition_score": rep, "collapse": rep > 0.5, "baseline_think_tokens": b_tok, "delta_think_tokens_vs_alpha1": delta, "token_rise_vs_alpha1": (delta is not None and delta > 0), "elapsed_s": elapsed, } fh.write(json.dumps(rec, ensure_ascii=False) + "\n") fh.flush() log.info( f"{key}: pred={pred} gt={gt.get(pi)} {'OK' if rec['correct'] else 'x'} " f"tok={ttok} mon={det['total']} rep={rep:.2f} delta_vs_a1={delta} t={elapsed:.0f}s" ) if fh: fh.close() records = [] if os.path.exists(out_path): for line in open(out_path, encoding="utf-8"): line = line.strip() if line: try: records.append(json.loads(line)) except Exception: pass avg = lambda xs: sum(xs) / len(xs) if xs else 0.0 summary = {} log.info("\n=== SUMMARY: GPQA-D layer ablation alpha=0.7 ===") log.info(f"baseline_for_token_rise={baseline_path}") log.info( f"{'condition':>18} {'layers':>6} {'n':>3} {'acc':>8} {'correct':>8} {'noBox':>6} " f"{'think_tok':>10} {'mon':>7} {'collapse':>9} {'rise_vs_a1':>10} {'delta_tok':>10}" ) for cond in conditions: name = cond["name"] rs = [r for r in records if r.get("condition") == name] if not rs: continue n = len(rs) n_correct = sum(bool(r["correct"]) for r in rs) no_box = n - sum(bool(r["has_boxed"]) for r in rs) collapse_rate = sum(bool(r["collapse"]) for r in rs) / n comparable = [r for r in rs if r.get("baseline_think_tokens") is not None] rise_rate = ( sum(bool(r.get("token_rise_vs_alpha1")) for r in comparable) / len(comparable) if comparable else None ) mean_delta = ( avg([r["delta_think_tokens_vs_alpha1"] for r in comparable]) if comparable else None ) summary[name] = { "n": n, "accuracy": n_correct / n, "n_correct": n_correct, "n_no_boxed": no_box, "mean_think_tokens": avg([r["think_tokens"] for r in rs]), "mean_chars": avg([r["n_chars"] for r in rs]), "mean_mon": avg([r["mon_total"] for r in rs]), "mean_repetition_score": avg([r["repetition_score"] for r in rs]), "collapse_rate": collapse_rate, "token_rise_vs_alpha1_rate": rise_rate, "mean_delta_think_tokens_vs_alpha1": mean_delta, "n_layers": len(cond["layers"]), "layers": cond["layers"], } rise_s = "NA" if rise_rate is None else f"{rise_rate*100:.1f}%" delta_s = "NA" if mean_delta is None else f"{mean_delta:.0f}" log.info( f"{name:>18} {len(cond['layers']):>6} {n:>3} {n_correct/n:>7.1%} {n_correct:>8} {no_box:>6} " f"{avg([r['think_tokens'] for r in rs]):>10.0f} " f"{avg([r['mon_total'] for r in rs]):>7.1f} " f"{collapse_rate*100:>8.1f}% {rise_s:>10} {delta_s:>10}" ) write_json( { "dataset": "GPQA-Diamond", "seed": args.seed, "alpha": float(args.alpha), "temperature": temperature, "top_p": top_p, "gen_max": gen_max, "selected_layer_file": sel_path, "all_candidate_layer_file": all_candidate_path, "baseline_alpha1_file_for_token_rise": baseline_path, "conditions": conditions, "summary": summary, }, sum_path, ) log.info(f"Saved {out_path}\n {sum_path}\nDone.") if __name__ == "__main__": main()