42e commited on
Commit
6f2d0b4
·
verified ·
1 Parent(s): 8296d78

Upload folder using huggingface_hub

Browse files
src/mathcompose/eval/prmbench.py CHANGED
@@ -83,3 +83,73 @@ def evaluate_prmbench(
83
  for c, d in sorted(per_cat.items())
84
  },
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  for c, d in sorted(per_cat.items())
84
  },
85
  }
86
+
87
+
88
+ def evaluate_batched(
89
+ runner,
90
+ dataset_id: str = "hitsmy/PRMBench_Preview",
91
+ split: str = "train",
92
+ limit: Optional[int] = None,
93
+ n: int = 1,
94
+ batch_size: int = 16,
95
+ temperature: float = 0.0,
96
+ max_new_tokens: int = 640,
97
+ error_steps_base: int = 1,
98
+ include_original: bool = True,
99
+ ) -> dict:
100
+ """Fast path: batched generation via an HFRunner."""
101
+ from collections import defaultdict as _dd
102
+
103
+ from datasets import load_dataset
104
+
105
+ from .runners import verify_batch_predictions
106
+
107
+ ds = load_dataset(dataset_id, split=split)
108
+ if limit is not None:
109
+ ds = ds.select(range(min(limit, len(ds))))
110
+
111
+ mod_items, mod_gold, mod_cat = [], [], []
112
+ orig_items = []
113
+ for row in ds:
114
+ steps_mod = list(row.get("modified_process") or [])
115
+ gold0 = {int(e) - error_steps_base for e in (row.get("error_steps") or [])}
116
+ if steps_mod and gold0:
117
+ mod_items.append((row.get("modified_question") or row.get("question") or "", steps_mod))
118
+ mod_gold.append(gold0)
119
+ mod_cat.append(row.get("classification") or "unknown")
120
+ if include_original:
121
+ steps_orig = list(row.get("original_process") or [])
122
+ if steps_orig:
123
+ orig_items.append((row.get("original_question") or row.get("question") or "", steps_orig))
124
+
125
+ mod_preds = verify_batch_predictions(runner, mod_items, n=n, batch_size=batch_size,
126
+ temperature=temperature, max_new_tokens=max_new_tokens,
127
+ desc="PRMBench/modified")
128
+ per_cat: dict[str, dict] = _dd(lambda: {"n": 0, "detected": 0})
129
+ detected = localized = 0
130
+ for pred, gold0, cat in zip(mod_preds, mod_gold, mod_cat):
131
+ is_det = pred != -1
132
+ detected += 1 if is_det else 0
133
+ localized += 1 if pred in gold0 else 0
134
+ per_cat[cat]["n"] += 1
135
+ per_cat[cat]["detected"] += 1 if is_det else 0
136
+
137
+ false_pos = None
138
+ if include_original and orig_items:
139
+ orig_preds = verify_batch_predictions(runner, orig_items, n=n, batch_size=batch_size,
140
+ temperature=temperature, max_new_tokens=max_new_tokens,
141
+ desc="PRMBench/original")
142
+ false_pos = sum(1 for p in orig_preds if p != -1) / len(orig_preds)
143
+
144
+ n_mod = len(mod_items)
145
+ return {
146
+ "dataset": dataset_id,
147
+ "n_modified": n_mod,
148
+ "detection": detected / n_mod if n_mod else 0.0,
149
+ "localization": localized / n_mod if n_mod else 0.0,
150
+ "false_positive_rate": false_pos,
151
+ "per_category_detection": {
152
+ c: {"n": d["n"], "detection": d["detected"] / d["n"] if d["n"] else 0.0}
153
+ for c, d in sorted(per_cat.items())
154
+ },
155
+ }
src/mathcompose/eval/processbench.py CHANGED
@@ -114,3 +114,39 @@ def evaluate(
114
  "average_f1": avg_f1,
115
  "gpt4o_reference_f1": 61.9,
116
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  "average_f1": avg_f1,
115
  "gpt4o_reference_f1": 61.9,
116
  }
