Skip to content

Commit 4e6ebae

Browse files
committed
continue fixing.
1 parent 8b96cfc commit 4e6ebae

4 files changed

Lines changed: 346 additions & 14 deletions

File tree

‎.github/workflows/README.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ locally.
1111
1. Add GitHub Secrets:
1212
- `OPENAI_API_KEY`: model API key (required)
1313
- `OPENAI_BASE_URL`: optional, defaults to `https://api.deepseek.com/v1`
14+
- `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`: optional, strongly
15+
recommended. They are used **only** to authenticate to ECR Public for the
16+
DeepSWE task images (no other AWS call is made). Anonymous ECR Public
17+
pulls are throttled per source IP, and GitHub runners share theirs, so
18+
without these the run can fail with `429 ... Data limit exceeded`.
1419

1520
2. Navigate to **Actions** → **DeepSWE Benchmark** → **Run workflow**
1621

@@ -29,6 +34,30 @@ Docker` step asserts it) and Python 3.12, which Pier 0.3.x requires
2934
`uv venv --python 3.12`; inside the task containers the harness runs on the
3035
image's own interpreter.
3136

37+
### Task images and the ECR Public quota
38+
39+
DeepSWE's prebuilt task images live in `public.ecr.aws/d3j8x8q7/swe-bench-202605`.
40+
ECR Public meters **anonymous** pulls per source IP (500 GB/month) and a
41+
GitHub-hosted runner's egress address is shared with many other jobs, so the
42+
allowance is frequently exhausted and an unauthenticated run dies with:
43+
44+
```
45+
failed to resolve source metadata for public.ecr.aws/...: 429 Too Many Requests
46+
- Server message: toomanyrequests: Data limit exceeded
47+
```
48+
49+
The workflow therefore pulls every image the run needs *before* Pier starts
50+
(`Pre-pull task images`, resolved through Pier's own sampler), retries each
51+
pull with exponential backoff, and stops early with an actionable error. With
52+
`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` set it logs in to ECR Public
53+
first, so the pulls are metered against your AWS account instead of the
54+
runner's shared IP. Any AWS account works; the IAM user needs only
55+
`ecr-public:GetAuthorizationToken` and `sts:GetServiceBearerToken`.
56+
57+
Without those secrets, re-running the job may help on its own — a new run
58+
usually gets a different egress IP — but the quota is monthly and the address
59+
pool is shared, so credentials are the only reliable fix.
60+
3261
### Artifacts
3362

3463
- `deepswe-results-{run_id}`: `pier-results/` (per-task `result.json`, verifier

‎.github/workflows/deepswe-benchmark.yml‎

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ jobs:
6767
if [ "$conc" -lt 1 ]; then conc=1; fi
6868
6969
# 360 min is the hard maximum for GitHub-hosted runners; keep a slice
70-
# for checkouts, install, wheel build, summary and artifact upload.
70+
# for checkouts, install, wheel build, image pre-pull, summary and
71+
# artifact upload.
7172
job_cap=360; job_overhead=15
72-
# Per task, OUTSIDE any Pier timeout: pulling the task's prebuilt
73-
# image (each task has its own tag, and Pier deletes images with
74-
# `down --rmi all` after every trial, so every trial pays a cold
75-
# pull) plus building the agent child image and starting the
76-
# container. Measured generously; underestimating here is what gets
77-
# the job killed with trials missing.
73+
# Per task, OUTSIDE any Pier timeout: building the agent child image,
74+
# starting the container, and whatever the pre-pull step did not
75+
# fetch. Pier deletes each trial's image afterwards (`down --rmi
76+
# all`), so the child-image build is paid per task even though the
77+
# pre-pulled base images stay cached.
7878
pull_setup=8
7979
8080
# DeepSWE declares, for all 113 tasks: agent 10800 s, verifier run
@@ -207,6 +207,135 @@ jobs:
207207
uv build --wheel --out-dir dist
208208
ls -l dist
209209
210+
# The DeepSWE task images live in public.ecr.aws, which throttles
211+
# *anonymous* pulls per source IP: 500 GB/month, an allowance routinely
212+
# exhausted because a GitHub-hosted runner's egress address is shared
213+
# with many other jobs:
214+
# 429 Too Many Requests - toomanyrequests: Data limit exceeded
215+
# Any AWS account fixes it: ECR Public accepts its token and meters the
216+
# pulls against that account instead. Without credentials the run still
217+
# works, but only while the runner's IP is under quota.
218+
- name: Log in to ECR Public
219+
env:
220+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
221+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
222+
run: |
223+
set -euo pipefail
224+
if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
225+
echo "::notice::No AWS credentials - pulling task images anonymously."
226+
echo "Anonymous ECR Public pulls are throttled per source IP and GitHub runners share theirs."
227+
exit 0
228+
fi
229+
if ! command -v aws >/dev/null 2>&1; then
230+
echo "::error::AWS credentials are set but the aws CLI is missing"; exit 1
231+
fi
232+
# ECR Public only exists in us-east-1; the token is valid for 12 h,
233+
# long enough for the whole job. A failed login is not fatal: the
234+
# pre-pull step then falls back to anonymous pulls and reports
235+
# whatever actually goes wrong there.
236+
if ! aws ecr-public get-login-password --region us-east-1 \
237+
| docker login --username AWS --password-stdin public.ecr.aws; then
238+
echo "::warning::ECR Public login failed - the credentials are invalid or lack ecr-public:GetAuthorizationToken. Falling back to anonymous pulls, which are rate limited per source IP."
239+
fi
240+
241+
# Pull every task image before Pier starts, for two reasons:
242+
# * a throttled pull then fails the run in minutes with a readable
243+
# message instead of surfacing as a docker-compose stack trace in
244+
# the middle of the benchmark;
245+
# * the retry loop below actually gets to run - Pier reports a failed
246+
# image pull as a failed trial and does not retry it.
247+
# The set of images is resolved through Pier's own DatasetConfig, so it
248+
# is exactly what `pier run --n-tasks ... --sample-seed ...` will use.
249+
- name: Pre-pull task images
250+
env:
251+
N_TASKS: ${{ inputs.n_tasks }}
252+
run: |
253+
set -euo pipefail
254+
# workflow_dispatch supplies the default, but a dispatch via the API
255+
# can omit inputs entirely; never call the resolver with an empty arg.
256+
n_tasks="${N_TASKS:-10}"
257+
# Assign first: a command substitution's exit status inside a process
258+
# substitution is discarded, and a resolver failure would then show up
259+
# as the much less useful "no task image resolved".
260+
if ! resolved=$(
261+
.venv/bin/python python-agent-harness/benchmarks/deepswe_images.py \
262+
--path deep-swe/tasks --n-tasks "$n_tasks" --sample-seed 0
263+
); then
264+
echo "::error::Could not resolve the task images for n_tasks=${n_tasks} (see the traceback above)"; exit 1
265+
fi
266+
if [ -z "$resolved" ]; then
267+
echo "::error::No task image resolved from deep-swe/tasks"; exit 1
268+
fi
269+
mapfile -t rows <<< "$resolved"
270+
echo "Resolved ${#rows[@]} task(s) for n_tasks=${n_tasks}"
271+
272+
pull() {
273+
local image="$1" attempt=1 delay=15
274+
until docker pull --quiet "$image"; do
275+
if [ "$attempt" -ge 5 ]; then
276+
echo "::warning::docker pull ${image} failed after ${attempt} attempts"
277+
return 1
278+
fi
279+
echo "::warning::docker pull ${image} failed (attempt ${attempt}); retrying in ${delay}s"
280+
sleep "$delay"
281+
attempt=$((attempt + 1))
282+
delay=$((delay * 2))
283+
done
284+
}
285+
286+
declare -A seen
287+
pulled=0
288+
total=${#rows[@]}
289+
failed=0
290+
# Room kept free for the trials themselves (each builds a child image
291+
# and writes logs and verifier artifacts).
292+
reserve_mb=4096
293+
for row in "${rows[@]}"; do
294+
image="${row#*$'\t'}"
295+
if [ -n "${seen[$image]:-}" ]; then continue; fi
296+
seen[$image]=1
297+
298+
# Different tasks' images share most layers, so N tasks cost far
299+
# less than N x an image: the 10-task sample is 3.2 GB compressed
300+
# (~7 GB unpacked) and the runner has ~14 GB. Stop here rather than
301+
# letting a trial die on `no space left on device`.
302+
avail=$(df -Pm / | awk 'NR==2 {print $4}')
303+
if [ "$avail" -lt "$reserve_mb" ]; then
304+
echo "::error::Only ${avail} MiB of disk left after ${pulled}/${total} image(s): this runner cannot hold the images plus the trials. Request fewer tasks (1-2 give a meaningful score) or use a larger runner."
305+
exit 1
306+
fi
307+
308+
if ! pull "$image"; then
309+
failed=$((failed + 1))
310+
# The anonymous quota does not recover inside one job, so stop
311+
# rather than spending four more minutes on every other image.
312+
break
313+
fi
314+
pulled=$((pulled + 1))
315+
done
316+
317+
if [ "$failed" -ne 0 ]; then
318+
echo "::error::${failed} task image(s) could not be pulled from public.ecr.aws."
319+
echo "Most likely ECR Public throttled the anonymous pull: it meters per source IP and GitHub runners share theirs."
320+
echo "Fix: add AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY secrets (any AWS account with ecr-public:GetAuthorizationToken and sts:GetServiceBearerToken) so pulls are authenticated, or re-run this job so it lands on another runner."
321+
echo "If the log shows 'no space left on device', the runner's disk is the limit: request fewer tasks."
322+
exit 1
323+
fi
324+
echo "All task images are present locally."
325+
326+
# The egress-proxy container builds FROM ubuntu:24.04 (Docker Hub),
327+
# which is rate limited too, though far less aggressively.
328+
# Best effort: a failure here is not fatal.
329+
docker pull --quiet ubuntu:24.04 \
330+
|| echo "::warning::Could not pre-pull ubuntu:24.04"
331+
332+
# Space left for the trials themselves.
333+
df -h / | tail -n 1
334+
avail=$(df -Pm / | awk 'NR==2 {print $4}')
335+
if [ "$avail" -lt "$reserve_mb" ]; then
336+
echo "::warning::Only ${avail} MiB free after pre-pulling. Trials may fail with 'no space left on device'; use fewer tasks or a larger runner."
337+
fi
338+
210339
- name: Run DeepSWE benchmark
211340
env:
212341
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

‎benchmarks/deepswe-benchmark.md‎

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ providers set it explicitly:
2424
- **Anthropic**: `https://api.anthropic.com/v1`
2525
- **ModelScope**: `https://api-inference.modelscope.cn/v1`
2626

27+
Optionally, add AWS credentials:
28+
29+
```
30+
AWS_ACCESS_KEY_ID = ...
31+
AWS_SECRET_ACCESS_KEY = ...
32+
```
33+
34+
These are used **only** to authenticate to ECR Public, where the prebuilt task
35+
images live. ECR Public meters anonymous pulls per source IP and GitHub runners
36+
share theirs, so without credentials the job can fail with
37+
`429 ... Data limit exceeded` even though the model API key is fine. See
38+
[ECR Public throttles the task image pulls](#ecr-public-throttles-the-task-image-pulls-429).
39+
2740
The request body carries `reasoning_effort: "max"` and a fixed `temperature: 0.0`
2841
(see `DEFAULT_REASONING_EFFORT` in the adapter). Both are overridable without
2942
editing code:
@@ -172,6 +185,8 @@ Checkout python-agent-harness + DeepSWE tasks
172185
↓
173186
Install harness (editable) + Pier >= 0.3.1 into a Python 3.12 venv, build wheel
174187
↓
188+
Log in to ECR Public (if AWS secrets are set), pre-pull the task images
189+
↓
175190
pier run -p deep-swe/tasks --agent-import-path ...
176191
↓
177192
Per task: build an image FROM the task's prebuilt image,
@@ -243,6 +258,43 @@ The workflow creates its venv with `uv venv .venv --python 3.12`. Only the
243258
host-side Pier driver runs on 3.12; inside the task container the harness runs
244259
on the image's own interpreter (`python3`, 3.11+).
245260

261+
### ECR Public throttles the task image pulls (429)
262+
263+
DeepSWE's prebuilt images come from `public.ecr.aws/d3j8x8q7/swe-bench-202605`
264+
(0.8–1.6 GB compressed each). ECR Public meters **anonymous** pulls per source
265+
IP, and GitHub-hosted runners share their egress addresses, so a lazy pull fails
266+
like this partway through a run:
267+
268+
```
269+
#6 [main internal] load metadata for public.ecr.aws/d3j8x8q7/swe-bench-202605:...:
270+
#6 ERROR: failed to copy: httpReadSeeker: failed open: unexpected status code
271+
https://public.ecr.aws/v2/...: 429 Too Many Requests
272+
- Server message: toomanyrequests: Data limit exceeded
273+
```
274+
275+
The workflow handles this in two steps before Pier starts:
276+
277+
1. `Log in to ECR Public` — if `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
278+
secrets exist, it runs `aws ecr-public get-login-password` and
279+
`docker login public.ecr.aws`. Authenticated pulls are metered against your
280+
AWS account (10 pulls/s per region) instead of the 500 GB/month anonymous
281+
allowance that is shared with everyone behind the runner's IP. Any AWS
282+
account works; the IAM user needs only `ecr-public:GetAuthorizationToken`
283+
and `sts:GetServiceBearerToken`.
284+
2. `Pre-pull task images` — resolves the exact image set for the selected
285+
`n_tasks`/`sample_seed` through Pier's own `DatasetConfig` (so it matches
286+
the run), pulls each with exponential backoff (15 s → 120 s, five attempts),
287+
and stops early with an actionable message if the quota is exhausted or the
288+
runner's disk fills up (see
289+
[Resource limits](#resource-limits-why-concurrency-defaults-to-1)).
290+
291+
Without AWS credentials the failure is now a two-minute error instead of a
292+
docker-compose stack trace an hour in. Re-running the job may help on its own,
293+
because a new run usually gets a different egress IP — but the quota is monthly
294+
and the address pool is shared, so credentials are the only reliable fix.
295+
296+
Pre-pulling does not change the scores: images are only fetched earlier.
297+
246298
### Docker Issues
247299

248300
GitHub-hosted `ubuntu-latest` **includes Docker Engine** by default. The
@@ -267,8 +319,17 @@ resource policy is `auto` → enforced as a hard Docker **limit**
267319
process, and the egress-proxy container are counted. Hence `concurrency: 1`
268320
by default (2 vCPU / 8 GB in use, leaving headroom).
269321

270-
Disk is bounded because Pier deletes each trial's container *and image*
271-
(`down --rmi all`) after it finishes, so prebuilt images do not accumulate.
322+
Disk stays bounded: Pier stops each environment with
323+
`down --rmi all --volumes`, which removes the trial's container and the child
324+
image it built, so per-trial layers do not accumulate. The prebuilt task images
325+
themselves are not compose service images and therefore stay cached for the rest
326+
of the job — but tasks' images share most of their layers, so Docker dedups
327+
them. Measured compressed layer unions: 0.9 GB for 1 task, 1.0 GB for 2,
328+
3.2 GB for the 10-task sample, 7 GB for 25 (a single image alone is ~0.8 GB).
329+
Unpacked, expect roughly 2×. Against the ~14 GB a GitHub-hosted runner has, a
330+
handful of tasks is fine and a large run is not; the `Pre-pull task images` step
331+
checks free space as it goes and fails with an actionable message instead of
332+
letting a trial die on `no space left on device`.
272333

273334
Do not "fix" a memory error with `--ignore` resource modes: Pier warns that
274335
running a task outside its declared resources can disqualify the result. If you
@@ -285,7 +346,7 @@ The `prepare` job therefore derives both values from your inputs and passes
285346
timeouts together):
286347

287348
- `agent budget = min(task_timeout_minutes, cap-driven ceiling)`
288-
- `job timeout = rounds × (scaled per-task worst case + cold pull/build) + overhead`,
349+
- `job timeout = rounds × (scaled per-task worst case + per-task setup) + overhead`,
289350
capped at 360
290351

291352
What that means in practice (`task_timeout_minutes: 180`, i.e. DeepSWE's own
@@ -297,11 +358,14 @@ budget):
297358
| 2 | 1 | 123 min | 359 min | yes |
298359
| 10 | 1 | 20 min | 359 min | yes |
299360
| 10 | 2 | 46 min | 360 min | yes (if the runner can host 2×8 GB) |
300-
| 25 | 1 | 9 min (floor) | 360 min | ~24 of 25 |
301-
| 0 (113) | 1 | 9 min (floor) | 360 min | ~24 of 113 |
361+
| 25 | 1 | 9 min (floor) | 360 min | ~17 of 25 |
362+
| 0 (113) | 1 | 9 min (floor) | 360 min | ~17 of 113 |
302363

303-
(8 min per task is reserved outside any Pier timeout for the cold image pull and
304-
the agent child-image build, because Pier deletes images after every trial.)
364+
(8 min per task is reserved outside any Pier timeout for building the agent
365+
child image, starting the container and unpacking whatever the pre-pull step
366+
did not download. Pier deletes each trial's image afterwards, so this cost is
367+
paid per task even though the base images stay cached — see
368+
[Resource limits](#resource-limits-why-concurrency-defaults-to-1).)
305369

306370
The derived budget is echoed to the run summary and a `::warning::` is emitted
307371
whenever it differs from what you asked for.
@@ -354,6 +418,13 @@ prepends that directory via `PYTHONPATH`, which is unaffected.
354418
- Use a different model endpoint
355419
- Re-run the job (`pier job resume`) once the limit clears
356420

421+
### `429 Too Many Requests` while pulling task images
422+
423+
That is the ECR Public quota, not your model API key — see
424+
[ECR Public throttles the task image pulls](#ecr-public-throttles-the-task-image-pulls-429).
425+
The workflow now pulls the images up front, retries, and reports which image
426+
failed; add the AWS secrets so the pulls are authenticated.
427+
357428
## Advanced Configuration
358429

359430
### Model Switching

0 commit comments

Comments
 (0)