SWE-bench

[TOC]

1. Code Analysis

1.1 Architecture

The architecture of this benchmark can be divided into 4 layer:

  1. Entry Layer
  2. Inference Layer & Harness Layer
  3. Core Engine Layer
  4. Data Collection Layer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌───────────────────────────────────────────────────────────────┐
│ Entry Layer: Entry Points │
│ Command Line / Python API / Modal Cloud │
│ Layer that interplays with User Directly │
├───────────────────────────────────────────────────────────────┤
│ Inference Layer │ Harness Layer │
│ Generate Patch │ Evaluate the patch │
│ │
│ - run_api.py (Call LLM) │ - run_evaluation.py │
│ - run_llama.py (Local Llama) │ - prepare_images.py │
│ - run_live.py (GitHub Issue) │ - reporting.py │
├───────────────────────────────────────────────────────────────┤
│ Docker Layer │
│ Docker Building / Test Execuation / Evaluation / Log Parsing│
│ │
│ - docker_build.py - grading.py │
│ - docker_utils.py - test_spec/ │
│ - log_parsers/ - constants/ │
├───────────────────────────────────────────────────────────────┤
│ Data Collection Layer │
│ │
│ Collect Issue from Github → Build Test Dataset │
│ - collect/ - versioning/ │
└───────────────────────────────────────────────────────────────┘

1.2 Core Data Structure

1.2.1 SWEbenchInstance (SWE-bench Instance)

1
2
3
4
5
6
7
8
9
10
11
12
class SWEbenchInstance(TypedDict):
repo: str # repo name, e.g. "sympy/sympy"
instance_id: str # Unique ID, e.g. "sympy__sympy-20590"
base_commit: str # git commit SHA where the issue exists
patch: str # patch from GitHub PR, standard answer
test_patch: str # patch that includes testcases
problem_statement: str # statement of the issue
hints_text: str # hint text
created_at: str # time when issue created
version: str # version
FAIL_TO_PASS: str # JSON: test list that should maintain a "fail→pass" status after repairing
PASS_TO_PASS: str # JSON: test list that should maintain a "pass→pass" status after repairing

The core judgement of the evaluation process are FAIL_TO_PASS and PASS_TO_PASS.

FAIL_TO_PASS: Tests that fail when the bug exists and pass when the bug is repaired.

PASS_TO_PASS: Tests that should pass when the bug both exists and is repaired.

1.2.2 TestSpec (Test Specification)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@dataclass
class TestSpec:
instance_id: str # Instance ID
repo: str # repo name
version: str # version
repo_script_list: list # Bash Commands List: to install repo code
eval_script_list: list # Bash Commands List: to evaluate result
env_script_list: list # Bash Commands List: to configurate env
arch: str # CPU arch: arm & x64
FAIL_TO_PASS: list # ditto
PASS_TO_PASS: list # ditto
language: str # language: py, java, js, go, rust...
docker_specs: dict # Docker specifications
namespace: str # DockerHub namespace

1.3.3 Three-Layer Structure of Docker Image

1
2
3
4
5
6
7
8
9
10
sweb.eval.x86_64.sympy__sympy-20590:latest    ← Instance Image
│ each task instance contains one, including specific repo code

└─ sweb.env.py.x86_64.a1b2c3...:latest ← Environment Image
│ each repo version has one, including pip/apt dependencies

└─ sweb.base.py.x86_64:latest ← Base Image
│ each language contains one, including Python/Java/...

└─ ubuntu:22.04 ← OS

Image Naming Rule :

sweb.{Layer}.{Language}.{Arch}.{Hash}:{Lable}

e.g. Test 3 different Django Issues

1
2
3
django__django-15022  (Django 4.1, Python 3.11)
django__django-15277 (Django 4.1, Python 3.11)
django__django-11333 (Django 3.2, Python 3.9)

If the docker is not layered, it is needed to create 3 different docker images:

1
2
3
django-15022 image ──── Ubuntu + Python + Django 4.1 + Code → 2GB
django-15277 image ──── Ubuntu + Python + Django 4.1 + Code → 2GB
django-11333 image ──── Ubuntu + Python + Django 3.2 + Code → 2GB

Totally 6 GB needed.

But if we layer the image:

  1. Base Layer: we need 2 images for Python 3.9 and Python 3.11, total 2 * 1 = 2GB.
  2. Env Layer: we need 2 images for Django 3.2 and Django 4.1, total 2 * 2 = 4GB
  3. Instance Layer: we need 3 images for each issue, total 50MB × 3 ≈ 0.15GB
  4. total 6.15 GB

It seems that the cost of 2 plans seem similar. But if we have 2294 instance, what will happen ?

  • No layer: 2294 * 2GB = 4588 GB
  • 8 images of Base Layer + 200 images of Env Layer + 2294 images of instance Layer = 8 * 1GB + 200 * 2GB + 0.15GB * 2294 ≈ 523 GB
  • 88.60% Memory are saved !

1.3 Inference Pipeline

1.3.1 Default Inference (Not Available Now)

In fact, the main function of SWE-bench is to evaluate the score of patch generated by LLM. It doesn’t care about how you generate the patch. There are three original scripts to call LLM to generate patch. However there are a lot of limits of these scripts because they were just created for the paper demonstration.

There are three ways of run interference command:

  1. Call cloud LLM API (OpenAI ChatGPT / Ahthropic Claude)

    1
    2
    3
    4
    python -m swebench.inference.run_api \
    --dataset_name_or_path princeton-nlp/SWE-bench_oracle \
    --model_name_or_path gpt-4 \
    --output_dir ./outputs
  2. Local LLM

    1
    2
    3
    4
    python -m swebench.inference.run_llama \
    --model_name_or_path princeton-nlp/SWE-Llama-13b \
    --dataset_path princeton-nlp/SWE-bench_oracle \
    --output_dir ./outputs
  3. Reason about issues that appear in real-time on GitHub

    1
    2
    3
    python -m swebench.inference.run_live \
    --model_name gpt-3.5-turbo-1106 \
    --issue_url https://github.com/some/repo/issues/123