117
+
118
+
119
+ def evaluate_batched(
120
+ runner,
121
+ splits: Sequence[str] = DEFAULT_SPLITS,
122
+ limit: Optional[int] = None,
123
+ n: int = 1,
124
+ batch_size: int = 16,
125
+ temperature: float = 0.0,
126
+ max_new_tokens: int = 640,
127
+ dataset_id: str = "Qwen/ProcessBench",
128
+ ) -> dict:
129
+ """Fast path: batched generation via an HFRunner (vs the per-example verify_fn)."""
130
+ from datasets import load_dataset
131
+
132
+ from .runners import verify_batch_predictions
133
+
134
+ per_subset: list[SubsetScore] = []
135
+ for split in splits:
136
+ ds = load_dataset(dataset_id, split=split)
137
+ if limit is not None:
138
+ ds = ds.select(range(min(limit, len(ds))))
139
+ items = [(row["problem"], list(row["steps"])) for row in ds]
140
+ golds = [int(row["label"]) for row in ds]
141
+ preds = verify_batch_predictions(
142
+ runner, items, n=n, batch_size=batch_size, temperature=temperature,
143
+ max_new_tokens=max_new_tokens, desc=f"ProcessBench/{split}",
144
+ )
145
+ per_subset.append(score_subset(split, golds, preds))
146
+
147
+ avg_f1 = sum(s.f1 for s in per_subset) / len(per_subset) if per_subset else 0.0
148
+ return {
149
+ "subsets": {s.subset: s.as_dict() for s in per_subset},
150
+ "average_f1": avg_f1,
151
+ "gpt4o_reference_f1": 61.9,
152
+ }
src/mathcompose/eval/run.py CHANGED
@@ -34,20 +34,22 @@ def _runner(base_id, adapter):
34
 
35
 
36
  def cmd_processbench(a):
37
- from .processbench import evaluate
38
- from .runners import make_verify_fn
39
  r = _runner(a.base_id, a.adapter)
40
- verify_fn = make_verify_fn(r, n=a.maj_k, temperature=(0.6 if a.maj_k > 1 else 0.0))
41
- res = evaluate(verify_fn, splits=tuple(a.splits), limit=a.limit)
 
 
42
  _save("processbench", a.tag, res)
43
 
44
 
45
  def cmd_prmbench(a):
46
- from .prmbench import evaluate_prmbench
47
- from .runners import make_verify_fn
48
  r = _runner(a.base_id, a.adapter)
49
- verify_fn = make_verify_fn(r, n=a.maj_k, temperature=(0.6 if a.maj_k > 1 else 0.0))
50
- res = evaluate_prmbench(verify_fn, limit=a.limit)
 
 
51
  _save("prmbench", a.tag, res)
52
 
53
 
@@ -105,6 +107,8 @@ def main() -> None:
105
  p.add_argument("--tag", default="tuned")
106
  p.add_argument("--maj-k", type=int, default=1)
107
  p.add_argument("--limit", type=int, default=None)
 
 
108
  p.add_argument("--splits", nargs="+", default=["gsm8k", "math", "olympiadbench", "omnimath"])
109
  p.set_defaults(func=cmd_processbench)
110
 
@@ -113,6 +117,8 @@ def main() -> None:
113
  p.add_argument("--tag", default="tuned")
114
  p.add_argument("--maj-k", type=int, default=1)
115
  p.add_argument("--limit", type=int, default=None)
 
 
116
  p.set_defaults(func=cmd_prmbench)
117
 
118
  p = sub.add_parser("math")
 
34
 
35
 
36
  def cmd_processbench(a):
37
+ from .processbench import evaluate_batched
 
38
  r = _runner(a.base_id, a.adapter)
