joule-bench/swebench/run_swebench.py
David Charlot 119b3afa70 Add metered consensus selection to the test-time-compute frontier
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]>
2026-06-25 10:55:28 -04:00

150 lines
7.4 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()