1.3.1.1 How Did The Original Inference Scripts Work?

1.3.2 How to Inference Personally

1.3.2.2 Prepare for the Env

1
conda activate /

1.3.2.1 Start a local LLM by vLLM (This section should be moved to Part 2 )

A local LLM has been deployed and is driven by vLLM:

1
2
3
4
5
6
7
8
9
10
docker run -d --name vllm_server --gpus all \
-v /data/disk2/wye/models:/models \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model /models/Qwen2.5-Coder-7B-Instruct \
--dtype auto \
--max-model-len 16384 \
--gpu-memory-utilization 0.9 \
--port 8000

1.3.2.2 Full run_local_vllm.py file

A inference script has been added:

1
swebench/inference/run_local_vllm.py

Parameters:

Parameter Type Default Description
–dataset_name str princeton-nlp/SWE-bench_Lite Dataset:
princeton-nlp/SWE-bench_Lite (300)
princeton-nlp/SWE-bench_verified (500)
princeton-nlp/SWE-bench (2249)
–split str test 数据集分割:test
–output_dir str /data/disk2/wye/SWE-bench/patches_by_llm JSONL output directory, auto-create if not exist
–model_short_name str None (Auto-Detect) If not setted, auto-get from vLLM/v1/models
–max_instances int None(Run all instances) Limit to only run the first N instances; 3 = test
–vllm_url str http://localhost:8000/v1 vLLM server URL
–model_name str /models/Qwen2.5-Coder-7B-Instruct the model’s name for vLLM
–max_context_files int 5 context file nums for phase2

1.3.2.3 Inference Commnds

Start vLLM:

1
2
3
4
5
6
7
8
9
10
docker run -d --name vllm_wye_swebench --gpus all \
-v /data/disk2/wye/models:/models \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model /models/Qwen2.5-Coder-7B-Instruct \
--dtype auto \
--max-model-len 16384 \
--gpu-memory-utilization 0.9 \
--port 8000

Check if the vLLM is started:

1
docker logs -f vllm_wye_swebench
1
cd /data/disk2/wye/SWE-bench

Run test:

1
python -m swebench.inference.run_local_vllm --max_instances 3

Run SWE-bench_Lite(default):

1
python -m swebench.inference.run_local_vllm

Run SWE-bench_Verified:

1
2
python -m swebench.inference.run_local_vllm \
--dataset_name princeton-nlp/SWE-bench_Verified

Run Full:

1
2
python -m swebench.inference.run_local_vllm \
--dataset_name princeton-nlp/SWE-bench

[!NOTE]

The inference script will catch the dataset automatically.

1
2
3
4
5
(base) ubuntu@ubuntu:/data/disk2/wye/SWE-bench$ ll /home/ubuntu/.cache/huggingface/datasets/

...
drwxrwxr-x 3 ubuntu ubuntu 4096 Jul 1 12:06 princeton-nlp___swe-bench_lite/
...

1.4 Evaluation Pipeline

If you run the main test command:

1
2
3
4
5
python -m swebench.harness.run_evaluation \
--dataset_name princeton-nlp/SWE-bench_Lite \
--predictions_path predictions.jsonl \
--max_workers 4 \
--run_id my_run

The actual functions call chain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
main()                                    # run_evaluation.py:main()
├─ get_predictions_from_file() # 读取模型预测的 patch
├─ load_swebench_dataset() # 从 HuggingFace 下载/加载数据集
├─ get_dataset_from_preds() # 匹配合并: 哪些实例有预测结果

├─ build_env_images() # 【阶段1】构建 Docker 环境镜像
│ ├─ build_base_images() # 先构建基础镜像 (Python/Java运行时)
│ ├─ get_env_configs_to_build() # 找出需要构建的环境镜像
│ └─ run_threadpool(build_image) # 多线程并行构建

├─ run_instances() # 【阶段2】并行跑所有实例
│ └─ for each instance (多线程):
│ └─ run_instance() # 单个实例的评测
│ ├─ build_container() # 创建 Docker 容器
│ ├─ container.start() # 启动容器
│ ├─ copy_to_container() # 把 patch.diff 拷进容器
│ ├─ git apply patch.diff # 尝试打补丁
│ ├─ copy_to_container() # 把 eval.sh 拷进容器
│ ├─ exec_run_with_timeout() # 执行 /bin/bash /eval.sh
│ ├─ get_eval_report() # 解析测试输出, 评分
│ └─ cleanup_container() # 清理容器

├─ clean_images() # 【阶段3】清理 Docker 镜像
└─ make_run_report() # 【阶段4】生成最终评测报告

1.4.1 main

test split of SWE-bench Multimodal does not support the local evaluation because of the vast cost of memory.

1
2
3
4
5
6
7
8
if dataset_name == "SWE-bench/SWE-bench_Multimodal" and split == "test":
print(
"⚠️ Local evaluation for the test split of SWE-bench Multimodal "
"is not supported. Please check out sb-cli "
"(https://github.com/swe-bench/sb-cli/) for instructions on how "
"to submit predictions."
)
return

Check ID length:

1
assert len(run_id) > 0, "Run ID must be provided"

Check / Create report_path:

1
2
3
4
if report_dir is not None:
report_dir = Path(report_dir)
if not report_dir.exists():
report_dir.mkdir(parents=True)

SWE-bench’s docker image has 2 pulling ways (DockerHub or Local):

namespace Image Source Action
“swebench” (default) DockerHub docker pull swebench/sweb.eval.xxx
None (pass -n none) Local docker build -t sweb.eval.xxx .

force_rebuild: Only works when you use local build. It will delete the old image and re-run docker build command.