39
+ bs = max(1, a.batch_size // max(1, a.maj_k)) # cap concurrent seqs = prompts * k
40
+ res = evaluate_batched(r, splits=tuple(a.splits), limit=a.limit, n=a.maj_k,
41
+ batch_size=bs, temperature=(0.6 if a.maj_k > 1 else 0.0),
42
+ max_new_tokens=a.max_new_tokens)
43
  _save("processbench", a.tag, res)
44
 
45
 
46
  def cmd_prmbench(a):
47
+ from .prmbench import evaluate_batched
 
48
  r = _runner(a.base_id, a.adapter)
49
+ bs = max(1, a.batch_size // max(1, a.maj_k))
50
+ res = evaluate_batched(r, limit=a.limit, n=a.maj_k, batch_size=bs,
51
+ temperature=(0.6 if a.maj_k > 1 else 0.0),
52
+ max_new_tokens=a.max_new_tokens)
53
  _save("prmbench", a.tag, res)
54
 
55
 
 
107
  p.add_argument("--tag", default="tuned")
108
  p.add_argument("--maj-k", type=int, default=1)
109
  p.add_argument("--limit", type=int, default=None)
110
+ p.add_argument("--batch-size", type=int, default=16, help="max concurrent sequences (÷ maj-k)")
111
+ p.add_argument("--max-new-tokens", type=int, default=640)
112
  p.add_argument("--splits", nargs="+", default=["gsm8k", "math", "olympiadbench", "omnimath"])
113
  p.set_defaults(func=cmd_processbench)
114
 
 
117
  p.add_argument("--tag", default="tuned")
118
  p.add_argument("--maj-k", type=int, default=1)
119
  p.add_argument("--limit", type=int, default=None)
120
+ p.add_argument("--batch-size", type=int, default=16, help="max concurrent sequences (÷ maj-k)")
121
+ p.add_argument("--max-new-tokens", type=int, default=640)
122
  p.set_defaults(func=cmd_prmbench)
123
 
124
  p = sub.add_parser("math")
src/mathcompose/eval/runners.py CHANGED
@@ -79,7 +79,7 @@ class HFRunner:
79
  input_ids = input_ids.to(self.device)
80
  attn = attn.to(self.device) if attn is not None else None
81
 
82
- do_sample = bool(temperature and temperature > 0)
83
  gen_kwargs = dict(
84
  max_new_tokens=max_new_tokens,
85
  do_sample=do_sample,
@@ -88,7 +88,8 @@ class HFRunner:
88
  repetition_penalty=repetition_penalty,
89
  )
90
  if do_sample:
91
- gen_kwargs.update(temperature=temperature, top_p=0.95)
 
92
 
93
  with torch.no_grad():
94
  out = self.model.generate(input_ids, attention_mask=attn, **gen_kwargs)
@@ -96,6 +97,52 @@ class HFRunner:
96
  return self.tokenizer.batch_decode(gen, skip_special_tokens=True)
97
 
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  def make_verify_fn(runner: HFRunner, n: int = 1, temperature: float = 0.0,
100
  max_new_tokens: int = 1024) -> Callable[[str, list[str]], list[str]]:
101
  def verify_fn(problem: str, steps: list[str]) -> list[str]:
@@ -110,3 +157,27 @@ def make_solve_fn(runner: HFRunner, n: int = 1, temperature: float = 0.0,
110
  return runner.chat(generator_messages(problem), n=n,
111
  temperature=temperature, max_new_tokens=max_new_tokens)
112
  return solve_fn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  input_ids = input_ids.to(self.device)
80
  attn = attn.to(self.device) if attn is not None else None
81
 
82
+ do_sample = bool(temperature and temperature > 0) or n > 1 # n>1 needs sampling
83
  gen_kwargs = dict(
84
  max_new_tokens=max_new_tokens,
85
  do_sample=do_sample,
 
88
  repetition_penalty=repetition_penalty,
89
  )
90
  if do_sample:
91
+ gen_kwargs.update(temperature=(temperature if temperature and temperature > 0 else 0.7),
92
+ top_p=0.95)
93
 
94
  with torch.no_grad():
95
  out = self.model.generate(input_ids, attention_mask=attn, **gen_kwargs)
 
97
  return self.tokenizer.batch_decode(gen, skip_special_tokens=True)
98
 
99
 
100
+ def chat_many(
101
+ self,
102
+ messages_list: list[list[dict]],
103
+ n: int = 1,
104
+ temperature: float = 0.0,
105
+ max_new_tokens: int = 640,
106
+ repetition_penalty: float = 1.1,
107
+ ) -> list[list[str]]:
108
+ """Batched generation across many prompts (left-padded). Returns, per input
109
+ prompt, a list of n decoded completions. Far faster than looping chat()."""
110
+ import torch
111
+
112
+ tok = self.tokenizer
113
+ old_side = tok.padding_side
114
+ tok.padding_side = "left" # required for correct batched generation
115
+ try:
116
+ rendered = [
117
+ tok.apply_chat_template(m, add_generation_prompt=True, tokenize=False)
118
+ for m in messages_list
119
+ ]
120
+ enc = tok(
121
+ rendered, return_tensors="pt", padding=True, truncation=True,
122
+ max_length=max(1, self.max_context - max_new_tokens),
123
+ add_special_tokens=False,
124
+ )
125
+ finally:
126
+ tok.padding_side = old_side
127
+
128
+ input_ids = enc["input_ids"].to(self.device)
129
+ attn = enc["attention_mask"].to(self.device)
130
+ do_sample = bool(temperature and temperature > 0) or n > 1 # n>1 needs sampling
131
+ gen_kwargs = dict(
132
+ max_new_tokens=max_new_tokens, do_sample=do_sample, num_return_sequences=n,
133
+ pad_token_id=tok.pad_token_id, repetition_penalty=repetition_penalty,
134
+ )
135
+ if do_sample:
136
+ gen_kwargs.update(temperature=(temperature if temperature and temperature > 0 else 0.7),
137
+ top_p=0.95)
138
+ with torch.no_grad():
139
+ out = self.model.generate(input_ids, attention_mask=attn, **gen_kwargs)
140
+ gen = out[:, input_ids.shape[1]:]
141
+ texts = tok.batch_decode(gen, skip_special_tokens=True)
142
+ # generate returns batch*n rows, grouped by input then sample
143
+ return [texts[i * n:(i + 1) * n] for i in range(len(messages_list))]
144
+
145
+
146
  def make_verify_fn(runner: HFRunner, n: int = 1, temperature: float = 0.0,
147
  max_new_tokens: int = 1024) -> Callable[[str, list[str]], list[str]]:
148
  def verify_fn(problem: str, steps: list[str]) -> list[str]:
 
157
  return runner.chat(generator_messages(problem), n=n,
158
  temperature=temperature, max_new_tokens=max_new_tokens)
159
  return solve_fn
160
+
161
+
162
+ def verify_batch_predictions(runner: HFRunner, items, n: int = 1, batch_size: int = 16,
163
+ temperature: float = 0.0, max_new_tokens: int = 640,
164
+ desc: str = "verify") -> list[int]:
165
+ """items: list of (problem, steps). Returns the Maj@n first-error index per item,
166
+ using batched generation. This is the fast path used by ProcessBench/PRMBench."""
167
+ from .parse import majority_index
168
+
169
+ try:
170
+ from tqdm.auto import tqdm
171
+ except Exception: # pragma: no cover
172
+ def tqdm(x, **k):
173
+ return x
174
+
175
+ preds: list[int] = []
176
+ chunks = list(range(0, len(items), batch_size))
177
+ for i in tqdm(chunks, desc=desc):
178
+ chunk = items[i:i + batch_size]
179
+ msgs = [verifier_messages(p, s) for p, s in chunk]
180
+ samples = runner.chat_many(msgs, n=n, temperature=temperature,
181
+ max_new_tokens=max_new_tokens)
182
+ preds.extend(majority_index(s) for s in samples)
183
+ return preds
src/mathcompose/train/colab_train.ipynb CHANGED
@@ -4,10 +4,10 @@
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
- "# mathcompose \u2014 QLoRA training on Colab (free T4)\n",
8
  "\n",
9
- "Pulls the code **and** the Model-V dataset from HF Hub with just your token \u2014 no\n",
10
- "GitHub needed. **Runtime \u2192 Change runtime type \u2192 T4 GPU** before running."
11
  ]
12
  },
13
  {
@@ -16,7 +16,7 @@
16
  "execution_count": null,
17
  "outputs": [],
18
  "source": [
19
- "# 1. GPU deps (versions this project was built against) + 4-bit kernels\n",
20
  "!pip -q install \"transformers>=5.13,<6\" \"trl>=1.7,<2\" \"peft>=0.19,<0.20\" \\\n",
21
  " \"accelerate>=1.14\" \"datasets>=5.0\" \"huggingface_hub>=1.22\" sympy bitsandbytes"
22
  ]
@@ -27,16 +27,16 @@
27
  "execution_count": null,
28
  "outputs": [],
29
  "source": [
30
- "# 2. Token -> pull the code+data snapshot -> install\n",
31
  "import os\n",
32
  "from huggingface_hub import HfApi, snapshot_download, login\n",
33
- "os.environ[\"HF_TOKEN\"] = \"hf_xxx\" # paste, or Colab Secrets: userdata.get('HF_TOKEN')\n",
34
  "login(os.environ[\"HF_TOKEN\"])\n",
35
  "USER = HfApi().whoami()[\"name\"]\n",
36
  "proj = snapshot_download(f\"{USER}/mathcompose\", repo_type=\"model\")\n",
37
  "%cd {proj}\n",
38
  "!pip -q install -e .\n",
39
- "print(\"project at\", proj, \"| data:\", os.listdir(\"data/verifier\"))"
40
  ]
41
  },
42
  {
@@ -53,7 +53,7 @@
53
  "cell_type": "markdown",
54
  "metadata": {},
55
  "source": [
56
- "## 4. Train Model V (verifier) \u2014 QLoRA on the balanced dataset"
57
  ]
58
  },
59
  {
@@ -69,8 +69,9 @@
69
  "cell_type": "markdown",
70
  "metadata": {},
71
  "source": [
72
- "## 5. Eval V on ProcessBench \u2014 base vs tuned (Maj@8) + PRMBench\n",
73
- "Use `--limit` for a fast read; drop it for the full 3,400-example score."
 
74
  ]
75
  },
76
  {
@@ -79,9 +80,13 @@
79
  "execution_count": null,
80
  "outputs": [],
81
  "source": [
82
- "!python -m mathcompose.eval.run processbench --tag base --maj-k 1 --limit 200\n",
83
- "!python -m mathcompose.eval.run processbench --adapter runs/verifier_v --tag tuned --maj-k 8 --limit 200\n",
84
- "!python -m mathcompose.eval.run prmbench --adapter runs/verifier_v --tag tuned --limit 300\n",
 
 
 
 
85
  "!python -m mathcompose.eval.run report"
86
  ]
87
  },
@@ -109,9 +114,8 @@
109
  "cell_type": "markdown",
110
  "metadata": {},
111
  "source": [
112
- "## 7. (Later) Model G + the composition\n",
113
- "Build G's dataset (teacher=claude-opus-4-8), train, then run V-reranks-G \u2014 the\n",
114
- "headline generator-verifier-gap number."
115
  ]
116
  },
