Skip to content

Commit f650b9d

Browse files
Meirtzclaude
andauthored
swe: re-apply spec pre_install after env reset; keep CI shards on disk (#128)
The attributed nightly (28560892001, 87/500 unresolved with per-stage log tails) decomposed into three classes, two of them ours: 1. Missing pre_install (48 instances). Task images bake the specs' pre_install tracked-file edits into /testbed at build time and prepare_env's `git reset --hard` reverts them. Without sphinx's `sed 's/pytest/pytest -rA/' tox.ini` the log parser sees zero per-test lines, so every sphinx run scored unresolved with exit 0 ("33 passed ... congratulations" in the tail); without astropy's setuptools pin the editable reinstall resolves a modern build chain on Python 3.9 and dies. _prepare_tests now re-applies pre_install ahead of eval_commands/install. 2. Runner disk exhaustion (24 instances): per-instance task images (~1-2 GB each, never reused) filled the runner mid-shard — "failed to register layer: ... no space left on device". The example gains --rmi-after (drop an instance's image once its rollout persists) and the nightly frees ~20 GB of preinstalled toolchains up front. 3. The remaining 11 are genuine gold-patch test failures (astropy-7606/8707/8872, psf-requests-1766/1921/2317, five sphinx) matching the environment-drift sets reported upstream (SWE-bench/SWE-bench#294, #484); they stay visible pending an explicit exclusion decision after the next full run. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 0700f66 commit f650b9d

4 files changed

Lines changed: 78 additions & 0 deletions

File tree

.github/workflows/nightly-ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ jobs:
5252
steps:
5353
- uses: actions/checkout@v4
5454

55+
# ~25 task images per shard exhaust the runner's default free
56+
# space even with per-instance cleanup; drop the preinstalled
57+
# toolchains we never use (~20 GB).
58+
- name: free runner disk
59+
run: |
60+
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL
61+
df -h /
62+
5563
- uses: astral-sh/setup-uv@v5
5664
with:
5765
enable-cache: true
@@ -90,6 +98,7 @@ jobs:
9098
uv run python main.py \
9199
--ground-truth \
92100
--fail-on-unresolved \
101+
--rmi-after \
93102
--bundle "$BUNDLE" \
94103
--docker-platform linux/amd64 \
95104
--dataset "$DATASET" \

examples/run-swe-rollouts/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import json
3434
import logging
3535
import os
36+
import subprocess
3637
import sys
3738
from pathlib import Path
3839
from typing import Any
@@ -215,6 +216,7 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
215216
parser.add_argument("--shard-index", type=int, default=0)
216217
parser.add_argument("--ground-truth", action="store_true")
217218
parser.add_argument("--fail-on-unresolved", action="store_true")
219+
parser.add_argument("--rmi-after", action="store_true")
218220
parser.add_argument("--concurrency", type=int, default=1)
219221
parser.add_argument("--openai-base-url", default=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"))
220222
parser.add_argument("--openai-api-key", default=os.environ.get("OPENAI_API_KEY", ""))
@@ -279,10 +281,19 @@ async def main(argv: list[str] | None = None) -> int:
279281
out_dir = Path(args.out)
280282
out_dir.mkdir(parents=True, exist_ok=True)
281283

284+
# Instance images are per-instance and never reused within a run;
285+
# on CI runners, keeping ~25 of them exhausts the disk mid-shard.
286+
images = {str(row["instance_id"]): dataset.image(row) for row in rows}
287+
282288
def _persist(rollout: Any) -> None:
283289
(out_dir / f"{rollout.instance_id}.json").write_text(json.dumps(rollout.to_dict(), indent=2))
284290
verdict = "PASS" if rollout.resolved else (rollout.skipped or rollout.error or "FAIL")
285291
print(f"[{rollout.instance_id}] {verdict} ({rollout.duration_s:.1f}s)")
292+
if args.rmi_after and rollout.instance_id in images:
293+
subprocess.run(
294+
["docker", "rmi", "-f", images[rollout.instance_id]],
295+
capture_output=True,
296+
)
286297

287298
print(f"selected {len(rows)} instance(s) (concurrency={args.concurrency}, ground_truth={args.ground_truth})")
288299
rollouts = await run_rollouts(

plugins/datasets/swebench/src/score.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ async def _apply_model_patch(patch: str, workdir: str, env: dict[str, str], time
135135
async def _prepare_tests(instance: dict, workdir: str, env: dict[str, str], timeout: float) -> tuple[bool, str]:
136136
specs = MAP_REPO_VERSION_TO_SPECS[instance["repo"]][instance["version"]]
137137
commands = [f"git config --global --add safe.directory {shlex.quote(workdir)}"]
138+
# Task images bake `pre_install` edits into /testbed's tracked files
139+
# at build time (sphinx: `sed 's/pytest/pytest -rA/' tox.ini` so the
140+
# log parser sees per-test lines; astropy: a setuptools pin that the
141+
# editable reinstall needs). `prepare_env`'s `git reset --hard`
142+
# reverts those edits, so re-apply them before install/eval.
143+
commands.extend(str(command) for command in specs.get("pre_install", []))
138144
commands.extend(str(command) for command in specs.get("eval_commands", []))
139145

140146
install = str(specs.get("install") or "").strip()

tests/test_swe_env.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,55 @@ async def test_score_apply_failure_is_attributed(monkeypatch) -> None:
183183
assert result["patch_applied"] is False
184184
assert result["failure_stage"] == "apply_patch"
185185
assert result["log_tail"] == "$ git apply\nerror: corrupt"
186+
187+
188+
async def test_prepare_tests_reapplies_pre_install_before_install(monkeypatch, tmp_path) -> None:
189+
# Task images bake pre_install's tracked-file edits in at build time
190+
# and prepare_env's `git reset --hard` reverts them — _prepare_tests
191+
# must re-apply them ahead of the (re)install.
192+
monkeypatch.setattr(
193+
swe_score,
194+
"MAP_REPO_VERSION_TO_SPECS",
195+
{
196+
"demo/demo": {
197+
"1.0": {
198+
"pre_install": ["sed -i 's/pytest/pytest -rA/' tox.ini"],
199+
"eval_commands": ["export LANG=C"],
200+
"install": "python -m pip install -e .",
201+
"test_cmd": "pytest -rA",
202+
}
203+
}
204+
},
205+
)
206+
ran: list[str] = []
207+
208+
async def record(command, workdir, env, timeout, *, conda=False):
209+
ran.append(command)
210+
return 0, "", False
211+
212+
monkeypatch.setattr(swe_score, "_run", record)
213+
monkeypatch.setattr(swe_score, "_remove_untracked_paths", _async(None))
214+
215+
ok, log = await swe_score._prepare_tests(
216+
{
217+
"repo": "demo/demo",
218+
"version": "1.0",
219+
"base_commit": "abc",
220+
"test_patch": (
221+
"diff --git a/tests/t.py b/tests/t.py\n"
222+
"--- a/tests/t.py\n"
223+
"+++ b/tests/t.py\n"
224+
"@@ -1 +1 @@\n"
225+
"-a\n"
226+
"+b\n"
227+
),
228+
},
229+
str(tmp_path),
230+
{},
231+
60.0,
232+
)
233+
234+
assert ok and log == ""
235+
sed_at = next(i for i, c in enumerate(ran) if c.startswith("sed "))
236+
install_at = next(i for i, c in enumerate(ran) if "pip install" in c)
237+
assert sed_at < install_at

0 commit comments

Comments
 (0)