Load predictions (records including patches generated by LLM for solving the issues).

And trans the list into hash map:

1
2
predictions = get_predictions_from_file(predictions_path, dataset_name, split)
predictions = {pred[KEY_INSTANCE_ID]: pred for pred in predictions}

Import dataset tested:

1
2
3
dataset = get_dataset_from_preds(
dataset_name, split, instance_ids, predictions, run_id, rewrite_reports
)

Full dataset including skipped test instances:

1
full_dataset = load_swebench_dataset(dataset_name, split, instance_ids)

Evaluating in the cloud:

1
2
3
4
5
6
7
if modal:
if not dataset:
print("No instances to run.")
else:
validate_modal_credentials()
run_instances_modal(predictions, dataset, full_dataset, run_id, timeout)
return

Evaluating locally:

  1. Set the maximum number of file descriptors (fds) that can be opened by a process. Docker APi opens several sockets/fds for each container. The default fds num 1024 might be insufficient.

    1
    2
    3
    4
    if platform.system() == "Linux":
    # resource.setrlimit: equal to setrlimit(2) sys call
    resource.setrlimit(resource.RLIMIT_NOFILE, (open_file_limit, open_file_limit))
    client = docker.from_env()
  2. recording the existing images before evaluating:

    1
    existing_images = list_images(client)
  3. Dataset comes from get_dataset_from_preds, it might be empty. The evaluation will go on only when it’s not empty. If it’s local docker image and it’s not a re-grading process, create shared dockers(base + env) for all instances:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    build_env_images(
    client,
    dataset,
    force_rebuild,
    max_workers,
    namespace,
    instance_image_tag,
    env_image_tag,
    )
  4. Run all the instances(Here, each instance will generate an independent docker based on the env_images):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    run_instances(
    predictions,
    dataset,
    cache_level,
    clean,
    force_rebuild,
    max_workers,
    run_id,
    timeout,
    namespace=namespace,
    instance_image_tag=instance_image_tag,
    env_image_tag=env_image_tag,
    rewrite_reports=rewrite_reports,
    )
  5. Clean images and generate report:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    clean_images(client, existing_images, cache_level, clean)
    return make_run_report(
    predictions,
    full_dataset,
    run_id,
    client,
    namespace,
    instance_image_tag,
    env_image_tag,
    )

1.4.2 run_instance

1.4.2.1 whole picture

1.4.2.1.1 Preparation

入参:

1
2
3
4
5
6
7
8
9
10
def run_instance(
test_spec: TestSpec, # test specification
pred: dict, # prediction {instance_id, model_name_or_path, model_patch}
rm_image: bool, # rm image after run_instance?
force_rebuild: bool, # rebuild image forcely?
client: docker.DockerClient, # Docker client
run_id: str, # unique run_id
timeout: int | None = None, # timeout
rewrite_reports: bool = False,# True = don't run test again, re-generate report.json from existing test_output.txt
) -> dict:

Initialization:

1
2
3
4
instance_id = test_spec.instance_id
model_name_or_path = pred.get(KEY_MODEL, "None").replace("/", "__")
log_dir = RUN_EVALUATION_LOG_DIR / run_id / model_name_or_path / instance_id
report_path = log_dir / LOG_REPORT

rewrite_reports method (existing test_output.txt) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if rewrite_reports:
test_output_path = log_dir / LOG_TEST_OUTPUT
if not test_output_path.exists():
raise ValueError(f"Test output file {test_output_path} does not exist")
# re-generate a new report from existing test_output file
report = get_eval_report(
test_spec=test_spec,
prediction=pred,
test_log_path=test_output_path,
include_tests_status=True,
)

with open(report_path, "w") as f:
# change report into a JSON string.
f.write(json.dumps(report, indent=4))
return {
"completed": True,
"resolved": report[instance_id]["resolved"],
}

If evaluation is interrupted mid-run, any instances whose report.json has already been written are automatically skipped on restart. This means you can safely kill the process (Ctrl+C, crash, etc.) and rerun it later — only unfinished instances will be evaluated, without duplicating completed work.

1
2
3
4
5
6
if report_path.exists():
report = json.loads(report_path.read_text())
return {
"completed": True,
"resolved": report[instance_id]["resolved"],
}

Create a shortcut in the logs directory, pointing to the image’s build directory. The purpose is to facilitate debugging - when you view the logs of a specific instance, you can follow this link to directly locate the corresponding Docker build files (Docker file, build logs, etc.).

1
2
3
4
5
6
7
8
9
10
11
12
13
if not test_spec.is_remote_image:
build_dir = INSTANCE_IMAGE_BUILD_DIR / test_spec.instance_image_key.replace(
":", "__"
)
image_build_link = log_dir / "image_build_dir"
if not image_build_link.exists():
try:
# symlink_to(): equal to ln -s <build_dir> <image_build_link>
image_build_link.symlink_to(
build_dir.absolute(), target_is_directory=True
)
except:
pass

log:

1
2
3
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / LOG_INSTANCE
logger = setup_logger(instance_id, log_file)
1.4.2.1.2 Apply Patch & Run eval.sh

Env & Variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
container = None
eval_completed = False
report = {}

try:
# If instance's image does not exist, trigger build_instance_image ()... run command `docker create` not `docker run`
container = build_container(
test_spec, client, run_id, logger, rm_image, force_rebuild
)

# container.start() equals to docker start <container_id>
container.start()
logger.info(f"Container for {instance_id} started: {container.id}")

# Apply patch phase:

# Create a local patch.diff file
patch_file = Path(log_dir / "patch.diff")
patch_file.write_text(pred[KEY_PREDICTION] or "")

logger.info(
f"Intermediate patch for {instance_id} written to {patch_file}, "
f"now applying to container..."
)

# Copy the diff file to docker
copy_to_container(container, patch_file, PurePosixPath(DOCKER_PATCHDOCKER_PATCH))

