File size: 5,750 Bytes
f0975ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Assemble generated Beam traces into release/SFT files with summary stats.

Outputs (in --out-dir):
  traces_all.jsonl     every successful generation, correct or not, with metadata
                       (held-out eval prompts included, marked by eval_ids.txt)
  sft_verified.jsonl   verified-correct, untruncated, non-eval traces in "messages" form
                       (assistant turn carries reasoning_content, which Dory's chat
                       template renders as <think>...</think>)
  stats.json           counts, accuracy and token statistics per domain/band/effort
"""

import argparse
import glob
import json
import random
import statistics
import sys
from collections import defaultdict
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from verify import check  # noqa: E402


def band(r):
    if r["domain"] != "math":
        return "science"
    return f"math_d{min(int(r['difficulty'] or 0), 9)}"


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--inputs", nargs="+",
                    default=[str(ROOT / "data" / "gen" / "*.jsonl"), str(ROOT / "data" / "selfhost" / "*.jsonl")])
    ap.add_argument("--out-dir", default=str(ROOT / "data" / "final"))
    ap.add_argument("--max-per-prompt", type=int, default=1)
    # shortest: tersest style (run A). random: an unbiased draw from Beam's correct traces, which
    # keeps its self-checking on hard prompts (shortest-of-n favours lucky, unchecked solutions).
    ap.add_argument("--select", choices=["shortest", "random"], default="shortest")
    ap.add_argument("--seed", type=int, default=0)
    ap.add_argument("--eval-ids", default=str(ROOT / "data" / "eval_ids.txt"))
    args = ap.parse_args()
    eval_ids = {line.strip() for line in open(args.eval_ids) if line.strip()}

    recs = []
    for path in sorted(p for pattern in args.inputs for p in glob.glob(pattern)):
        for line in open(path):
            r = json.loads(line)
            if "error" not in r:
                # Re-check with the current verifier so fixes apply retroactively.
                ok, pred = check(r["content"], r["gold"], r["answer_type"])
                r["pred"], r["correct"] = pred, ok and r.get("finish_reason") == "stop"
                recs.append(r)

    out = Path(args.out_dir)
    out.mkdir(parents=True, exist_ok=True)

    by_id = defaultdict(list)
    for r in recs:
        by_id[r["id"]].append(r)

    # Per prompt, keep the shortest (or randomly drawn) verified-correct traces (length by
    # characters: SGLang n>1 requests only report total tokens, so per-sample counts are estimates).
    rng = random.Random(args.seed)
    n_sft = 0
    with open(out / "traces_all.jsonl", "w") as fa, open(out / "sft_verified.jsonl", "w") as fs:
        for r in recs:
            fa.write(json.dumps(r, ensure_ascii=False) + "\n")
        for pid, rs in by_id.items():
            if pid in eval_ids:
                continue
            good = [r for r in rs if r.get("correct") and r.get("reasoning", "").strip()]
            good.sort(key=lambda r: len(r["reasoning"]) + len(r["content"]))
            if args.select == "random":
                rng.shuffle(good)
            for r in good[: args.max_per_prompt]:
                n_sft += 1
                fs.write(
                    json.dumps(
                        {
                            "id": r["id"],
                            "domain": r["domain"],
                            "source": r["source"],
                            "effort": r["effort"],
                            "teacher": r.get("teacher", "beam-api"),
                            "difficulty": r.get("difficulty"),
                            "messages": [
                                {"role": "user", "content": r["prompt"]},
                                {
                                    "role": "assistant",
                                    "reasoning_content": r["reasoning"],
                                    "content": r["content"],
                                },
                            ],
                            "gold": r["gold"],
                            "n_samples": len(rs),
                            "n_correct": len(good),
                            "chars": len(r["reasoning"]) + len(r["content"]),
                            "completion_tokens": r["usage"].get("completion_tokens"),
                        },
                        ensure_ascii=False,
                    )
                    + "\n"
                )

    groups = defaultdict(list)
    for r in recs:
        split = "eval" if r["id"] in eval_ids else "train"
        groups[(r["effort"], split, band(r))].append(r)
        groups[(r["effort"], split, "ALL")].append(r)
    stats = {"generations": len(recs), "sft_examples": n_sft, "groups": {}}
    for (effort, split, b), rs in sorted(groups.items()):
        toks = [r["usage"].get("completion_tokens", 0) for r in rs]
        ok_toks = [r["usage"].get("completion_tokens", 0) for r in rs if r.get("correct")]
        stats["groups"][f"{effort}/{split}/{b}"] = {
            "n": len(rs),
            "accuracy": round(sum(r.get("correct", False) for r in rs) / len(rs), 3),
            "truncated": sum(r.get("finish_reason") == "length" for r in rs),
            "out_tokens_mean": round(statistics.mean(toks)),
            "out_tokens_median": round(statistics.median(toks)),
            "out_tokens_correct_mean": round(statistics.mean(ok_toks)) if ok_toks else None,
            "out_tokens_total": sum(toks),
        }
    (out / "stats.json").write_text(json.dumps(stats, indent=1))
    print(json.dumps(stats, indent=1))


if __name__ == "__main__":
    main()