117
  {
 
4
  "cell_type": "markdown",
5
  "metadata": {},
6
  "source": [
7
+ "# mathcompose \u2014 QLoRA training on Colab\n",
8
  "\n",
9
+ "Pulls code+data from HF with just your token. **Runtime \u2192 GPU** before running.\n",
10
+ "Training auto-tunes to the detected GPU (`--gpu-preset auto`)."
11
  ]
12
  },
13
  {
 
16
  "execution_count": null,
17
  "outputs": [],
18
  "source": [
19
+ "# 1. GPU deps (+ 4-bit kernels)\n",
20
  "!pip -q install \"transformers>=5.13,<6\" \"trl>=1.7,<2\" \"peft>=0.19,<0.20\" \\\n",
21
  " \"accelerate>=1.14\" \"datasets>=5.0\" \"huggingface_hub>=1.22\" sympy bitsandbytes"
22
  ]
 
27
  "execution_count": null,
28
  "outputs": [],
29
  "source": [
30
+ "# 2. Token -> pull code+data snapshot -> install\n",
31
  "import os\n",
32
  "from huggingface_hub import HfApi, snapshot_download, login\n",
33
+ "os.environ[\"HF_TOKEN\"] = \"hf_xxx\" # paste, or Colab Secrets\n",
34
  "login(os.environ[\"HF_TOKEN\"])\n",
35
  "USER = HfApi().whoami()[\"name\"]\n",
36
  "proj = snapshot_download(f\"{USER}/mathcompose\", repo_type=\"model\")\n",
37
  "%cd {proj}\n",
38
  "!pip -q install -e .\n",
39
+ "print(\"data:\", os.listdir(\"data/verifier\"))"
40
  ]
41
  },
