Scoring preflight: auto batch-pull for multi-image benches

Single-image benches keep fail-fast; >8 distinct images (swe's 500
per-instance) now pull concurrently with resume state inside the run
itself -- exactly like the old auto-pull behavior, just resilient:
scoring proceeds with whatever images landed, missing ones score 0
and only a total wipeout fails the bench. Ctrl+C-safe (state file),
network recovery resumes automatically on the next run.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sora 2026-09-17 09:38:16 +00:00
parent 587274d8b6
commit c23665fed7
2 changed files with 56 additions and 3 deletions

View File

@ -65,6 +65,20 @@ def evaluate(
if getattr(s, 'sandbox', None) and s.sandbox.image: if getattr(s, 'sandbox', None) and s.sandbox.image:
_imgs.add(s.sandbox.image) _imgs.add(s.sandbox.image)
if _imgs: if _imgs:
# 多镜像 benchswe 500 个逐题镜像):并发批量拉取 + 断点 state
# 拉到多少算多少、判分不中断(缺镜像的样本计 0 并在报告中标 missing
# 少数镜像bigcodebench 单镜像)保持 fail-fast
if len(_imgs) > 8:
from ..data.dataset import get_cache_root
from ..sandbox.pull_swe import pull_many
fails = pull_many(sorted(_imgs), str(get_cache_root()),
max_workers=4)
if fails == len(_imgs):
raise RuntimeError(
f'全部 {len(_imgs)} 个沙箱镜像拉取失败(网络/镜像源问'
'题);已保留断点,网络恢复后重跑自动续拉')
else:
from ..sandbox.docker import ensure_image from ..sandbox.docker import ensure_image
for _img in sorted(_imgs): for _img in sorted(_imgs):

View File

@ -81,6 +81,45 @@ def _pull_one(image: str) -> tuple:
return proc.returncode, '\n'.join(lines) return proc.returncode, '\n'.join(lines)
def pull_many(names, cache_dir, max_workers: int = 4,
verbose: bool = True) -> int:
"""批量拉取一组镜像名(断点续传复用 <cache-dir>/swe_pull_state.json
返回失败数给判分预检用多镜像 benchswe 500 逐个 fail-fast
会把整个 bench 卡死在第一个失败上这里并发拉不中断返回失败数"""
cdir = Path(cache_dir)
state = _load_state(cdir)
local = set()
r = subprocess.run(['docker', 'images', '--format', '{{.Repository}}:{{.Tag}}'],
capture_output=True, text=True)
if r.returncode == 0:
local = {l.strip() for l in r.stdout.splitlines() if l.strip()}
todo = [nm for nm in names
if nm not in state['done'] and nm not in local]
if verbose:
_log(f'· {len(names)} 个沙箱镜像: 本地/已完成 {len(names) - len(todo)}, '
f'待拉 {len(todo)} (并发 {max_workers}, 进度存 {_state_path(cdir)})')
def work(nm):
rc, out = _pull_one(nm)
if rc == 0:
state['done'].append(nm)
else:
state['failed'].append(nm)
_save_state(cdir, state)
return nm, rc
fails = 0
with ThreadPoolExecutor(max_workers=max_workers) as pool:
for i, (nm, rc) in enumerate(
pool.map(work, todo), 1):
if rc:
fails += 1
if verbose and (i % 10 == 0 or i == len(todo)):
_log(f' 镜像进度 [{i}/{len(todo)}] 失败 {fails}')
return fails
def run_pull(dataset: str, cache_dir: str, max_workers: int = 4, def run_pull(dataset: str, cache_dir: str, max_workers: int = 4,
dry_run: int = 0, retry_failed: bool = False): dry_run: int = 0, retry_failed: bool = False):
cdir = Path(cache_dir) cdir = Path(cache_dir)