# Try to apply the diff file
# Try tree times. The three commands are in descending order of strictness.
# Break if one of three commands succeed.
"""
e.g.
workdir = "testbed"
user = "root"
git_apply_cmd = "git apply --verbose"
DOCKER_PATCH = "/tmp/patch.diff"

Full Command:
docker exec -w /testbed -u root <container> git apply --verbose /tmp/patch.diff
"""
applied_patch = False
for git_apply_cmd in GIT_APPLY_CMDS:
val = container.exec_run(
f"{git_apply_cmd} {DOCKER_PATCH}",
workdir=DOCKER_WORKDIR,
user=DOCKER_USER,
)
# val.exit_code == 0 means apply successful.
if val.exit_code == 0:
logger.info(f"{APPLY_PATCH_PASS}:\n{val.output.decode(UTF8)}")
applied_patch = True
break
else:
logger.info(f"Failed to apply patch to container: {git_apply_cmd}")

# If all three git apply commands fail:
if not applied_patch:
logger.info(f"{APPLY_PATCH_FAIL}:\n{val.output.decode(UTF8)}")
raise EvaluationError(
instance_id,
f"{APPLY_PATCH_FAIL}:\n{val.output.decode(UTF8)}",
logger,
)

# Record the status of repo before executing eval.sh
# git -c core.fileMode=false diff: Ignore the permission changes (Only track the diff of contents)
git_diff_output_before = (
container.exec_run(
"git -c core.fileMode=false diff", workdir=DOCKER_WORKDIR
)
.output.decode(UTF8)
.strip()
)
logger.info(f"Git diff before:\n{git_diff_output_before}")

Prepare the eval.sh file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# eval.sh is generated dynamically by properites of TestSpec.eval_script, including:
# - apply test_patch
"""
test_patch is a diff file provided by the issue provider, which includes the test item that will trigger the isssue.
"""
# - run test commands
# - restore test file

eval_file = Path(log_dir / "eval.sh")
eval_file.write_text(test_spec.eval_script)
logger.info(
f"Eval script for {instance_id} written to {eval_file}; "
f"copying to container..."
)
copy_to_container(container, eval_file, PurePosixPath("/eval.sh"))

Run eval.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# record the test_output, timed_out, total_runtime
test_output, timed_out, total_runtime = exec_run_with_timeout(
container, "/bin/bash /eval.sh", timeout
)

test_output_path = log_dir / LOG_TEST_OUTPUT
logger.info(f"Test runtime: {total_runtime:_.2f} seconds")

# write test_output into a local file
with open(test_output_path, "w") as f:
f.write(test_output)
logger.info(f"Test output for {instance_id} written to {test_output_path}")
if timed_out:
f.write(f"\n\nTimeout error: {timeout} seconds exceeded.")
raise EvaluationError(
instance_id,
f"Test timed out after {timeout} seconds.",
logger,
)

Record the repo status( Check if the eval.sh changed the repo accidently):

1
2
3
4
5
6
7
8
git_diff_output_after = (
container.exec_run(
"git -c core.fileMode=false diff", workdir=DOCKER_WORKDIR
)
.output.decode(UTF8)
.strip()
)
logger.info(f"Git diff after:\n{git_diff_output_after}")
1
2
if git_diff_output_after != git_diff_output_before:
logger.info("Git diff changed after running eval script")
1.4.2.1.3 Grading
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
logger.info(f"Grading answer for {instance_id}...")
report = get_eval_report(
test_spec=test_spec,
prediction=pred,
test_log_path=test_output_path,
include_tests_status=True, # True = including state of each instance's test case
)

logger.info(
f"report: {report}\n"
f"Result for {instance_id}: resolved: {report[instance_id]['resolved']}"
)

with open(report_path, "w") as f:
f.write(json.dumps(report, indent=4))
eval_completed = True
1.4.2.1.4 Exception Dealing & Clear Env

Exception:

1
2
3
4
5
6
7
8
9
10
11
except (EvaluationError, BuildImageError) as e:
error_msg = traceback.format_exc()
logger.info(error_msg)
print(e)
except Exception as e:
error_msg = (
f"Error in evaluating model for {instance_id}: {e}\n"
f"{traceback.format_exc()}\n"
f"Check ({logger.log_file}) for more information."
)
logger.error(error_msg)

Clear Env:

1
2
3
4
5
6
7
8
9
10
11
12
13
finally:
cleanup_container(client, container, logger)
# optional
if rm_image:
remove_image(client, test_spec.instance_image_key, logger)

# close logger system
close_logger(logger)

return {
"completed": eval_completed,
"resolved": report.get(instance_id, {}).get("resolved", False),
}

1.4.2.2 eval.sh

eval.sh is generated dynamically:

1
2
eval_file = Path(log_dir / "eval.sh")
eval_file.write_text(test_spec.eval_script)

e.g:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/bin/bash
set -uxo pipefail

# ============================================================
# ① activate conda env
# ============================================================
source /opt/miniconda3/bin/activate
conda activate testbed
cd /testbed

# ============================================================
# ② Language Setting (from specs["eval_commands"])
# ============================================================
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PYTHONIOENCODING=utf8
export LANGUAGE=en_US:en

# ============================================================
# ③ safety directory + state recording
# ============================================================
git config --global --add safe.directory /testbed
cd /testbed

# print current repo status
git status
git show
git -c core.fileMode=false diff abc123def456...

# ============================================================
# ④ re-activate env
# ============================================================
source /opt/miniconda3/bin/activate
conda activate testbed

# ============================================================
# ⑤ install (from specs["install"])
# ============================================================
python -m pip install -e .

# ============================================================
# ⑥ restore test project to the state of base_commit
# then apply test_patch
# ============================================================
# test_patch modified tests/validators/test_email.py
# no newly added file
# → reset_commands = ["git checkout abc123def456... tests/validators/test_email.py"]
git checkout abc123def456... tests/validators/test_email.py