42
  {
 
53
  "cell_type": "markdown",
54
  "metadata": {},
55
  "source": [
56
+ "## 4. Train Model V (auto-tunes batch/seq/precision to the GPU)"
57
  ]
58
  },
59
  {
 
69
  "cell_type": "markdown",
70
  "metadata": {},
71
  "source": [
72
+ "## 5. Eval V on ProcessBench \u2014 base vs tuned (now batched \u2192 fast)\n",
73
+ "Fast first read below (1 split, small limit, Maj@4). For the headline number,\n",
74
+ "drop `--splits`/raise `--limit` and use `--maj-k 8`."
75
  ]
76
  },
77
  {
 
80
  "execution_count": null,
81
  "outputs": [],
82
  "source": [
83
+ "# fast sanity read (~a few min on A100):\n",
84
+ "!python -m mathcompose.eval.run processbench --tag base --maj-k 1 --limit 100 --splits gsm8k\n",
85
+ "!python -m mathcompose.eval.run processbench --adapter runs/verifier_v --tag tuned --maj-k 4 --limit 100 --splits gsm8k\n",
86
+ "# full run (all 4 splits, Maj@8) \u2014 uncomment when the fast read looks right:\n",
87
+ "# !python -m mathcompose.eval.run processbench --tag base --maj-k 1\n",
88
+ "# !python -m mathcompose.eval.run processbench --adapter runs/verifier_v --tag tuned --maj-k 8\n",
89
+ "!python -m mathcompose.eval.run prmbench --adapter runs/verifier_v --tag tuned --maj-k 1 --limit 200\n",
90
  "!python -m mathcompose.eval.run report"
91
  ]
92
  },
 
