42e commited on
Commit
047277d
·
verified ·
1 Parent(s): 3161c99

Upload folder using huggingface_hub

Browse files
src/mathcompose/common/chat.py CHANGED
@@ -58,6 +58,31 @@ def verifier_messages(problem: str, steps: list[str]) -> list[dict]:
58
  ]
59
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  def build_generator_prompt(problem: str) -> str:
62
  return problem.strip()
63
 
 
58
  ]
59
 
60
 
61
+ CODE_VERIFIER_SYSTEM = (
62
+ "You are a meticulous math grader with a Python interpreter. You are given a "
63
+ "problem and a candidate solution split into indexed paragraphs. Check each "
64
+ "paragraph; whenever a step involves arithmetic or algebra, VERIFY it by writing "
65
+ "a Python snippet in a ```python ... ``` block and reading the printed result "
66
+ "before judging. Find the FIRST paragraph containing an error."
67
+ )
68
+
69
+ _CODE_VERIFIER_INSTRUCTION = _VERIFIER_INSTRUCTION + (
70
+ "\n\nYou may write ```python``` blocks to check any computation; I will run them "
71
+ "and return the output. When you are certain, give the final index in \\boxed{{}}."
72
+ )
73
+
74
+
75
+ def build_code_verifier_prompt(problem: str, steps: list[str]) -> str:
76
+ return _CODE_VERIFIER_INSTRUCTION.format(problem=problem.strip(), tagged_steps=tag_steps(steps))
77
+
78
+
79
+ def code_verifier_messages(problem: str, steps: list[str]) -> list[dict]:
80
+ return [
81
+ {"role": "system", "content": CODE_VERIFIER_SYSTEM},
82
+ {"role": "user", "content": build_code_verifier_prompt(problem, steps)},
83
+ ]
84
+
85
+
86
  def build_generator_prompt(problem: str) -> str:
87
  return problem.strip()
88
 
