File size: 7,640 Bytes
b28c738 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | """AIME25 avg@k 评测。
针对 non-thinking 模型: prompt 必须用 enable_thinking=False 渲染
(尾部会带 <think>\n\n</think>\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)
|