# trans test_patch's content to git apply by heredoc
git apply -v - <<'EOF_114329324912'
diff --git a/tests/validators/test_email.py b/tests/validators/test_email.py
--- a/tests/validators/test_email.py
+++ b/tests/validators/test_email.py
@@ -45,6 +45,12 @@ class EmailValidatorTest(TestCase):
self.assertIsNone(validate_email("user@domain.com"))

+ def test_email_with_trailing_dot_fails(self):
+ """Email with trailing dot should raise ValidationError."""
+ with self.assertRaises(ValidationError):
+ validate_email("user@domain.com.")
+
def test_email_with_ipv4_address(self):
"""Email with IPv4 address should be valid."""
EOF_114329324912

# ============================================================
# ⑦ run test command (核心步骤)
# ============================================================
: '>>>>> Start Test Output'
./tests/runtests.py --verbosity 2 --settings=test_sqlite --parallel 1 validators.test_email
: '>>>>> End Test Output'

# ============================================================
# ⑧ restore test file
# ============================================================
git checkout abc123def456... tests/validators/test_email.py

1.4.2.3 get_eval_report

1
2
3
4
5
6
def get_eval_report(
test_spec, # including FAIL_TO_PASS / PASS_TO_PASS / repo...
prediction, # {"instance_id": "...", "model_patch": "diff --git ...", ...}
test_log_path, # "logs/.../test_output.txt"
include_tests_status,
) -> dict:

Step 1 : initialize report_map

1
2
3
4
5
6
7
8
report_map = {}
instance_id = prediction[KEY_INSTANCE_ID] # "django__django-11815"
report_map[instance_id] = {
"patch_is_None": False,
"patch_exists": False,
"patch_successfully_applied": False,
"resolved": False,
}

now the report_map:

1
2
3
4
5
6
7
8
report_map = {
"django__django-11815": {
"patch_is_None": False,
"patch_exists": False,
"patch_successfully_applied": False,
"resolved": False,
}
}

Step 2 : Check if patch exists

1
2
3
4
if prediction[KEY_PREDICTION] is None:
report_map[instance_id]["patch_is_None"] = True
return report_map # return, all False
report_map[instance_id]["patch_exists"] = True

Step 3 : Parse test_output.txt ➡ eval_status_map

1
2
3
4
eval_status_map, found = get_logs_eval(test_spec, test_log_path)
if not found:
return report_map # Parse failed,patch_successfully_applied keeps False
report_map[instance_id]["patch_successfully_applied"] = True

what did get_logs_eval() do ?

1
2
3
4
5
6
7
8
9
10
① Open test_output.txt
② Find if there's any error sign (APPLY_PATCH_FAIL / TESTS_ERROR / TESTS_TIMEOUT)
→ Yes?return ({}, False)
③ Find contents between ">>>>> Start Test Output" and ">>>>> End Test Output"
④ Call log_parser (e.g. parse_log_django) parse the log into dict:
"test_foo ... ok" → {"test_foo": "PASSED"}
"test_bar ... FAIL" → {"test_bar": "FAILED"}
"test_baz ... ok" → {"test_baz": "PASSED"}
⑤ return ({"test_foo": "PASSED", "test_bar": "FAILED", "test_baz": "PASSED"}, True)

Step 4 : Build gold standard reference:

1
2
3
4
5
eval_ref = {
"instance_id": test_spec.instance_id,
"FAIL_TO_PASS": test_spec.FAIL_TO_PASS, # ["test_bar"] — should be PASS after repairing
"PASS_TO_PASS": test_spec.PASS_TO_PASS, # ["test_foo", "test_baz"] — should keep PASS
}

Step 5 : Compare item by item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
eval_type = EvalType.PASS_AND_FAIL
report = get_eval_tests_report(eval_status_map, eval_ref, eval_type=eval_type)

# inner logic:
# FAIL_TO_PASS: gold said the [test_bar] should FAIL -> PASS
for test_case in ["test_F2P"]:
if test_case in eval_status_map && test_case.stat is PASSED:
f2p_success.append(test_case) # ✅
else:
f2p_failure.append(test_case) # ❌

# PASS_TO_PASS: gold said the [test_bar] should PASS -> PASS
for test_case in ["test_P2P_A", "test_P2P_B"]:
if test_case in eval_status_map && test_case.stat is PASSED:
f2p_success.append(test_case) # ✅
else:
f2p_failure.append(test_case) # ❌

report:

