The best-of-k frontier previously reported only the oracle ceiling (resolved-if-any), which isn't realizable: you'd need the hidden grader to know which rollout to ship. This adds the selection half — a no-leak BFT tournament vote that actually picks ONE candidate per instance — so the frontier reports the resolution a realizable selector achieves alongside the ceiling, at identical cost (all k rollouts run either way). - TaskRun carries a stable patch_digest (FNV-1a over the changed non-test files vs the pristine tree), computed before grading so it never sees the oracle verdict. - select_consensus: plurality vote over candidate digests (ties → earliest rollout); abstains when nothing patched. Maps onto agent-joule's lockstep-quic BFT fleet — N metered workers vote, consensus ships one. - TestTimePoint/test_time_frontier + render gain selected_resolved; the oracle−consensus gap is pure selection-quality signal. - Mirrored in the SWE-bench harness (consensus_pick over per-rollout patches) so real Verified runs report oracle vs consensus. Tests: plurality pick, the honest case where consensus MISSES an oracle win (lone correct patch outvoted), abstention, frontier monotonicity (selected ≤ oracle), digest stability. 5/5 pass; end-to-end smoke renders. Co-Authored-By: Claude Opus 4.8 <[email protected]>
150 lines
7.4 KiB
Python
Executable file
150 lines
7.4 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""Run a solver over a SWE-bench Verified subset with the real Docker eval harness.
|
||
|
||
Pipeline per instance: clone the repo at base_commit, run the solver on the
|
||
problem statement to produce a source patch (tests restored so it can't edit the
|
||
grader's tests), capture cost, then evaluate every patch through the official
|
||
`swebench` harness (build image, apply patch + hidden test patch, run
|
||
FAIL_TO_PASS + PASS_TO_PASS). Reports resolution and dollars per resolved task.
|
||
|
||
With --rollouts N, runs N independent rollouts per instance and reports the
|
||
**metered test-time-compute frontier**: best-of-k resolution (an instance counts
|
||
resolved if any of the first k rollouts resolves it) paired with the cumulative
|
||
dollars those k rollouts cost — the resolution × $ curve of test-time compute,
|
||
metered. (Literature: best-of-N + selection lifts frontier agents ~7pp on
|
||
SWE-bench Verified; this measures what that lift costs.)
|
||
|
||
Setup (macOS, Apple Silicon):
|
||
open -a OrbStack # Docker daemon (emulates linux/amd64)
|
||
python3.11 -m venv ~/.swebench-venv # swebench needs Python 3.10+
|
||
~/.swebench-venv/bin/pip install swebench datasets
|
||
~/.swebench-venv/bin/python swebench/run_swebench.py --solver claude \
|
||
--instances psf__requests-5414 pylint-dev__pylint-7080 --rollouts 3
|
||
"""
|
||
import argparse, json, subprocess, sys, shutil
|
||
from pathlib import Path
|
||
|
||
def sh(cmd, cwd=None, timeout=1800):
|
||
return subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
||
|
||
def claude_solve(repo_dir, problem):
|
||
prompt = (
|
||
f"You are fixing a real bug in this checkout. Edit ONLY the library source "
|
||
f"(NOT tests — the grader applies its own). Explore the code, make the fix, "
|
||
f"then run the repo's existing pytest for the affected module to confirm you "
|
||
f"did not break anything; iterate until those pass.\n\nISSUE:\n{problem}"
|
||
)
|
||
safe = prompt.replace("'", "'\\''")
|
||
out = sh(f"claude -p '{safe}' --dangerously-skip-permissions --output-format json", cwd=repo_dir)
|
||
try:
|
||
return json.loads(out.stdout).get("total_cost_usd", 0.0) or 0.0
|
||
except Exception:
|
||
return 0.0
|
||
|
||
def one_rollout(ds, instances, solver, work, dataset, run_id):
|
||
"""Generate a patch per instance and evaluate the batch.
|
||
|
||
Returns (resolved_set, total_cost, patches) where patches maps each
|
||
instance_id to the rollout's candidate patch text — needed so a no-leak
|
||
consensus selector can vote across rollouts (see consensus_pick)."""
|
||
preds, total_cost, patches = [], 0.0, {}
|
||
for iid in instances:
|
||
inst = ds[iid]
|
||
repo_dir = work / f"{iid}__{run_id}"
|
||
if repo_dir.exists():
|
||
shutil.rmtree(repo_dir)
|
||
sh(f"git clone -q https://github.com/{inst['repo']} {repo_dir}", timeout=600)
|
||
sh(f"git checkout -q {inst['base_commit']}", cwd=repo_dir)
|
||
cost = claude_solve(repo_dir, inst["problem_statement"]) if solver == "claude" else 0.0
|
||
total_cost += cost
|
||
sh("git checkout -- $(git diff --name-only | grep -E '(^|/)(test_|tests/)' || true)", cwd=repo_dir)
|
||
patch = sh("git diff", cwd=repo_dir).stdout
|
||
patches[iid] = patch
|
||
preds.append({"instance_id": iid, "model_name_or_path": solver, "model_patch": patch})
|
||
print(f" [{iid}] cost=${cost:.3f} patch_bytes={len(patch)}", flush=True)
|
||
preds_path = work / f"predictions_{run_id}.json"
|
||
json.dump(preds, open(preds_path, "w"))
|
||
subprocess.run(
|
||
[sys.executable, "-m", "swebench.harness.run_evaluation",
|
||
"--dataset_name", dataset, "--predictions_path", str(preds_path),
|
||
"--max_workers", "2", "--run_id", run_id,
|
||
"--instance_ids", *instances, "--cache_level", "instance"],
|
||
check=False, capture_output=True)
|
||
report = Path(f"{solver}.{run_id}.json")
|
||
resolved = set(json.load(open(report))["resolved_ids"]) if report.exists() else set()
|
||
return resolved, total_cost, patches
|
||
|
||
def _normalize_patch(patch):
|
||
"""Drop git blob-hash lines so identical *edits* compare equal regardless of
|
||
incidental index metadata. Empty/whitespace patch → '' (an abstention)."""
|
||
lines = [ln for ln in patch.splitlines() if not ln.startswith("index ")]
|
||
return "\n".join(ln.rstrip() for ln in lines).strip()
|
||
|
||
def consensus_pick(patch_list):
|
||
"""No-leak BFT tournament vote over an instance's per-rollout candidate
|
||
patches (ordered by rollout). Returns the 0-based rollout index of the
|
||
plurality patch (ties → earliest rollout), or None if every rollout
|
||
abstained (no patch). Never consults the hidden grader — pure agreement."""
|
||
counts = {}
|
||
for patch in patch_list:
|
||
key = _normalize_patch(patch)
|
||
if key:
|
||
counts[key] = counts.get(key, 0) + 1
|
||
if not counts:
|
||
return None
|
||
top = max(counts.values())
|
||
for index, patch in enumerate(patch_list):
|
||
key = _normalize_patch(patch)
|
||
if key and counts[key] == top:
|
||
return index
|
||
return None
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--solver", default="claude", choices=["claude"])
|
||
ap.add_argument("--instances", nargs="+", required=True)
|
||
ap.add_argument("--dataset", default="princeton-nlp/SWE-bench_Verified")
|
||
ap.add_argument("--run-id", default="joule-bench-swe")
|
||
ap.add_argument("--rollouts", type=int, default=1)
|
||
ap.add_argument("--work", default=str(Path.home() / "data-share" / "swebench-run"))
|
||
args = ap.parse_args()
|
||
|
||
from datasets import load_dataset
|
||
ds = {r["instance_id"]: r for r in load_dataset(args.dataset, split="test")}
|
||
work = Path(args.work); work.mkdir(parents=True, exist_ok=True)
|
||
n = len(args.instances)
|
||
|
||
per_rollout = [] # (resolved_set, cost, patches) per rollout
|
||
for k in range(1, args.rollouts + 1):
|
||
print(f"\n=== rollout {k}/{args.rollouts} ===", flush=True)
|
||
per_rollout.append(one_rollout(ds, args.instances, args.solver, work, args.dataset, f"{args.run_id}-r{k}"))
|
||
|
||
# Best-of-k metered test-time-compute frontier.
|
||
print("\n" + "=" * 60)
|
||
if args.rollouts == 1:
|
||
resolved, cost, _ = per_rollout[0]
|
||
res = len(resolved); per = f"${cost/res:.2f}" if res else "—"
|
||
print(f" RESOLVED {res}/{n} = {100*res/n:.0f}% total ${cost:.2f} $/resolved {per}")
|
||
else:
|
||
# Oracle (resolved-if-any) = the ceiling of any selection method.
|
||
# Consensus = the no-leak BFT vote actually ships ONE patch per instance
|
||
# (consensus_pick over the first k rollouts' candidates), so it reports
|
||
# the resolution a *realizable* selector achieves. Same cost either way.
|
||
print(" test-time-compute frontier (best-of-k):")
|
||
print(f" {'k':>2} {'oracle':>10} {'consensus':>11} {'cum $':>9}")
|
||
cum = 0.0
|
||
for k in range(1, args.rollouts + 1):
|
||
cum += per_rollout[k - 1][1]
|
||
union = set().union(*(per_rollout[i][0] for i in range(k)))
|
||
selected = set()
|
||
for iid in args.instances:
|
||
candidates = [per_rollout[i][2].get(iid, "") for i in range(k)]
|
||
pick = consensus_pick(candidates)
|
||
if pick is not None and iid in per_rollout[pick][0]:
|
||
selected.add(iid)
|
||
print(f" {k:>2} {len(union):>6}/{n:<3} {len(selected):>7}/{n:<3} {cum:>9.2f}")
|
||
print(" oracle = resolved-if-any (ceiling); consensus = the patch the BFT vote ships.")
|
||
print(" gap = what better selection could still capture; cost is identical.")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|