Upload folder using huggingface_hub
Browse files- src/mathcompose/common/pyexec.py +67 -0
- src/mathcompose/eval/run.py +36 -4
- tests/test_pyexec.py +24 -0
src/mathcompose/common/pyexec.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sandboxed Python execution for the code-verifying verifier (GenPRM-style).
|
| 2 |
+
|
| 3 |
+
The verifier can emit ```python ... ``` blocks to *check* arithmetic (the thing a
|
| 4 |
+
1.5B model can't do by reading); we execute them and feed stdout back. Model-
|
| 5 |
+
generated code is untrusted, so we run it in a subprocess with:
|
| 6 |
+
* a wall-clock timeout,
|
| 7 |
+
* no inherited environment / no network creds,
|
| 8 |
+
* a scratch cwd,
|
| 9 |
+
* output truncation.
|
| 10 |
+
|
| 11 |
+
This is defense-in-depth, not a hard jail — run it only in a disposable
|
| 12 |
+
environment (Colab/Modal VM), never on a privileged host.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import subprocess
|
| 17 |
+
import sys
|
| 18 |
+
import tempfile
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class ExecResult:
|
| 24 |
+
ok: bool
|
| 25 |
+
stdout: str
|
| 26 |
+
stderr: str
|
| 27 |
+
|
| 28 |
+
def as_feedback(self, max_chars: int = 600) -> str:
|
| 29 |
+
if self.ok:
|
| 30 |
+
out = self.stdout.strip() or "(no output)"
|
| 31 |
+
return out[:max_chars]
|
| 32 |
+
err = (self.stderr.strip() or "error").splitlines()[-1]
|
| 33 |
+
return f"[error] {err[:max_chars]}"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
_PREAMBLE = "import math, fractions, itertools\nfrom fractions import Fraction\n"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def run_python(code: str, timeout: float = 5.0, max_output: int = 4000) -> ExecResult:
|
| 40 |
+
"""Execute a short snippet in a subprocess and capture stdout/stderr."""
|
| 41 |
+
src = _PREAMBLE + code
|
| 42 |
+
try:
|
| 43 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 44 |
+
proc = subprocess.run(
|
| 45 |
+
[sys.executable, "-I", "-c", src], # -I: isolated (ignore env/site)
|
| 46 |
+
capture_output=True, text=True, timeout=timeout, cwd=tmp,
|
| 47 |
+
env={"PYTHONHASHSEED": "0"},
|
| 48 |
+
)
|
| 49 |
+
return ExecResult(
|
| 50 |
+
ok=(proc.returncode == 0),
|
| 51 |
+
stdout=proc.stdout[:max_output],
|
| 52 |
+
stderr=proc.stderr[:max_output],
|
| 53 |
+
)
|
| 54 |
+
except subprocess.TimeoutExpired:
|
| 55 |
+
return ExecResult(ok=False, stdout="", stderr=f"timeout after {timeout}s")
|
| 56 |
+
except Exception as e: # noqa: BLE001
|
| 57 |
+
return ExecResult(ok=False, stdout="", stderr=f"{type(e).__name__}: {e}")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# Extract fenced python blocks from a model critique.
|
| 61 |
+
import re # noqa: E402
|
| 62 |
+
|
| 63 |
+
_CODE_BLOCK = re.compile(r"```(?:python)?\s*\n(.*?)```", re.DOTALL)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def extract_code_blocks(text: str) -> list[str]:
|
| 67 |
+
return [m.group(1).strip() for m in _CODE_BLOCK.finditer(text)]
|
src/mathcompose/eval/run.py
CHANGED
|
@@ -28,9 +28,40 @@ def _save(name: str, tag: str, payload: dict) -> None:
|
|
| 28 |
print(json.dumps(payload, indent=2))
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def _runner(base_id, adapter):
|
| 32 |
from .runners import HFRunner
|
| 33 |
-
return HFRunner(base_id, adapter_path=adapter)
|
| 34 |
|
| 35 |
|
| 36 |
def _prompts_per_batch(a):
|
|
@@ -77,8 +108,8 @@ def cmd_compose(a):
|
|
| 77 |
from .compose import evaluate_composition, make_v_scorer
|
| 78 |
from .runners import HFRunner, make_solve_fn
|
| 79 |
|
| 80 |
-
gen = HFRunner(a.base_id, adapter_path=a.gen_adapter)
|
| 81 |
-
ver = HFRunner(a.base_id, adapter_path=a.ver_adapter)
|
| 82 |
solve_fn = make_solve_fn(gen, n=a.n, temperature=a.temperature)
|
| 83 |
v_score_fn = make_v_scorer(ver, k=a.ver_k)
|
| 84 |
|
|
@@ -107,7 +138,8 @@ def cmd_report(a):
|
|
| 107 |
|
| 108 |
def main() -> None:
|
| 109 |
ap = argparse.ArgumentParser(description=__doc__)
|
| 110 |
-
ap.add_argument("--base-id", default=
|
|
|
|
| 111 |
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 112 |
|
| 113 |
p = sub.add_parser("processbench")
|
|
|
|
| 28 |
print(json.dumps(payload, indent=2))
|
| 29 |
|
| 30 |
|
| 31 |
+
DEFAULT_BASE = "Qwen/Qwen2.5-Math-1.5B-Instruct"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _base_from_adapter(adapter):
|
| 35 |
+
"""Read base_model_name_or_path from an adapter's config (local dir or hub id)."""
|
| 36 |
+
import json
|
| 37 |
+
import os
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
if os.path.isdir(adapter):
|
| 41 |
+
cfg = json.load(open(os.path.join(adapter, "adapter_config.json")))
|
| 42 |
+
else:
|
| 43 |
+
from huggingface_hub import hf_hub_download
|
| 44 |
+
cfg = json.load(open(hf_hub_download(adapter, "adapter_config.json")))
|
| 45 |
+
return cfg.get("base_model_name_or_path")
|
| 46 |
+
except Exception:
|
| 47 |
+
return None
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _resolve_base(base_id, adapter):
|
| 51 |
+
"""Explicit --base-id wins; else infer from the adapter; else the 1.5B default."""
|
| 52 |
+
if base_id:
|
| 53 |
+
return base_id
|
| 54 |
+
if adapter:
|
| 55 |
+
inferred = _base_from_adapter(adapter)
|
| 56 |
+
if inferred:
|
| 57 |
+
print(f"[eval] base inferred from adapter: {inferred}")
|
| 58 |
+
return inferred
|
| 59 |
+
return DEFAULT_BASE
|
| 60 |
+
|
| 61 |
+
|
| 62 |
def _runner(base_id, adapter):
|
| 63 |
from .runners import HFRunner
|
| 64 |
+
return HFRunner(_resolve_base(base_id, adapter), adapter_path=adapter)
|
| 65 |
|
| 66 |
|
| 67 |
def _prompts_per_batch(a):
|
|
|
|
| 108 |
from .compose import evaluate_composition, make_v_scorer
|
| 109 |
from .runners import HFRunner, make_solve_fn
|
| 110 |
|
| 111 |
+
gen = HFRunner(_resolve_base(a.base_id, a.gen_adapter), adapter_path=a.gen_adapter)
|
| 112 |
+
ver = HFRunner(_resolve_base(a.base_id, a.ver_adapter), adapter_path=a.ver_adapter)
|
| 113 |
solve_fn = make_solve_fn(gen, n=a.n, temperature=a.temperature)
|
| 114 |
v_score_fn = make_v_scorer(ver, k=a.ver_k)
|
| 115 |
|
|
|
|
| 138 |
|
| 139 |
def main() -> None:
|
| 140 |
ap = argparse.ArgumentParser(description=__doc__)
|
| 141 |
+
ap.add_argument("--base-id", default=None,
|
| 142 |
+
help="base model; if omitted, inferred from the adapter (else 1.5B default)")
|
| 143 |
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 144 |
|
| 145 |
p = sub.add_parser("processbench")
|
tests/test_pyexec.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sandboxed executor for the code-verifying verifier (M1c)."""
|
| 2 |
+
from mathcompose.common.pyexec import run_python, extract_code_blocks
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def test_arithmetic_and_output():
|
| 6 |
+
assert run_python("print(sum(range(101)))").as_feedback() == "5050"
|
| 7 |
+
assert run_python("print(6*7)").stdout.strip() == "42"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_timeout_is_caught():
|
| 11 |
+
r = run_python("while True: pass", timeout=1.0)
|
| 12 |
+
assert not r.ok and "timeout" in r.stderr
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_error_is_caught():
|
| 16 |
+
r = run_python("print(1/0)")
|
| 17 |
+
assert not r.ok and "ZeroDivisionError" in r.as_feedback()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_extract_code_blocks():
|
| 21 |
+
text = "a\n```python\nprint(1)\n```\nb\n```\nprint(2)\n```"
|
| 22 |
+
blocks = extract_code_blocks(text)
|
| 23 |
+
assert blocks == ["print(1)", "print(2)"]
|
| 24 |
+
assert run_python(blocks[0]).as_feedback() == "1"
|