"""Exactly paired test of the forced code-check module on recorded runs. For every recorded answer whose thinking closed, the same thinking is replayed (prompt now declares the python tool), the model is made to write a check program right after , the program runs, and the model continues with the output in a new thinking turn and gives its final answer. Truncated records are copied unchanged, so the difference to the source run is due to the check alone. usage: posthoc_check.py --src results/m180_base/mini.jsonl --out results/m180_chk/mini.jsonl [--max-tokens 6144] """ import argparse import json import os import sys import threading import time import urllib.request import zlib from concurrent.futures import ThreadPoolExecutor from transformers import AutoTokenizer sys.argv, _argv = sys.argv[:1] + ["--inp", "x", "--out", "x"], sys.argv # reuse tir.py's helpers without running it _src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "tir.py"), encoding="utf-8").read() exec(_src[:_src.index("def solve(")]) # noqa: S102 (constants, TOOL, SYSTEMS, run_code, post, CALL_RE) sys.argv = _argv sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from grade import last_boxed # noqa: E402 ap2 = argparse.ArgumentParser() ap2.add_argument("--src", required=True) ap2.add_argument("--out", required=True) ap2.add_argument("--items", nargs="+", default=["data/coc/math180.jsonl"]) ap2.add_argument("--max-tokens", type=int, default=6144) ap2.add_argument("--system", default="verify") ap2.add_argument("--check-line", default="# Independent check of the answer: compute it directly / by brute force\n") ap2.add_argument("--workers", type=int, default=32) ap2.add_argument("--mode", default="check", choices=["check", "switch"], help="check: code check after the thinking closes; switch: if the thinking runs longer than " "--switch-at tokens, stop it there and make the model compute the answer with a program") ap2.add_argument("--switch-at", type=int, default=3072) ap2.add_argument("--more-calls", type=int, default=0, help="further tool calls the model may make after the forced one") ap2.add_argument("--switch-text", default="\n\nThis is taking long. Let me compute the answer directly with a program.\n") a2 = ap2.parse_args() ENDS = {i for i in range(len(tok)) if (t := tok.convert_ids_to_tokens(i)) is not None and (t.endswith(".") or t.endswith("Ċ"))} items = {} for p in a2.items: for l in open(p, encoding="utf-8"): r = json.loads(l) items[r["id"]] = r SAMP = {"temperature": 0.6, "top_p": 0.95, "top_k": 50, "min_p": 0.0, "samplers": ["top_k", "top_p", "temperature"]} def check(rec): try: return _check(rec) except Exception as e: return {**rec, "check_error": repr(e)[:200]} def _check(rec): c = rec["completion"] it = items[rec["id"]] think = c[len(""):].split("", 1)[0] if a2.mode == "check" and (rec["truncated"] or "" not in c): return rec th = tok(think, add_special_tokens=False)["input_ids"] if a2.mode == "switch": if len(th) <= a2.switch_at: return rec cut = next((j for j in range(a2.switch_at, a2.switch_at // 2, -1) if th[j - 1] in ENDS), a2.switch_at) think = tok.decode(th[:cut]) + a2.switch_text th = tok(think, add_special_tokens=False)["input_ids"] th = th + [THINK_END] msgs = ([{"role": "system", "content": SYSTEMS[a2.system]}] if SYSTEMS[a2.system] else []) + \ [{"role": "user", "content": it["prompt"]}] ids = tok.apply_chat_template(msgs, tools=[TOOL], tokenize=True, add_generation_prompt=True, return_dict=False) used = len(th) seed = zlib.crc32(rec["id"].encode()) % 1000003 * 7 + rec["sample"] + 7 pre = tok(FORCE_PREFIX + a2.check_line, add_special_tokens=False)["input_ids"] r2 = post({"prompt": ids + th + pre, "n_predict": max(64, min(1536, a2.max_tokens - used)), "seed": seed, "cache_prompt": False, "stop": [""], **SAMP}) used += r2["tokens_predicted"] code = a2.check_line + r2["content"] out = run_code(code) ids = ids + th + pre + tok(r2["content"].rstrip("\n") + "\n\n\n<|im_end|>\n" "<|im_start|>tool\n\n" + out + "\n<|im_end|>\n" "<|im_start|>assistant\n", add_special_tokens=False)["input_ids"] log = "" for extra in range(a2.more_calls + 1): # the model may run more code (e.g. after an error) before answering r3 = post({"prompt": ids, "n_predict": max(256, a2.max_tokens - used), "seed": seed + 1 + extra, "cache_prompt": False, "return_tokens": True, **SAMP}) used += r3["tokens_predicted"] after = r3["content"] m = CALL_RE.search(after.split("", 1)[1]) if "" in after else None if not m or extra == a2.more_calls or used >= a2.max_tokens: break out2 = run_code(m.group(1)) turn = after.split("", 1)[0] + "" log += turn.replace("", "\n", 1) + "\n\n" + out2 + "\n\n" ids = ids + [t for t in r3["tokens"] if t != EOS] + tok( "<|im_end|>\n<|im_start|>tool\n\n" + out2 + "\n<|im_end|>\n" "<|im_start|>assistant\n", add_special_tokens=False)["input_ids"] after = log + after if "" not in after: # the post-check turn ran out: keep the original answer (switch: no answer) if a2.mode == "switch": return {**rec, "completion": "" + think + after, "n_tokens": used, "truncated": True, "check": {"code": code, "out": out, "kept": "none"}} return {**rec, "check": {"code": code, "out": out, "kept": "original"}} t2, final = after.split("", 1) if "\\boxed" in it["prompt"] and last_boxed(final) is None: # after a tool turn the model often answers in prose: read the answer out in the requested form last = [t for t in r3["tokens"] if t != EOS] k = last.index(THINK_END) + 1 if THINK_END in last else len(last) pr = post({"prompt": ids + last[:k] + tok("The final answer is $\\boxed{", add_special_tokens=False)["input_ids"], "n_predict": 48, "temperature": 0.0, "cache_prompt": False, "stop": ["}$", "}\n"]}) final = final.rstrip() + "\n\nThe final answer is $\\boxed{" + pr["content"] + "}$" return {**rec, "completion": "" + think + "\n" + FORCE_PREFIX + code + "\n\n\n" "\n\n" + out + "\n\n" + t2 + "" + final, "n_tokens": used, "truncated": False, "check": {"code": code, "out": out, "kept": "new"}} recs = [json.loads(l) for l in open(a2.src, encoding="utf-8")] os.makedirs(os.path.dirname(a2.out), exist_ok=True) t0 = time.time() with ThreadPoolExecutor(a2.workers) as ex: res = list(ex.map(check, recs)) with open(a2.out, "w", encoding="utf-8") as f: for r in res: f.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(res)} records, {sum('check' in r for r in res)} checked, {(time.time() - t0) / 60:.1f} min")