1
2
3
4
5
6
7
8
9
report = {
"FAIL_TO_PASS": {
"SUCCESS": ["test_F2P"], # BUG fixed
"FAILURE": [],
},
"PASS_TO_PASS": {
"SUCCESS": ["test_P2P_A", "test_P2P_B"], # existing fucntions not modified
"FAILURE": [],
},

Step 6 : Final Judgement

1
2
3
4
5
6
7
8
9
if get_resolution_status(report) == ResolvedStatus.FULL.value:
report_map[instance_id]["resolved"] = True

# inner part of get_resolution_status:
f2p = 1/1 = 1.0 # FAIL_TO_PASS success / total = 100%
p2p = 2/2 = 1.0 # PASS_TO_PASS success / total = 100%

if f2p == 1 and p2p == 1:
return ResolvedStatus.FULL.value # → resolved = True

Final Return:

1
2
3
4
5
6
7
8
9
10
11
12
report_map = {
"django__django-11815": {
"patch_is_None": False,
"patch_exists": True,
"patch_successfully_applied": True,
"resolved": True, # ← answer:this issue is resolved
"tests_status": { # ← exists when include_tests_status=True
"FAIL_TO_PASS": {"success": ["test_bar"], "failure": []},
"PASS_TO_PASS": {"success": ["test_foo", "test_baz"], "failure": []},
},
}
}

1.4.2.4 get_resolution_status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def get_resolution_status(report: dict[str, dict[str, Any]]) -> str:
"""
Determine resolved status of an evaluation instance

Criteria:
- If fail-to-pass (Resolution) = 1 and pass-to-pass (Maintenance) = 1 -> FULL
- If (fail-to-pass (Resolution) < 1 and > 0) and pass-to-pass (Maintenance) = 1 -> PARTIAL
- Otherwise -> NO
"""
f2p = compute_fail_to_pass(report)
p2p = compute_pass_to_pass(report)

if f2p == 1 and p2p == 1:
return ResolvedStatus.FULL.value
elif f2p < 1 and f2p > 0 and p2p == 1:
return ResolvedStatus.PARTIAL.value
else:
return ResolvedStatus.NO.value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def compute_fail_to_pass(report: dict[str, dict[str, Any]]) -> float:
"""
Compute fail-to-pass metric. Accepts single report as argument.
"""
total = len(report[FAIL_TO_PASS]["success"]) + len(report[FAIL_TO_PASS]["failure"])
if total == 0:
return 1
return len(report[FAIL_TO_PASS]["success"]) / total


def compute_pass_to_pass(report: dict[str, dict[str, Any]]) -> float:
"""
Compute pass-to-pass metric. Accepts single report as argument.
"""
total = len(report[PASS_TO_PASS]["success"]) + len(report[PASS_TO_PASS]["failure"])
if total == 0:
# TODO: Don't factor in p2p metrics
return 1
return len(report[PASS_TO_PASS]["success"]) / total

1.4.2.5 get_eval_tests_report

The function aims to do one thing: Classify the test cases into two categories:

FAIL_TO_PASS and PASS_TO_PASS.

Input:

1
2
3
4
5
6
7
8
9
10
11
12
13
# real results:
eval_status_map = {
"test_A": "PASSED",
"test_B": "FAILED",
"test_C": "FAILED",
"test_D": "PASSED",
}

# the gold references:
gold_results = {
"FAIL_TO_PASS": ["test_A", "test_B"]
"PASS_TO_PASS": ["test_C", "test_D"]
}

Output:

1
2
3
4
5
6
7
8
9
10
{
"FAIL_TO_PASS": {
"success": ["test_B"],
"failure": ["test_A"],
}
"PASS_TO_PASS": {
"success": ["test_D"],
"failure": ["test_C"],
}
}

1.4.3 run_instances

Step 1: instance → TestSpec

1
2
3
4
test_specs = list(map(
lambda instance: make_test_spec(instance, namespace=...),
instances,
))

trans JSON into TestSpec; generate all the scripts, and images’ tag;

300 instance -> 300 TestSpec.

Step 2: Check existing images

1
2
3
4
5
6
7
existing_images = {
tag
for i in client.images.list(all=True) # 本地所有 Docker 镜像
for tag in i.tags
if tag in instance_image_ids # 只保留本次需要的
}
print("Found 150 existing instance images. Will reuse them.")

Step 3: Package payloads

1
2
3
4
5
6
7
8
9
10
11
12
payloads = []
for test_spec in test_specs:
payloads.append((
test_spec,
predictions[test_spec.instance_id],
should_remove(...),
force_rebuild,
client,
run_id,
timeout,
rewrite_reports,
))

Step 4: Thread Pool Parallelization & progress bar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
stats = {"✓": 0, "✖": 0, "error": 0}
pbar = tqdm(total=len(payloads), desc="Evaluation", postfix=stats)
lock = threading.Lock()

def run_evaluation_with_progress(*args):
result = run_instance(*args)
with lock:
if result["completed"]:
if result["resolved"]:
stats["✓"] += 1
else:
stats["✖"] += 1
else:
stats["error"] += 1

pbar.set_postfix(stats)
pbar.update()

return

run_threadpool(run_evaluation_with_progress, payloads, max_workers)
print("All instances run.")

1.4.4 make_run_report

This function will traverse all instances’ report.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
for instance in full_dataset:
instance_id = instance["instance_id"]

# Does ths instance have prediction ?
if instance_id not in predictions:
incomplete_ids.add(instance_id) # didn't run
continue

# Is prediction empty?
if prediction["model_patch"] in ["", None]:
empty_patch_ids.add(instance_id) # empty prediction
continue

# Does report.json exist?
report_file = logs/.../django__django-11099/report.json
if report_file.exists(): # Test Completed
completed_ids.add(instance_id)

try:
content = report_file.read_text().strip()
if not content: # empty report, error
error_ids.add(instance_id)
continue

report = json.loads(content)

if report[instance_id]["resolved"]:
resolved_ids.add(instance_id) # ✅
else:
unresolved_ids.add(instance_id) # ❌

except (json.JSONDecodeError, KeyError):
error_ids.add(instance_id)
else:
error_ids.add(instance_id)

Clean env:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if client:
# get remaining images and containers
images = list_images(client)
test_specs = list(
map(
lambda x: make_test_spec(
x,
namespace=namespace,
instance_image_tag=instance_image_tag,
env_image_tag=env_image_tag,
),
full_dataset,
)
)
for spec in test_specs:
image_name = spec.instance_image_key
if image_name in images:
unremoved_images.add(image_name)
containers = client.containers.list(all=True)
for container in containers:
if run_id in container.name:
unstopped_containers.add(container.name)

Generate Final Report:

e.g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
report = {
"total_instances": 300,
"submitted_instances": 298,
"completed_instances": 285,
"resolved_instances": 134,
"unresolved_instances": 151,
"empty_patch_instances": 5,
"error_instances": 8,
"resolved_ids": ["django__django-11099", "sympy__sympy-12481", ...], # detailed list
"unresolved_ids": [...],
"error_ids": [...],
...
}
report_file = "gpt-4.my_run.json"
with open(report_file, "w") as f:
json.dump(report, f, indent=4)

2. Training

We can train a model to make it perform better on specific tasks like SWE-bench.

On SWE-bench, we usually use SFT + LoRA to train the model.

2.1 SFT (Supervised Fine Tuning)

The essence of SFT is that give LLM a set of [input - output] data. Then train it to imitate.

e.g. We have a 10000 pieces of data which includes:

  • The Problem (issue description + relative code file)
  • The Solution (repair patch)

By repeating studying those problems and solutions, the LLM will learn the ability:

1
See similary problem -> Inference correct solution

2.1.1 Mathematical Essence of SFT

We know that the LLM’s core thought is Next Token Prediction. The LLM outputs token according to the probability.

The SFT’s mathematical essence is maximizing the conditional probability:

1
2
3
4
Training Objective:
Maximize the (completion|prompt)
i.e.
Given specified prompt, the LLM should output the completion tokens with highest probability.

Next Token Prediction

  1. LLM see prompt, predict 1st token, compare the token with then one in completion -> calculate error.

  2. LLM see prompt + 1st token, predict 2nd token, compare the token with then one in completion -> calculate error.

  3. LLM see prompt + 1st +2nd token, predict 3rd token, compare the token with then one in completion -> calculate error.

  4. LLM see prompt + 1st +2nd + … (n-1)-th token, predict n-th token, compare the token with then one in completion -> calculate error.

Total loss = average of sum of the all errors.

In fact, the error is something to evaluation the gap between the real-token and predict-token whose real name is Cross-Entropy.

2.1.2 Cross Entropy

2.1.2.1 Introduction

Each time the LLM predicts the next token, it will give each token in vocabulary a probability.

e.g. At one moment, the LLM’s input:

1
2
"<issue>fix devide zero bug in foo.py</issue>\n<code>\ndef div(a,b):\n    return
a/b\n</code>\n\ndiff --git a/foo.py b/foo.py\n@@ -1,2 +1,4 @@\n"

The LLM will predict the next token, and give the candidates a probability:

1
2
3
4
5
6
" def"  → 0.72
" foo" → 0.08
" div" → 0.03
" the" → 0.01
...
(totally 150 K token candidates)

This is the probability. But how do we evaluate the quality of the prediction ?

We use the Cross Entropy:

$$ \boxed{\ell = -\ln P\bigl(x_t \;\big|\; x_{<t}\bigr)} $$

  • x < t = all context before position − t
  • xt = real token of position − t(comes from the real answer of completion )
  • P(xt ∣ x < t) = LLM’s Prediction Probability to position − t with context
  • = this step’s cross entropy

This formula’s essence is a Penalty Mechanism. It will give high probability a low penalty(loss), give low probability a high penalty (loss).

The probability P(xt | x < t) is between 0 and 1. So ln P(xt | x < t) is between −∞ and 0. So we can take the negative to have a positive interval: −ln P(xt | x < t).

How do the cross entropy work?

Example 1:

  1. P = 0.95
  2. ln (0.95) ≈ −0.051
  3.  = 0.051
  4. In Optimizer’s view, the loss is very low, the prediction is very perfect. The weights is nearly needed to adjust.

Example 2:

  1. P = 0.01
  2. ln (0.01) ≈ −4.605
  3.  = 4.605
  4. Optimizer will notice that 4.605 is a pretty huge penalty, the weights should be adjusted substantially.

2.1.2.2 Why Use the Cross Entropy?

Why we use −ln  as loss instead of using simply 1 − P?

The core thought of Cross Entropy is to Penalty the low probability. So we hope the penalty(Loss) of the low probability is extremely high. From math’s perspective, Function ln  has many properties:

Loss = −ln (P)

$\frac{d\ell}{dp} = - \frac{1}{P}$

The ln ’s slope will tend towards infinity when P → 0+.

This means that if the model is very confident in a wrong prediction (for example, P=0.0001), the loss will instantly soar above 9.29.2, and the gradient of backpropagation will also be extremely large, thereby forcing the model to quickly correct those “outrageous” errors.

2.1.3 Workflow of Processing One Piece of SFT Data

SFT’s training data is assembled by Prompt + Completion.

Assume we have one piece of data: Prompt: "Q: Hello" Completion: "A: Hi"; Assume their token id:

  • "Q" = 10, ":" = 11, "Hello" = 12
  • "A" = 20, ":" = 21, "Hi" = 22
  • <EOS> = 50

2.1.3.1 Step 1: Concatenation & Tokenization (Build Input)

1
2
3
tokens:  	 [Q,    :,    Hello,  A,    :,    Hi,   <EOS>]
token_ids: [10, 11, 12, 20, 21, 22, 50 ]
Index: 0 1 2 3 4 5 6

2.1.3.2 Step 2: Build Labels

Label in machine learning area means the Standard Answer.

LLM is a Next-Token-Prediction model. At post predict the post + 1 token.

Here the labels come from original token_ids we build:

1
2
3
4
token_ids:  [10,   11,   12,    20,   21,   22,   50  ]
↓ ↓ ↓ ↓ ↓ ↓ ↓
labels: [11, 12, 20, 21, 22, 50, -100 ]
Index: 0 1 2 3 4 5 6

At index 6, no more token need to be predicted, so use -100 as label.

2.1.3.3 Step 3: Loss Masking

The loss we discussed earlier, is a criterion to evaluate the gap between the predicted token and standard answer.

In SFT, we train the LLM to enhance the ability to predict the standard answer ( The Completion).

The full input text to LLM is Prompt + Completion, we should take the loss of Completion into consideration. So the ability to predict the Completion will improve.

[!WARNING]

However, we should not take the loss of original “Prompt” into consideration. We just want LLM learn how to predict the “Completion” part instead of “Prompt” part.

If LLM take the loss of Prompt into consideration, it will repeat the Prompt very well.

So we should remove the effect of Prompt's Loss. Modify the labels:

1
2
3
4
5
6
7
8
tokens:     [Q,    :,    Hello, A,    :,    Hi,   <EOS>]
token_ids: [10, 11, 12, 20, 21, 22, 50 ]
↓ ↓ ↓ ↓ ↓ ↓ ↓
labels: [11, 12, 20, 21, 22, 50, -100 ]
↓ ↓ ↓ ↓ ↓ ↓ ↓
labels: [-100, -100, 20, 21, 22, 50, -100 ]

Index: 0 1 2 3 4 5 6

We mask the labels of prediction to “Prompt”, e.g. label(0), and label(1).

[!NOTE]

Notice that the label(2), is to predict the first token of “Completion, do not mask it.”

2.1.3.4 Step 4: Forward Propagation & Loss Computing

Input of LLM:

1
["Q", ":", "Hello", "A", ":", "Hi", "<EOS>"]

After calculating by transformer, the transformer output 7 predictions, each prediction is a set of candidates’ probability:

Position Context Probability Distribution (Top 3)
0 ["Q"] ":" → 0.85
"Q" → 0.05
"A" → 0.02
1 ["Q", ":"] "Hello" → 0.70
"Hi" → 0.15
"How" → 0.05
2 ["Q", ":", "Hello"] "A" → 0.60
"How" → 0.10
"Hi" → 0.08
3 ["Q", ":", "Hello", "A"] ":" → 0.90
" " → 0.04
"nswer" → 0.02
4 ["Q", ":", "Hello", "A", ":"] "Hi" → 0.80
"Hello" → 0.10
"Hey" → 0.05
5 ["Q", ":", "Hello", "A", ":", "Hi"] "<EOS>" → 0.95
"!" → 0.02
"there" → 0.01
6 ["...", "<EOS>"] "The" → 0.10
"A" → 0.05

Now we have the answer of LLM, we should grade homework by calculating Cross Entropy according the Masked Labels.

Position Label Action Calculation Loss
0 -100 Skip 0
1 -100 Skip 0
2 "A" P → 0.60 −log⁡(0.60) 0.511
3 ":" P → 0.90 −log⁡(0.90) 0.105
4 "Hi" P → 0.80 −log⁡(0.80) 0.223
5 "<EOS>" P → 0.95 −log⁡(0.95) 0.051
6 -100 Skip 0

All loss:

1
[0, 0, 0.511, 0.105, 0.223, 0.051, 0]

Mean Reduction:

We should sum the loss and get an average loss to represent the loss of the whole answer. Notice that we should not take items of ignore_index=-100 into the calculation.

$$ \begin{align} \mathcal{L}_{total} &= \text{MeanReduction}(\mathcal{L}_{elementwise}) \\ &= \frac{\sum_{i \in \text{Valid}} \mathcal{L}_i}{N_{\text{valid}}} \\ &= \frac{0 + 0 + 0.511 + 0.105 + 0.223 + 0.051 + 0}{4} \\ &= 0.2225 \end{align} $$

2.1.3.4 Appendix: Gradient

In a neural network, Loss Function has many independent variables.

L = f(w1, w2, ...wn)

Gradient is a column vector includes all partial derivatives all L:

$$ \begin{equation} \nabla L = \begin{bmatrix} \frac{\partial L}{\partial w_1} \\ \frac{\partial L}{\partial w_2} \\ \vdots \\ \frac{\partial L}{\partial w_n} \end{bmatrix} \end{equation} $$

Directional Derivative :

Partial Derivative like $\frac{\partial L}{\partial w_1}$, means the rate of change of L along variable w1.

Directional Derivative : rate of change of the function L along any specified direction in space.

Assume that we want to calculate the function:

L(w1, w2, ...wn)’s Directional Derivative

along unit direction vector u at point P, denoted as Du⃗L .

Core Theorem: Directional Derivative is equal to the dot product Gradient Vector and Unit Direction Vector(Length == 1).

Du⃗L = ∇L ⋅ u⃗

  • Direction of the gradient is “Direction that makes L grow fastest”.

  • Negative Direction

…. to be continued

2.1.3.5 Step 5: Backward Propagation & Parameters Update

Now we have the loss of this ground of generation. We should adjust our weights according to reduce the Loss.

  1. Clear the gradient

    The default design of PyTorch is gradient accumulation. If not manually reset to zero, the newly calculated gradients will be accumulated on top of the old ones, leading to a “schizophrenic” model and completely chaotic parameter updates.

    1
    optimizer.zero_grad()
  2. Backward Propagation

We want to reduce the value of Loss, so we should calculate its gradient and calculate its direction.

Because of the setting-zero to the Loss of Prompt part, the gradient component of the parameter directly related to the prediction is also strictly 0.

Although the model “sees” the Prompt during forward propagation, the input from the Prompt part does not make any direct gradient contribution to the update of model parameters during Backward propagation.

  1. Update Weights

    After calculating the gradient, the Optimizer will update weights according to the gradient.

    1
    optimizer.step()

    θt = θt − 1 − η ⋅ ∇θℒ(θt − 1)

AdamW:

$$ \begin{align} % 1. 计算一阶矩估计(动量 Momentum) m_t &= \beta_1 m_{t-1} + (1 - \beta_1) g_t \\ % 2. 计算二阶矩估计(未中心化的方差) v_t &= \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \\ % 3. 偏差修正(Bias Correction),防止初始阶段估计偏小 \hat{m}_t &= \frac{m_t}{1 - \beta_1^t} \\ \hat{v}_t &= \frac{v_t}{1 - \beta_2^t} \\ % 4. 最终参数更新(包含自适应步长和权重衰减) \theta_t &= \theta_{t-1} - \eta \left( \frac{\hat{m}_t}{\sqrt{\hat{v}_t} + \epsilon} + \lambda \theta_{t-1} \right) \end{align} $$

本文链接:https://wangyier.top/swe-bench/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 The Great Library