"""AIME25 avg@k 评测。 针对 non-thinking 模型: prompt 必须用 enable_thinking=False 渲染 (尾部会带 \n\n\n\n)。见 MEMORY: nothink-flag-for-1p7b-sft。 答案抽取按优先级: \boxed{} -> "Answer: X" -> 文末最后一个整数。 AIME 答案恒为 0-999 的整数, 比较时按 int 比。 """ import argparse import json import os import re import sys import time from collections import Counter p = argparse.ArgumentParser() p.add_argument("--model", required=True) p.add_argument("--data", default="/home/haojinw2/datasets/aime25/test.jsonl") p.add_argument("--out", required=True) p.add_argument("--n", type=int, default=16) p.add_argument("--max-new", type=int, default=8192) p.add_argument("--ctx", type=int, default=10240) p.add_argument("--tp", type=int, default=1) p.add_argument("--temp", type=float, default=0.7) p.add_argument("--top-p", type=float, default=0.8) p.add_argument("--top-k", type=int, default=20) p.add_argument("--seed", type=int, default=0) p.add_argument("--enable-thinking", type=lambda s: s.lower() == "true", default=False, help="False = 关闭 think(non-thinking 模型必须用 False)") p.add_argument("--gpu-util", type=float, default=0.85) a = p.parse_args() def log(*x): print(f"[{time.strftime('%H:%M:%S')}]", *x, flush=True) INSTR = "Please reason step by step, and put your final answer within \\boxed{}." BOXED = re.compile(r"\\boxed\s*\{([^{}]*)\}") ANSW = re.compile(r"[Aa]nswer\s*[::]\s*\$?(-?\d+)") INTS = re.compile(r"-?\d+") def _safe_int(s): """AIME 答案恒为 0-999。超长数字是模型退化的产物(见过 28000 位), 直接 int() 会撞 Python 3.11 的 int_max_str_digits=4300 抛 ValueError。""" if s is None: return None t = s.lstrip("-") if len(t) > 10 or not t: return None try: return int(s) except ValueError: return None def extract(text): m = BOXED.findall(text) if m: digits = INTS.findall(m[-1]) if digits: v = _safe_int(digits[-1]) if v is not None: return v m = ANSW.findall(text) if m: v = _safe_int(m[-1]) if v is not None: return v m = INTS.findall(text) if m: return _safe_int(m[-1]) return None def _kill_descendants(): """递归杀掉本进程的所有子孙(vLLM worker 是独立进程, 不杀会变孤儿占满卡)。""" import signal import subprocess me = os.getpid() try: out = subprocess.run(["ps", "-eo", "pid,ppid", "--no-headers"], capture_output=True, text=True, timeout=20).stdout except Exception: return children = {} for ln in out.splitlines(): parts = ln.split() if len(parts) != 2: continue try: pid, ppid = int(parts[0]), int(parts[1]) except ValueError: continue children.setdefault(ppid, []).append(pid) doomed, stack = [], [me] while stack: for c in children.get(stack.pop(), []): doomed.append(c) stack.append(c) for pid in reversed(doomed): # 先叶子后根 try: os.kill(pid, signal.SIGKILL) except OSError: pass def _hard_exit(code=0): sys.stdout.flush() sys.stderr.flush() _kill_descendants() os._exit(code) def _die(exc_type, exc, tb): import traceback as _tb _tb.print_exception(exc_type, exc, tb) _hard_exit(1) # 异常路径同样要把 worker 一起带走 sys.excepthook = _die rows = [json.loads(l) for l in open(a.data) if l.strip()] log(f"{len(rows)} 题 x n={a.n}, enable_thinking={a.enable_thinking}") from transformers import AutoTokenizer from vllm import LLM, SamplingParams tok = AutoTokenizer.from_pretrained(a.model, trust_remote_code=True) prompts = [] for r in rows: msgs = [{"role": "user", "content": r["problem"].strip() + "\n\n" + INSTR}] prompts.append(tok.apply_chat_template( msgs, add_generation_prompt=True, tokenize=False, enable_thinking=a.enable_thinking)) log("prompt 尾部抽检: " + repr(prompts[0][-60:])) def _fit_tp(model_path, tp): """attn heads 必须能被 TP 整除, 否则 vLLM 起不来(R1-Distill-1.5B 是 12 heads)。""" try: cfg = json.load(open(os.path.join(model_path, "config.json"))) h = cfg.get("num_attention_heads") except Exception: return tp if not h or h % tp == 0: return tp for cand in range(tp, 0, -1): if h % cand == 0: log(f"注意: {h} 个 attention head 不能被 TP={tp} 整除, 自动降为 TP={cand}") return cand return 1 a.tp = _fit_tp(a.model, a.tp) llm = LLM(model=a.model, tensor_parallel_size=a.tp, gpu_memory_utilization=a.gpu_util, max_model_len=a.ctx, trust_remote_code=True, seed=a.seed) # 不给 SamplingParams 传 seed: per-request seed 会让 n>1 退出批量采样快路径, # 480 个 seeded 样本会逐个采样、慢到形同挂死。可复现性由 LLM(seed=) 保证。 sp = SamplingParams(n=a.n, temperature=a.temp, top_p=a.top_p, top_k=a.top_k, max_tokens=a.max_new) t0 = time.time() outs = llm.generate(prompts, sp) log(f"生成 {time.time()-t0:.0f}s") recs, n_correct, n_tot, n_trunc, lens = [], 0, 0, 0, [] per_problem = [] for r, out in zip(rows, outs): gt = int(str(r["answer"]).strip()) hits, samples = 0, [] for o in out.outputs: pred = extract(o.text) ok = (pred is not None and pred == gt) hits += ok n_correct += ok n_tot += 1 n_trunc += (o.finish_reason == "length") lens.append(len(o.token_ids)) samples.append({"pred": pred, "ok": bool(ok), "finish": o.finish_reason, "len": len(o.token_ids), "text": o.text}) per_problem.append({"id": r.get("id"), "gt": gt, "hits": hits, "n": len(out.outputs)}) recs.append({"id": r.get("id"), "problem": r["problem"], "gt": gt, "hits": hits, "samples": samples}) os.makedirs(os.path.dirname(os.path.abspath(a.out)) or ".", exist_ok=True) with open(a.out, "w") as f: for rec in recs: f.write(json.dumps(rec, ensure_ascii=False) + "\n") avg = n_correct / n_tot solved_any = sum(1 for x in per_problem if x["hits"] > 0) solved_all = sum(1 for x in per_problem if x["hits"] == x["n"]) summary = { "model": a.model, "data": a.data, "n": a.n, "enable_thinking": a.enable_thinking, "temperature": a.temp, "top_p": a.top_p, "top_k": a.top_k, "max_new": a.max_new, "seed": a.seed, "problems": len(rows), "samples": n_tot, f"avg@{a.n}": avg, "solved_any": solved_any, "solved_all": solved_all, "truncated_rate": n_trunc / n_tot, "resp_len_mean": sum(lens) / len(lens), "resp_len_max": max(lens), "hits_hist": dict(Counter(x["hits"] for x in per_problem)), } with open(a.out.replace(".jsonl", "_summary.json"), "w") as f: json.dump(summary, f, indent=2, ensure_ascii=False) print() print(json.dumps(summary, indent=2, ensure_ascii=False), flush=True) print() print(f"AIME25 avg@{a.n} = {avg:.4f} ({n_correct}/{n_tot})", flush=True) print(f" 解出>=1次 {solved_any}/{len(rows)}, 全对 {solved_all}/{len(rows)}", flush=True) print(f" 截断率 {n_trunc/n_tot:.3f}, 平均长度 {sum(lens)/len(lens):.0f}", flush=True) # vLLM TP>1 的 worker 常在析构时挂尸、占着整卡不放, 后续任务就起不来。 # 结果已全部写盘, 直接跳过析构强制退出。 _hard_exit(0)