114
  "cell_type": "markdown",
115
  "metadata": {},
116
  "source": [
117
+ "## 7. (Later) Model G + composition\n",
118
+ "Build G's dataset (teacher=claude-opus-4-8), train, then V-reranks-G."
 
119
  ]
120
  },
121
  {
tests/test_eval_plumbing.py CHANGED
@@ -31,3 +31,17 @@ def test_eval_pipeline_runs():
31
  s = score_subset("smoke", golds, preds) # tiny model -> garbage preds, but scoring runs
32
  assert 0.0 <= s.f1 <= 1.0
33
  assert s.n_error + s.n_correct == 3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  s = score_subset("smoke", golds, preds) # tiny model -> garbage preds, but scoring runs
32
  assert 0.0 <= s.f1 <= 1.0
33
  assert s.n_error + s.n_correct == 3
34
+
35
+
36
+ @pytest.mark.slow
37
+ @pytest.mark.network
38
+ def test_batched_processbench_eval_runs():
39
+ """The fast batched path: load -> chat_many -> parse -> score over a tiny slice."""
40
+ pytest.importorskip("torch")
41
+ from mathcompose.eval.runners import HFRunner
42
+ from mathcompose.eval.processbench import evaluate_batched
43
+
44
+ runner = HFRunner(TINY, max_context=512)
45
+ res = evaluate_batched(runner, splits=["gsm8k"], limit=4, n=1, batch_size=2, max_new_tokens=6)
46
+ assert 0.0 <= res["average_f1"] <= 1.0
47
+ assert "gsm8k" in res["subsets"]