- AdaptiveGate rewritten (Netflix Gradient2): window-vs-window per-stream speed gradient, count-driven windows with admission stamps, no thresholds or mode state machine; failures x0.7 + 30s drain pause - session-level admission for multi-turn agents (_SessionGate): in-progress sessions hold slots until done, newcomers queue at the door; capacity follows the model gate's discovered limit (CONCUR-style continuity) - image service: memory-first register (zero docker calls for known images), TTL-cached docker images listing, optimistic ready when the daemon is unreachable (docker save contention no longer kills runs); es tar loading removed in favor of ModelScope shipping (ms_images.py per-image tar upload/pull with round-trip verification) - runner: circuit breaker (12 consecutive failures abort the bench), first-failure error printed immediately - swe_agentic: image wait / docker run / rm off the event loop; exec timeout becomes an observation the agent can react to; container gets curlrc + git low-speed aborts (stalled github downloads fail fast) - eval run excludes its own endpoints from http_proxy (a sick personal proxy read as 'endpoint dead' and killed whole runs) - progress bar shows failed count; swe agentic exec_workers 2 -> 4 Co-Authored-By: Claude <noreply@anthropic.com>
237 lines
8.6 KiB
Python
237 lines
8.6 KiB
Python
"""Background sandbox-image service: pull-ahead + wait-if-missing.
|
|
|
|
One service per process. Benches declare the images they will need (the
|
|
runner registers them when a task arrives); a small worker pool pulls
|
|
them in the background from the CN mirror chain. Sample execution calls
|
|
``ensure``/``wait_ready`` BEFORE ``docker run``: an image still in flight
|
|
BLOCKS that sample (no failure, no timeout at 120s) until the pull
|
|
service delivers it or exhausts all sources.
|
|
|
|
This replaces the two broken timings:
|
|
- gen-phase `docker run` implicit pull (zero output, 120s timeout kill)
|
|
- score-phase batch pull that ran AFTER generation already failed.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import queue
|
|
import subprocess
|
|
import threading
|
|
from typing import Dict, Iterable, List, Optional
|
|
|
|
|
|
def _docker(args, timeout=300):
|
|
return subprocess.run(['docker'] + args, capture_output=True, text=True,
|
|
timeout=timeout)
|
|
|
|
|
|
_LOCAL_TTL_S = 60.0 # `docker images` on a daemon busy with saves
|
|
_LOCAL_CACHE = [0.0, set()] # [expires_at, names] -- one query per TTL
|
|
|
|
|
|
def _local_images() -> set:
|
|
import time
|
|
|
|
now = time.monotonic()
|
|
if now < _LOCAL_CACHE[0]:
|
|
return _LOCAL_CACHE[1]
|
|
r = _docker(['images', '--format', '{{.Repository}}:{{.Tag}}'], timeout=60)
|
|
names = set(r.stdout.split()) if r.returncode == 0 else set()
|
|
_LOCAL_CACHE[0] = now + _LOCAL_TTL_S
|
|
_LOCAL_CACHE[1] = names
|
|
return names
|
|
|
|
|
|
class ImageService:
|
|
"""Pull-ahead pool + readiness barrier, one per process."""
|
|
|
|
def __init__(self, workers: int = 2, verbose: bool = True):
|
|
self.workers = workers
|
|
self.verbose = verbose
|
|
self._wanted: Dict[str, str] = {} # image -> 'pulling'|'ready'|'failed'
|
|
self._lock = threading.Lock()
|
|
self._events: Dict[str, threading.Event] = {}
|
|
self._queue: 'queue.Queue[Optional[str]]' = queue.Queue()
|
|
self._threads: List[threading.Thread] = []
|
|
self._started = False
|
|
self._pulled: List[str] = [] # for atexit release
|
|
|
|
# ---- lifecycle ----
|
|
def start(self):
|
|
if self._started:
|
|
return
|
|
self._started = True
|
|
for i in range(self.workers):
|
|
t = threading.Thread(target=self._worker, daemon=True,
|
|
name=f'img-pull-{i}')
|
|
t.start()
|
|
self._threads.append(t)
|
|
|
|
def register(self, images: Iterable[str]):
|
|
"""Declare upcoming images (called when tasks arrive). Idempotent
|
|
and MEMORY-FIRST: the run bulk-registers everything once, so the
|
|
per-task register must be a dict hit with ZERO docker calls --
|
|
`docker images` per task (x100) against a daemon busy with
|
|
concurrent `docker save`s timed out at 60s and cascaded whole
|
|
runs into failure. Only genuinely UNKNOWN images trigger one
|
|
(TTL-cached) docker query for the batch; if even that fails under
|
|
daemon pressure, mark ready optimistically and let `docker run`
|
|
be the referee -- it has its own timeout and a clear error."""
|
|
self.start()
|
|
unknown = []
|
|
with self._lock:
|
|
for img in images:
|
|
if not img:
|
|
continue
|
|
if img in self._wanted:
|
|
continue
|
|
self._wanted[img] = 'pulling'
|
|
self._events[img] = threading.Event()
|
|
unknown.append(img)
|
|
if not unknown:
|
|
return
|
|
try:
|
|
local = _local_images()
|
|
except Exception:
|
|
local = None # daemon query failed: optimistic path below
|
|
for img in unknown:
|
|
if local is None or img in local:
|
|
if local is None:
|
|
self._log(f'· docker images unavailable -- assuming '
|
|
f'local: {img.split("/")[-1][:44]}')
|
|
with self._lock:
|
|
self._wanted[img] = 'ready'
|
|
self._events[img].set()
|
|
continue
|
|
self._queue.put(img)
|
|
self._log(f'· image queued: {img}')
|
|
|
|
def wait_ready(self, image: str, timeout_s: float = 3600) -> bool:
|
|
"""Block until the image is local (True) or all sources failed /
|
|
timeout (False). Register-on-demand so a wait without register
|
|
still works."""
|
|
self.register([image])
|
|
ev = self._events.get(image)
|
|
if ev is None:
|
|
return True # never registered -> assume caller handled it
|
|
ok = ev.wait(timeout_s)
|
|
if not ok:
|
|
self._log(f'· image wait TIMEOUT {image} ({timeout_s:.0f}s)')
|
|
return False
|
|
with self._lock:
|
|
return self._wanted.get(image) == 'ready'
|
|
|
|
# ---- worker ----
|
|
def _worker(self):
|
|
while True:
|
|
img = self._queue.get()
|
|
if img is None:
|
|
return
|
|
try:
|
|
ok = self._pull_one(img)
|
|
except Exception:
|
|
ok = False
|
|
with self._lock:
|
|
self._wanted[img] = 'ready' if ok else 'failed'
|
|
self._events[img].set()
|
|
self._queue.task_done()
|
|
|
|
def _pull_one(self, image: str) -> bool:
|
|
# fast path: a concurrent worker / previous run may have delivered
|
|
# it already (also flips any other queued-now-local images ready)
|
|
if image in _local_images():
|
|
self._sweep_local()
|
|
return True
|
|
# 1. OUR ModelScope dataset shipment (CN CDN, per-image tars). The
|
|
# swebench/* namespace is 403 on every public CN mirror, so this
|
|
# is the only network source for those images. Opt out with
|
|
# EVALHARNESS_MS_IMAGE_REPO='' .
|
|
repo = os.environ.get('EVALHARNESS_MS_IMAGE_REPO',
|
|
'SoraAmami/swebench-images')
|
|
if repo:
|
|
try:
|
|
from .ms_images import ms_pull
|
|
|
|
if ms_pull(image, repo):
|
|
self._pulled.append(image)
|
|
self._sweep_local()
|
|
return True
|
|
except Exception:
|
|
pass
|
|
# 2. CN mirror chain (progress + idle watchdog inside); on hit the
|
|
# puller retags to the canonical name so callers see it directly
|
|
try:
|
|
from .prefetch import _pull_one as net_pull
|
|
|
|
rc, _ = net_pull(image)
|
|
if rc == 0:
|
|
self._pulled.append(image)
|
|
return True
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
def _sweep_local(self):
|
|
"""A tar load makes ~10 images local at once: flip every queued
|
|
'pulling' image that is now local to 'ready' (and drop it from the
|
|
release list duty) -- otherwise each waits its own full queue turn
|
|
just to discover it already arrived."""
|
|
with self._lock:
|
|
waiting = [img for img, st in self._wanted.items()
|
|
if st == 'pulling']
|
|
if not waiting:
|
|
return
|
|
local = _local_images()
|
|
with self._lock:
|
|
for img in waiting:
|
|
if img in local:
|
|
self._wanted[img] = 'ready'
|
|
self._events[img].set()
|
|
self._pulled.append(img) # we caused it; release at exit
|
|
|
|
# ---- misc ----
|
|
def _log(self, msg):
|
|
if self.verbose:
|
|
print(msg, flush=True)
|
|
|
|
def stats(self) -> Dict[str, int]:
|
|
with self._lock:
|
|
vals = list(self._wanted.values())
|
|
return {'wanted': len(vals), 'ready': vals.count('ready'),
|
|
'pulling': vals.count('pulling'), 'failed': vals.count('failed')}
|
|
|
|
def release_all(self):
|
|
"""Remove images this service pulled (atexit / bench end)."""
|
|
import atexit
|
|
|
|
ids = _docker(['ps', '-aq', '--filter', 'name=eh-swe-'],
|
|
timeout=60).stdout.split()
|
|
if ids:
|
|
try:
|
|
_docker(['rm', '-f'] + ids, timeout=180)
|
|
except Exception:
|
|
pass
|
|
for img in list(self._pulled):
|
|
try:
|
|
_docker(['rmi', '-f', img], timeout=180)
|
|
self._log(f'· released {img}')
|
|
except Exception:
|
|
pass
|
|
self._pulled.clear()
|
|
|
|
|
|
_SERVICE: Optional[ImageService] = None
|
|
|
|
|
|
def get_image_service() -> ImageService:
|
|
"""Process-wide singleton; release hook auto-registered once."""
|
|
global _SERVICE
|
|
if _SERVICE is None:
|
|
_SERVICE = ImageService(workers=int(os.environ.get(
|
|
'EVALHARNESS_IMAGE_WORKERS', '2')))
|
|
if not os.environ.get('EVALHARNESS_KEEP_SWE_IMAGES'):
|
|
import atexit
|
|
|
|
atexit.register(_SERVICE.release_all)
|
|
return _SERVICE
|