src/mathcompose/eval/code_verify.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ r"""Inference-time code-execution verify loop (GenPRM-style, no retrain required).
2
+
3
+ The verifier is told it may write ```python``` blocks to check arithmetic. We run
4
+ this loop per example:
5
+ 1. generate a critique,
6
+ 2. if it emitted code AND hasn't boxed a final index yet, execute the code and
7
+ feed the outputs back as a tool turn,
8
+ 3. repeat up to ``max_rounds``, then read the boxed first-error index.
9
+
10
+ Because Qwen2.5-Math already supports tool-use, this can be run on the *existing*
11
+ base/tuned models to test whether a calculator closes the arithmetic gap — before
12
+ committing to a code-verified retrain.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from ..common.chat import code_verifier_messages
17
+ from ..common.math_grade import extract_boxed_int
18
+ from ..common.pyexec import extract_code_blocks, run_python
19
+ from .parse import PARSE_FAIL
20
+
21
+
22
+ def verify_with_code(
23
+ runner,
24
+ problem: str,
25
+ steps: list[str],
26
+ max_rounds: int = 3,
27
+ max_new_tokens: int = 512,
28
+ temperature: float = 0.0,
29
+ exec_timeout: float = 5.0,
30
+ ) -> int:
31
+ """Return the first-error index (or -1 all-correct, PARSE_FAIL if unparseable)."""
32
+ messages = code_verifier_messages(problem, steps)
33
+ last = ""
34
+ for round_i in range(max_rounds):
35
+ out = runner.chat(messages, n=1, temperature=temperature, max_new_tokens=max_new_tokens)[0]
36
+ last = out
37
+ idx = extract_boxed_int(out)
38
+ blocks = extract_code_blocks(out)
39
+
40
+ # Boxed answer with no pending code, or out of rounds -> done.
41
+ if idx is not None and (not blocks or round_i == max_rounds - 1):
42
+ return idx
43
+ if not blocks:
44
+ if idx is not None:
45
+ return idx
46
+ # nudge once for the boxed index
47
+ messages.append({"role": "assistant", "content": out})
48
+ messages.append({"role": "user", "content": "State the first-error index as \\boxed{index}."})
49
+ continue
50
+
51
+ # run the code, feed results back
52
+ results = [run_python(b, timeout=exec_timeout).as_feedback() for b in blocks]
53
+ feedback = "\n".join(f"[block {i} output]\n{r}" for i, r in enumerate(results))
54
+ messages.append({"role": "assistant", "content": out})
55
+ messages.append({"role": "user", "content":
56
+ f"Execution results:\n{feedback}\n\nContinue, then give \\boxed{{index}}."})
57
+
58
+ idx = extract_boxed_int(last)
59
+ return idx if idx is not None else PARSE_FAIL
src/mathcompose/eval/processbench.py CHANGED
@@ -116,6 +116,54 @@ def evaluate(
116
  }
117
 
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  def evaluate_batched(
120
  runner,
121
  splits: Sequence[str] = DEFAULT_SPLITS,
 
116
  }
117
 
118
 
119
+ def evaluate_codeexec(
120
+ runner,
121
+ splits: Sequence[str] = DEFAULT_SPLITS,
122
+ limit: Optional[int] = None,
123
+ max_rounds: int = 3,
124
+ max_new_tokens: int = 512,
125
+ temperature: float = 0.0,
126
+ dataset_id: str = "Qwen/ProcessBench",
127
+ ) -> dict:
128
+ """Per-example code-execution verify loop (GenPRM-style). Slower (sequential
129
+ tool-use per example) — use a small --limit. Tests whether a calculator closes
130
+ the arithmetic gap, on any existing model."""
131
+ from datasets import load_dataset
132
+
133
+ from .code_verify import verify_with_code
134
+
135
+ try:
136
+ from tqdm.auto import tqdm
137
+ except Exception: # pragma: no cover
138
+ def tqdm(x, **k):
139
+ return x
140
+
141
+ per_subset = []
142
+ for split in splits:
143
+ ds = load_dataset(dataset_id, split=split)
144
+ if limit is not None:
145
+ ds = ds.shuffle(seed=0).select(range(min(limit, len(ds))))
146
+ golds, preds = [], []
147
+ for row in tqdm(ds, desc=f"ProcessBench+code/{split}"):
148
+ golds.append(int(row["label"]))
149
+ preds.append(verify_with_code(
150
+ runner, row["problem"], list(row["steps"]),
151
+ max_rounds=max_rounds, max_new_tokens=max_new_tokens, temperature=temperature))
152
+ score = score_subset(split, golds, preds)
153
+ sd = score.as_dict()
154
+ tot = len(preds) or 1
155
+ sd["pred_dist"] = {
156
+ "all_correct(-1)": round(sum(1 for p in preds if p == -1) / tot, 3),
157
+ "error_idx(>=0)": round(sum(1 for p in preds if p >= 0) / tot, 3),
158
+ "parse_fail(-2)": round(sum(1 for p in preds if p == -2) / tot, 3),
159
+ }
160
+ per_subset.append((score, sd))
161
+
162
+ avg_f1 = sum(s.f1 for s, _ in per_subset) / len(per_subset) if per_subset else 0.0
163
+ return {"subsets": {sd["subset"]: sd for _, sd in per_subset},
164
+ "average_f1": avg_f1, "gpt4o_reference_f1": 61.9, "mode": "code-exec"}
165
+
166
+
167
  def evaluate_batched(
168
  runner,
169
  splits: Sequence[str] = DEFAULT_SPLITS,
src/mathcompose/eval/run.py CHANGED
@@ -78,12 +78,17 @@ def _prompts_per_batch(a):
78
 
79
 
80
  def cmd_processbench(a):
81
- from .processbench import evaluate_batched
82
  r = _runner(a.base_id, a.adapter)
83
- res = evaluate_batched(r, splits=tuple(a.splits), limit=a.limit, n=a.maj_k,
84
- batch_size=_prompts_per_batch(a),
85
- temperature=(0.6 if a.maj_k > 1 else 0.0),
86
- max_new_tokens=a.max_new_tokens)
 
 
 
 
 
 
87
  _save("processbench", a.tag, res)
88
 
89
 
@@ -154,6 +159,9 @@ def main() -> None:
154
  p.add_argument("--max-seqs", type=int, default=0, help="max concurrent sequences (0=auto by VRAM)")
155
  p.add_argument("--max-new-tokens", type=int, default=640)
156
  p.add_argument("--splits", nargs="+", default=["gsm8k", "math", "olympiadbench", "omnimath"])
 
 
 
157
  p.set_defaults(func=cmd_processbench)
158
 
159
  p = sub.add_parser("prmbench")
 
78
 
79
 
80
  def cmd_processbench(a):
 
81
  r = _runner(a.base_id, a.adapter)
82
+ if getattr(a, "code_exec", False):
83
+ from .processbench import evaluate_codeexec
84
+ res = evaluate_codeexec(r, splits=tuple(a.splits), limit=a.limit,
85
+ max_rounds=a.max_rounds, max_new_tokens=a.max_new_tokens)
86
+ else:
87
+ from .processbench import evaluate_batched
88
+ res = evaluate_batched(r, splits=tuple(a.splits), limit=a.limit, n=a.maj_k,
89
+ batch_size=_prompts_per_batch(a),
90
+ temperature=(0.6 if a.maj_k > 1 else 0.0),
91
+ max_new_tokens=a.max_new_tokens)
92
  _save("processbench", a.tag, res)
93
 
94
 
 
159
  p.add_argument("--max-seqs", type=int, default=0, help="max concurrent sequences (0=auto by VRAM)")
160
  p.add_argument("--max-new-tokens", type=int, default=640)
161
  p.add_argument("--splits", nargs="+", default=["gsm8k", "math", "olympiadbench", "omnimath"])
162
+ p.add_argument("--code-exec", action="store_true",
163
+ help="run the code-execution verify loop (GenPRM-style; per-example, slower)")
164
+ p.add_argument("--max-rounds", type=int, default=3, help="code-exec: max generate/execute rounds")
165
  p.set_defaults(func=cmd_processbench)
166
 
167
  p = sub.add_parser("prmbench")
tests/test_code_verify.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Code-execution verify loop (M1c) — with a scripted runner (no model/network)."""
2
+ from mathcompose.eval.code_verify import verify_with_code
3
+ from mathcompose.eval.parse import PARSE_FAIL
4
+
5
+
6
+ class _ScriptedRunner:
7
+ """Emits a code block first, then boxes an index after seeing execution output."""
8
+ def chat(self, messages, n=1, temperature=0.0, max_new_tokens=512):
9
+ saw_exec = any("Execution results" in m["content"] for m in messages if m["role"] == "user")
10
+ if saw_exec:
11
+ return ["The output shows 5, contradicting the step. \\boxed{2}"]
12
+ return ["Check paragraph 2.\n```python\nprint(2+3)\n```"]
13
+
14
+
15
+ class _ImmediateRunner:
16
+ def chat(self, messages, n=1, temperature=0.0, max_new_tokens=512):
17
+ return ["All steps verify. \\boxed{-1}"]
18
+
19
+
20
+ class _GarbageRunner:
21
+ def chat(self, messages, n=1, temperature=0.0, max_new_tokens=512):
22
+ return ["hmm no answer here"]
23
+
24
+
25
+ def test_executes_then_boxes():
26
+ assert verify_with_code(_ScriptedRunner(), "2+3", ["a", "2+3=4", "c"], max_rounds=3) == 2
27
+
28
+
29
+ def test_immediate_box_no_code():
30
+ assert verify_with_code(_ImmediateRunner(), "p", ["a", "b"]) == -1
31
+
32
+
33
+ def test_unparseable_returns_sentinel():
34
+ assert verify_with_code(_GarbageRunner(), "p", ["a", "b"], max_rounds=2) == PARSE_FAIL