mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-26 16:35:05 +00:00
254 lines
11 KiB
Python
254 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
EVAL_DIR = Path(__file__).resolve().parent
|
|
if str(EVAL_DIR) not in sys.path:
|
|
sys.path.insert(0, str(EVAL_DIR))
|
|
|
|
from common.io import read_jsonl, write_jsonl # noqa: E402
|
|
from common.loader import load_bench # noqa: E402
|
|
from common.passk import default_k_list, print_report, summarize # noqa: E402
|
|
from common.paths import ensure_sys_path # noqa: E402
|
|
|
|
ensure_sys_path()
|
|
|
|
|
|
# Default judge per benchmark: all three use the LLM judge, matching the reference protocol.
|
|
BENCH_DEFAULT_JUDGE = {"aime": "llm", "hle": "llm", "gpqa": "llm"}
|
|
|
|
|
|
def load_raw(out_dir: str) -> list[dict]:
|
|
"""Merge every rank's raw output; for a given (task_id, sample_idx) the last write wins."""
|
|
merged: dict[tuple, dict] = {}
|
|
order: list[tuple] = []
|
|
paths = sorted(glob.glob(os.path.join(out_dir, "raw_rank*.jsonl")))
|
|
if not paths:
|
|
raise FileNotFoundError(f"no raw_rank*.jsonl under {out_dir} (did generation finish?)")
|
|
for path in paths:
|
|
for row in read_jsonl(path):
|
|
key = (row["task_id"], row["sample_idx"])
|
|
if key not in merged:
|
|
order.append(key)
|
|
merged[key] = row
|
|
return [merged[key] for key in order]
|
|
|
|
|
|
def _judge(results: list[dict], by_id: dict, bench_name: str, judge: str, concurrency: int) -> str | None:
|
|
"""Score `results` in place. Returns the judge model name for the LLM judge, else None."""
|
|
if judge == "rule" and bench_name == "hle":
|
|
raise SystemExit(
|
|
"HLE does not support rule judging: the official protocol requires an LLM judge.\n"
|
|
"Most HLE answers are free text, so normalized exact match marks equivalent-but-"
|
|
"differently-written answers wrong and systematically understates the score.\n"
|
|
"Set EVAL_JUDGE_BASE_URL / EVAL_JUDGE_API_KEY / EVAL_JUDGE_MODEL and use --judge llm."
|
|
)
|
|
if judge != "llm":
|
|
for row in results:
|
|
row["rule_correct"] = row["correct"]
|
|
return None
|
|
|
|
from common.judge import judge_answers, resolve_model, truncate_output
|
|
|
|
judge_kind = "hle" if bench_name == "hle" else "equivalence"
|
|
|
|
def judge_input(text: str) -> str:
|
|
# HLE official protocol: drop the thinking section, send the rest in full, no truncation.
|
|
if judge_kind == "hle":
|
|
return text.split("</think>")[-1].strip()
|
|
return truncate_output(text)
|
|
|
|
items = [
|
|
(
|
|
by_id[row["task_id"]].get("judge_question") or by_id[row["task_id"]]["question"],
|
|
by_id[row["task_id"]]["answer"],
|
|
judge_input(row["raw_text"]),
|
|
)
|
|
for row in results
|
|
]
|
|
for row, verdict in zip(results, judge_answers(items, kind=judge_kind, concurrency=concurrency), strict=True):
|
|
row["rule_correct"] = row["correct"]
|
|
row["correct"] = bool(verdict["judge_correct"])
|
|
row["judge_raw"] = verdict.get("judge_raw", "")
|
|
row["judge_parsed"] = bool(verdict.get("judge_parsed", True))
|
|
if "judge_verdict_unparsed" in verdict:
|
|
row["judge_verdict_unparsed"] = bool(verdict["judge_verdict_unparsed"])
|
|
if verdict.get("judge_error"):
|
|
row["judge_error"] = verdict["judge_error"]
|
|
return resolve_model()
|
|
|
|
|
|
def score_run(
|
|
out_dir: str,
|
|
*,
|
|
judge: str = "auto",
|
|
judge_concurrency: int = 8,
|
|
data_file: str | None = None,
|
|
verbose: bool = True,
|
|
) -> int:
|
|
meta_path = os.path.join(out_dir, "meta.json")
|
|
meta: dict = {}
|
|
if os.path.isfile(meta_path):
|
|
with open(meta_path) as f:
|
|
meta = json.load(f)
|
|
|
|
bench_name = meta.get("bench", "aime")
|
|
bench = load_bench(bench_name)
|
|
if judge == "auto":
|
|
judge = BENCH_DEFAULT_JUDGE.get(bench_name, "rule")
|
|
data_path = data_file or meta.get("data_file")
|
|
if not data_path:
|
|
raise SystemExit(f"{meta_path} has no data_file; pass --data-file explicitly")
|
|
problems = bench.load(data_path)
|
|
records = bench.build_records(problems, limit=meta.get("limit"))
|
|
by_id = {r["task_id"]: r for r in records}
|
|
|
|
rollouts = load_raw(out_dir)
|
|
unknown = [r["task_id"] for r in rollouts if r["task_id"] not in by_id]
|
|
assert not unknown, f"raw output has task_ids that are not in the dataset: {sorted(set(unknown))[:5]}"
|
|
|
|
results: list[dict] = []
|
|
for rollout in rollouts:
|
|
record = by_id[rollout["task_id"]]
|
|
row = bench.score_rollout(rollout, record)
|
|
results.append(row)
|
|
|
|
judge_model = _judge(results, by_id, bench_name, judge, judge_concurrency)
|
|
|
|
if not results:
|
|
raise SystemExit(f"no rollout at all in {out_dir}/raw_rank*.jsonl; refusing to emit an empty score")
|
|
|
|
# Coverage: missing rank files, problems or samples must be surfaced, never silently ignored.
|
|
seen = {row["task_id"] for row in results}
|
|
n_sample = int(meta.get("n_sample", max((row["sample_idx"] for row in results), default=0) + 1))
|
|
missing = sorted(set(by_id) - seen)
|
|
expected_rollouts = len(records) * n_sample
|
|
coverage_complete = not missing and len(results) == expected_rollouts
|
|
if not coverage_complete:
|
|
print(
|
|
f"[warn] incomplete coverage: {len(seen)}/{len(records)} tasks, {len(results)}/{expected_rollouts} "
|
|
"rollouts; this is not a complete result",
|
|
flush=True,
|
|
)
|
|
|
|
# Truncated rollouts and fallback extraction paths both depress the score, so surface them.
|
|
finish_reasons = dict(Counter(str(row["finish_reason"]) for row in results if row.get("finish_reason")))
|
|
extract_sources = dict(Counter(str(row["extract_source"]) for row in results if row.get("extract_source")))
|
|
truncated = sum(finish_reasons.get(reason, 0) for reason in ("length", "prompt_length_exceeded"))
|
|
invalid = sum(finish_reasons.get(reason, 0) for reason in ("ignored", "aborted", "error", "preempted"))
|
|
|
|
summary = summarize(
|
|
results,
|
|
n_sample=n_sample,
|
|
k_list=meta.get("k_list") or default_k_list(n_sample),
|
|
extra={
|
|
"bench": meta.get("bench", "aime"),
|
|
"model_name": meta.get("model_name"),
|
|
"judge": judge,
|
|
"judge_model": judge_model,
|
|
"temperature": meta.get("temperature"),
|
|
"top_p": meta.get("top_p"),
|
|
"reasoning_effort": meta.get("reasoning_effort"),
|
|
"max_len": meta.get("max_len"),
|
|
"extract_failed": sum(1 for row in results if row.get("extract_failed", row.get("pred") is None)) / len(results),
|
|
"expected_tasks": len(records),
|
|
"expected_rollouts": expected_rollouts,
|
|
"coverage_complete": coverage_complete,
|
|
"missing_tasks": len(missing),
|
|
"ragged_samples": len({sum(1 for row in results if row["task_id"] == task_id) for task_id in seen}) > 1,
|
|
"finish_reasons": finish_reasons,
|
|
"extract_sources": extract_sources,
|
|
"truncated_rollouts": truncated,
|
|
"invalid_rollouts": invalid,
|
|
},
|
|
)
|
|
groups: dict[str, list[dict]] = {}
|
|
for row in results:
|
|
if key := str(by_id[row["task_id"]].get("modality") or ""):
|
|
groups.setdefault(key, []).append(row)
|
|
if len(groups) > 1:
|
|
summary["by_modality"] = {
|
|
modality: {
|
|
"num_tasks": len({row["task_id"] for row in rows}),
|
|
"num_rollouts": len(rows),
|
|
"Pass@1": sum(1 for row in rows if row["correct"]) / len(rows),
|
|
}
|
|
for modality, rows in sorted(groups.items())
|
|
}
|
|
|
|
if judge == "llm":
|
|
agree = sum(1 for row in results if bool(row["rule_correct"]) == bool(row["correct"]))
|
|
summary["rule_vs_llm_agreement"] = agree / len(results)
|
|
unparsed = sum(1 for row in results if not row.get("judge_parsed", True))
|
|
summary["judge_unparsed"] = unparsed / len(results)
|
|
verdict_unparsed = sum(1 for row in results if row.get("judge_verdict_unparsed"))
|
|
summary["judge_verdict_unparsed"] = verdict_unparsed / len(results)
|
|
if verdict_unparsed:
|
|
print(
|
|
f"[warn] {verdict_unparsed}/{len(results)} judge replies had no extractable verdict; "
|
|
"all three fallbacks missed, they count as wrong",
|
|
flush=True,
|
|
)
|
|
if unparsed:
|
|
print(
|
|
f"[warn] {unparsed}/{len(results)} judge calls returned no verdict and count as wrong; "
|
|
"this depresses the score -- raise the judge max_tokens and re-run",
|
|
flush=True,
|
|
)
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
write_jsonl(os.path.join(out_dir, "scored.jsonl"), results)
|
|
with open(os.path.join(out_dir, "summary.json"), "w") as f:
|
|
json.dump(summary, f, indent=2, ensure_ascii=False)
|
|
|
|
if verbose:
|
|
print_report(summary, title=f"{summary['bench']} {summary.get('model_name')} judge={judge}")
|
|
print(
|
|
f"coverage: expected {summary['expected_rollouts']} rollouts / "
|
|
f"{summary['expected_tasks']} tasks, got {summary['num_rollouts']} / {summary['num_tasks']}, "
|
|
f"complete={summary['coverage_complete']}"
|
|
)
|
|
print(f"finish_reason={summary['finish_reasons']}")
|
|
for key, stat in (summary.get("by_modality") or {}).items():
|
|
print(f" Pass@1[{key}] = {stat['Pass@1'] * 100:6.2f} (tasks={stat['num_tasks']})")
|
|
if summary["extract_sources"]:
|
|
print(f"extract_source={summary['extract_sources']}")
|
|
if summary["truncated_rollouts"] or summary["invalid_rollouts"]:
|
|
print(
|
|
f"[warn] {summary['truncated_rollouts']} truncated / "
|
|
f"{summary['invalid_rollouts']} invalid rollouts, all counted as wrong; "
|
|
"this depresses the score"
|
|
)
|
|
print(f"extract_failed={summary['extract_failed'] * 100:.2f}%")
|
|
if "rule_vs_llm_agreement" in summary:
|
|
print(f"rule vs llm agreement = {summary['rule_vs_llm_agreement'] * 100:.2f}%")
|
|
print(f"wrote {os.path.join(out_dir, 'scored.jsonl')} / summary.json")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="score a benchmark run and summarize Pass@k")
|
|
parser.add_argument("--out-dir", required=True)
|
|
parser.add_argument(
|
|
"--judge", choices=["auto", "rule", "llm"], default="auto", help="auto: per-benchmark default (all three use the LLM judge)"
|
|
)
|
|
parser.add_argument("--judge-concurrency", type=int, default=8)
|
|
parser.add_argument("--data-file", default=None, help="override meta.json's data_file")
|
|
args = parser.parse_args()
|
|
return score_run(
|
|
args.out_dir,
|
|
judge=args.judge,
|
|
judge_concurrency=args.judge_concurrency,
|
|
data_file=args.data_file,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|