sora ae5a1a192e Optimize SWE-bench build_images: skip make_test_spec when images are pre-loaded
- make_test_spec() for all 500 samples was the real bottleneck that made
  swe_bench_verified appear stuck at 'Processing records: 0%' even when all
  instance images were already loaded locally.
- Now image names are computed directly from instance IDs, matching the
  swebench naming convention. Local images are listed once and missing ones
  are reported immediately. make_test_spec is only called for images that
  actually need to be built.
- With 500 pre-loaded images, build_images() now completes in ~45s instead of
  hanging indefinitely.

Also add bash/diagnose_swe_images.py for quickly checking which images are
missing locally.
2026-07-27 08:46:15 +00:00

211 lines
9.2 KiB
Python

import traceback
from typing import TYPE_CHECKING, Dict, List, Literal
from evalscope.api.dataset import Dataset
from evalscope.utils.function_utils import run_in_threads_with_progress
from evalscope.utils.logger import get_logger
if TYPE_CHECKING:
from swebench.harness.test_spec.test_spec import TestSpec
logger = get_logger()
def build_images(
samples: Dataset,
max_workers: int = 4,
force_rebuild: bool = False,
use_remote_images: bool = True,
force_arch: Literal['', 'arm64', 'x86_64'] = '',
dockerhub_username: str = 'swebench',
) -> Dict[str, str]:
"""This function uses the swe_bench library to build the docker images for the SWE-bench dataset.
It can also try to pull images from a registry before building them locally.
Args:
samples (Dataset): The dataset to build the images for
max_workers (int): The maximum number of workers to use for building images. Defaults to 4.
force_rebuild (bool, optional): Whether to force a rebuild of the images. Defaults to False.
use_remote_images (bool, optional): Whether to try pulling images from Docker Hub before building locally. Defaults to True. See https://hub.docker.com/u/swebench
force_arch (str, optional): Optionally force the docker images to be pulled/built for a specific architecture. Defaults to "".
dockerhub_username (str, optional): DockerHub user/org namespace for remote images. Defaults to "swebench".
""" # noqa: E501
from docker.client import DockerClient # type: ignore
from swebench.harness.constants import LATEST, SWEbenchInstance # type: ignore
from swebench.harness.docker_build import build_instance_images # type: ignore
from swebench.harness.test_spec.test_spec import make_test_spec # type: ignore
from .utils import resolve_swebench_arch
extra_build_instance_images_kwargs = {'tag': LATEST, 'env_image_tag': LATEST}
# Code copied from the swe_bench repository
docker_client = DockerClient.from_env()
def _get_available_docker_images() -> List[str]:
return [tag for image in docker_client.images.list() for tag in image.tags]
# The swebench library requires a huggingface version of the code to be loaded in order to build the images.
# We load the dataset and then use the library to build the images.
samples_hf: List[SWEbenchInstance] = [s.metadata for s in samples]
# We also keep a mapping from instance_ids to the name of the docker image
id_to_docker_image: Dict[str, str] = {}
id_to_test_spec: Dict[str, TestSpec] = {}
# Note that remote images are named eg "sphinx-doc_1776_sphinx-11502"
namespace = (dockerhub_username or 'swebench') if use_remote_images else None
def _get_instance_image_name(instance_id: str) -> str:
"""Fast image name computation that matches make_test_spec output."""
arch = resolve_swebench_arch(instance_id, force_arch)
if namespace:
# Remote / pre-built image format: swebench/sweb.eval.x86_64.repo_1776_name-123:latest
updated_id = instance_id.replace('__', '_1776_')
return f'{namespace}/sweb.eval.{arch}.{updated_id}:latest'
# Local build format: sweb.eval.x86_64.repo__name-123:latest
return f'sweb.eval.{arch}.{instance_id}:latest'
# Fast path: compute image names directly without calling make_test_spec.
# make_test_spec is very slow for 500 samples and is only needed when we
# actually have to build missing images locally.
logger.info('Computing SWE-Bench image names from instance IDs...')
for swebench_instance in samples_hf:
instance_id = swebench_instance['instance_id']
id_to_docker_image[instance_id] = _get_instance_image_name(instance_id)
# Get list of locally available Docker images
logger.info('Listing local Docker images...')
available_docker_images = _get_available_docker_images()
samples_to_build_images_for = [
s for s in samples_hf if id_to_docker_image[s['instance_id']] not in available_docker_images
]
logger.info(
f'SWE-Bench images: {len(samples_hf)} needed, '
f'{len(samples_hf) - len(samples_to_build_images_for)} already local, '
f'{len(samples_to_build_images_for)} missing'
)
# Only build expensive test_specs for images that are actually missing.
if samples_to_build_images_for:
logger.info('Building test specs for missing images (this may take a while)...')
for swebench_instance in samples_to_build_images_for:
arch = resolve_swebench_arch(swebench_instance['instance_id'], force_arch)
test_spec = make_test_spec(swebench_instance, namespace=namespace, arch=arch)
id_to_test_spec[swebench_instance['instance_id']] = test_spec
# Try to pull images from Docker Hub first if requested
if use_remote_images and len(samples_to_build_images_for) > 0:
logger.info(f'Attempting to pull {len(samples_to_build_images_for)} SWE-BENCH images from Docker Hub')
successfully_pulled: List[str] = []
def _pull_image(sample) -> str:
"""Pull a single SWE-Bench image; returns instance_id on success."""
instance_id: str = sample['instance_id']
image_name: str = id_to_docker_image[instance_id]
image_base_name: str = image_name.split(':')[0]
logger.info(f'Pulling {image_name}...')
for line in docker_client.api.pull(image_name, stream=True, decode=True):
status = line.get('status')
progress = line.get('progress')
if progress:
logger.info(f'{image_name} {status} {progress}')
elif status:
logger.info(f'{image_name} {status}')
docker_client.api.tag(image_name, image_base_name, 'latest')
logger.info(f'Successfully pulled {image_name}')
return instance_id
def _on_error(sample, exc: Exception) -> None:
image_name = id_to_docker_image[sample['instance_id']]
logger.warning(f'Failed to pull {image_name}: {exc}')
def _on_result(sample, instance_id: str) -> None:
successfully_pulled.append(instance_id)
run_in_threads_with_progress(
items=samples_to_build_images_for,
worker=_pull_image,
desc='Pulling SWE-Bench images',
max_workers=max_workers,
log_interval=30,
on_result=_on_result,
on_error=_on_error,
skip_failed=True,
)
logger.info(f'Pulled {len(successfully_pulled)} images from Docker Hub')
# Remove successfully pulled images from the build list
samples_to_build_images_for = [
s for s in samples_to_build_images_for if s['instance_id'] not in successfully_pulled
]
# Update available images list
available_docker_images = _get_available_docker_images()
# Build any remaining images locally
if len(samples_to_build_images_for) > 0:
logger.warning('BUILDING SWE-BENCH IMAGES. NOTE: This can take a long time.')
test_specs_to_build = [id_to_test_spec[s['instance_id']] for s in samples_to_build_images_for]
build_instance_images(
client=docker_client,
dataset=test_specs_to_build,
force_rebuild=force_rebuild,
max_workers=max_workers,
**extra_build_instance_images_kwargs,
)
# Check that all the images were built
available_docker_images = _get_available_docker_images()
missing_images = [
id_to_docker_image[s['instance_id']]
for s in samples_hf
if id_to_docker_image[s['instance_id']] not in available_docker_images
]
assert len(missing_images) == 0, (f'Not all images were built: {missing_images}, {id_to_docker_image}')
return id_to_docker_image
def build_container(
test_spec: 'TestSpec',
client,
):
"""
Builds the instance image for the given test spec and creates a container from the image.
Args:
test_spec (TestSpec): Test spec to build the instance image and container for
client (docker.DockerClient): Docker client for building image + creating the container
"""
from swebench.harness.constants import DOCKER_USER
from swebench.harness.docker_utils import cleanup_container
# Build corresponding instance image
container = None
try:
# Create the container
logger.info(f'Creating container for {test_spec.instance_id}...')
# Define arguments for running the container
run_args = test_spec.docker_specs.get('run_args', {})
cap_add = run_args.get('cap_add', [])
container = client.containers.create(
image=test_spec.instance_image_key,
user=DOCKER_USER,
detach=True,
command='tail -f /dev/null',
platform=test_spec.platform,
cap_add=cap_add,
)
logger.info(f'Container for {test_spec.instance_id} created: {container.id}')
return container
except Exception as e:
# If an error occurs, clean up the container and raise an exception
logger.error(f'Error creating container for {test_spec.instance_id}: {e}')
logger.info(traceback.format_exc())
cleanup_container(client, container, logger)
raise e