Skip to content

Commit ef25a75

Browse files
authored
Merge pull request #61 from alanshurafa/claude/swebench-50task-resilience
Make batch runs resumable, fault-tolerant, and shardable
2 parents 505f6f0 + 3a97ee5 commit ef25a75

10 files changed

Lines changed: 5427 additions & 281 deletions

File tree

benchmarks/code/scripts/evaluate-swebench.sh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,23 @@ case "$MODE" in
4343
predictions)
4444
predictions="${1:-}"
4545
[[ -n "$predictions" ]] || { code_die "predictions mode needs a JSONL file"; exit 2; }
46+
shift || true
47+
# The evaluator names its report after the run id and nothing else, so two
48+
# benchmark runs of the same condition are indistinguishable afterwards and
49+
# a page built from them silently mixes subsets. --label stamps the batch
50+
# into the run id so a report can be traced back to the run that made it.
51+
LABEL="code-bench"
52+
while (( $# > 0 )); do
53+
case "$1" in
54+
--label) LABEL="${2:?--label needs a value}"; shift 2 ;;
55+
*) code_die "unknown evaluate option: $1"; exit 2 ;;
56+
esac
57+
done
58+
[[ "$LABEL" =~ ^[A-Za-z0-9._-]+$ ]] || { code_die "--label must be filesystem-safe"; exit 2; }
4659
bash "$CODE_DIR/validate-predictions.sh" "$predictions" "$SUITE"
4760
predictions_dir=$(cd "$(dirname "$predictions")" && pwd -P)
4861
predictions="$predictions_dir/$(basename "$predictions")"
49-
run_id="code-bench-$(date -u +%Y%m%dT%H%M%SZ)"
62+
run_id="$LABEL-$(date -u +%Y%m%dT%H%M%SZ)"
5063
(cd "$EVAL_ROOT" && "$CLI" eval verified -p "$predictions" --run-id "$run_id" -j "${CODE_BENCH_EVAL_JOBS:-1}")
5164
;;
5265
*)

benchmarks/code/scripts/run-canary.sh

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ RUN_ID=""
1010
CONDITIONS="A,B,C"
1111
TASK_LIMIT=1
1212
TASK=""
13+
SHARD_SPEC=""
1314
MAX_CLAUDE=""
1415
DRY_RUN=false
1516
SUITE=$(code_suite_id)
@@ -20,6 +21,7 @@ while (( $# > 0 )); do
2021
--conditions) CONDITIONS="${2:?--conditions needs a value}"; shift 2 ;;
2122
--task-limit) TASK_LIMIT="${2:?--task-limit needs a value}"; shift 2 ;;
2223
--task) TASK="${2:?--task needs a value}"; shift 2 ;;
24+
--shard) SHARD_SPEC="${2:?--shard needs N/M}"; shift 2 ;;
2325
--max-claude-dispatches) MAX_CLAUDE="${2:?--max-claude-dispatches needs a value}"; shift 2 ;;
2426
--dry-run) DRY_RUN=true; shift ;;
2527
*) code_die "unknown canary option: $1"; exit 2 ;;
@@ -30,6 +32,21 @@ done
3032
[[ "$TASK_LIMIT" =~ ^[1-9][0-9]*$ ]] || { code_die "--task-limit must be positive"; exit 2; }
3133
[[ "$MAX_CLAUDE" =~ ^[0-9]+$ ]] || { code_die "--max-claude-dispatches is required"; exit 2; }
3234

35+
# --shard N/M splits the subset across M processes that run at the same time.
36+
# Shards take disjoint instances, so their cell directories never collide, and
37+
# each writes its own prediction file because concurrent appends to one file
38+
# interleave. Wall clock falls by roughly M; provider concurrency rises by M,
39+
# which is the real limit on how far this can be pushed.
40+
SHARD_INDEX=0
41+
SHARD_COUNT=1
42+
if [[ -n "$SHARD_SPEC" ]]; then
43+
[[ "$SHARD_SPEC" =~ ^[0-9]+/[0-9]+$ ]] || { code_die "--shard must look like N/M"; exit 2; }
44+
SHARD_INDEX="${SHARD_SPEC%%/*}"
45+
SHARD_COUNT="${SHARD_SPEC##*/}"
46+
(( SHARD_COUNT >= 1 )) || { code_die "--shard M must be at least 1"; exit 2; }
47+
(( SHARD_INDEX < SHARD_COUNT )) || { code_die "--shard N must be less than M"; exit 2; }
48+
fi
49+
3350
suite_json=$(code_suite_json "$SUITE")
3451
subset=$(code_subset_path "$suite_json")
3552

@@ -54,36 +71,91 @@ fi
5471
pred_dir="$CODE_BENCH_RESULTS_ROOT/predictions/$RUN_ID"
5572
mkdir -p "$pred_dir"
5673

74+
runs_root="$CODE_BENCH_RESULTS_ROOT/runs/$RUN_ID"
75+
failed_cells=0
76+
done_cells=0
77+
skipped_cells=0
78+
5779
task_index=0
5880
while IFS= read -r instance; do
5981
task_index=$((task_index + 1))
6082
(( task_index <= TASK_LIMIT )) || break
83+
(( (task_index - 1) % SHARD_COUNT == SHARD_INDEX )) || continue
6184
for condition in $(printf '%s' "$CONDITIONS" | tr ',' ' '); do
62-
input=$(bash "$CODE_DIR/scripts/prepare-swebench-instance.sh" "$instance" "$RUN_ID" "$condition")
85+
cell="$runs_root/$instance/$condition"
86+
if (( SHARD_COUNT > 1 )); then
87+
pred_file="$pred_dir/$condition.s$SHARD_INDEX.jsonl"
88+
else
89+
pred_file="$pred_dir/$condition.jsonl"
90+
fi
91+
# A fifty-task batch is hours long, so it has to be restartable. A cell that
92+
# already holds a prediction is finished and is left alone; a cell without
93+
# one is an abandoned clone from an interrupted attempt, and re-preparing it
94+
# is the only way forward because prepare refuses an existing directory.
95+
if [[ -f "$cell/prediction.json" ]]; then
96+
# The cell is the record of truth; the prediction file is a projection of
97+
# it. Re-link a finished cell whose line is missing, so a resume under a
98+
# different shard layout still yields complete prediction files.
99+
if ! jq -e --arg id "$instance" 'select(.instance_id == $id)' "$pred_file" \
100+
>/dev/null 2>&1; then
101+
jq -c . "$cell/prediction.json" >> "$pred_file"
102+
fi
103+
printf 'SKIP: %s/%s already has a prediction\n' "$instance" "$condition"
104+
skipped_cells=$((skipped_cells + 1)); continue
105+
fi
106+
[[ ! -d "$cell" ]] || rm -rf "$cell"
107+
if ! input=$(bash "$CODE_DIR/scripts/prepare-swebench-instance.sh" "$instance" "$RUN_ID" "$condition"); then
108+
printf 'CELL FAILED: could not prepare %s/%s\n' "$instance" "$condition" >&2
109+
failed_cells=$((failed_cells + 1)); continue
110+
fi
63111
tier=$(jq -r --arg id "$condition" '.conditions[] | select(.id == $id) | .tier' \
64112
"$CODE_DIR/conditions.json" | tr -d '\r')
65113
# Single-shot cells have no agent loop, so they take the other driver. The
66114
# tier decides, not a hardcoded condition list: a new single-shot arm routes
67115
# itself the moment conditions.json declares its tier.
116+
#
117+
# A cell that produces no patch is a zero for that arm, not a reason to
118+
# abandon the batch: the subset is the denominator either way, and an abort
119+
# here used to throw away every remaining cell in the run.
120+
cell_rc=0
68121
if [[ "$tier" == "single-shot" ]]; then
69122
agent=$(jq -r --arg id "$condition" \
70123
'.conditions[] | select(.id == $id) | .dispatches | to_entries
71124
| map(select(.value > 0)) | .[0].key' \
72125
"$CODE_DIR/conditions.json" | tr -d '\r')
73126
bash "$CODE_DIR/drivers/run-single-shot.sh" --input "$input" \
74-
--predictions "$pred_dir/$condition.jsonl" --agent "$agent"
127+
--predictions "$pred_file" --agent "$agent" || cell_rc=$?
75128
else
76129
per_condition=$(jq -r --arg id "$condition" '.conditions[] | select(.id == $id) | .dispatches.claude' \
77130
"$CODE_DIR/conditions.json" | tr -d '\r')
78131
bash "$CODE_DIR/drivers/run-workflow.sh" --input "$input" \
79-
--predictions "$pred_dir/$condition.jsonl" \
80-
--max-claude-dispatches "$per_condition"
132+
--predictions "$pred_file" \
133+
--max-claude-dispatches "$per_condition" || cell_rc=$?
134+
fi
135+
if (( cell_rc != 0 )); then
136+
printf 'CELL FAILED: %s/%s produced no prediction (rc=%s)\n' "$instance" "$condition" "$cell_rc" >&2
137+
failed_cells=$((failed_cells + 1))
138+
else
139+
done_cells=$((done_cells + 1))
81140
fi
82141
done
83142
done < <(if [[ -n "$TASK" ]]; then printf '%s\n' "$TASK"
84143
else jq -r '.instances[].instance_id' "$subset" | tr -d '\r'; fi)
85144

86-
for predictions in "$pred_dir"/*.jsonl; do
145+
# Validate only this shard's own files. A sibling shard is still appending to
146+
# its own, and validating a file mid-write reports a failure that is not real.
147+
shopt -s nullglob
148+
if (( SHARD_COUNT > 1 )); then
149+
validate_glob=("$pred_dir"/*.s"$SHARD_INDEX".jsonl)
150+
else
151+
validate_glob=("$pred_dir"/*.jsonl)
152+
fi
153+
for predictions in "${validate_glob[@]}"; do
87154
bash "$CODE_DIR/validate-predictions.sh" "$predictions" "$SUITE"
88155
done
89-
printf 'COMPLETE: canary predictions -> %s\n' "$pred_dir"
156+
printf 'COMPLETE: %s cell(s) generated, %s reused, %s failed -> %s\n' \
157+
"$done_cells" "$skipped_cells" "$failed_cells" "$pred_dir"
158+
if (( failed_cells > 0 )); then
159+
printf 'INCOMPLETE: %s cell(s) produced no prediction. They score zero against the\n' "$failed_cells" >&2
160+
printf 'subset; rerun the same command to retry only those cells.\n' >&2
161+
fi

benchmarks/code/tests/test-code-bench.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,55 @@ else
207207
fail "the canary runner routes single-shot cells to their own driver"
208208
fi
209209

210+
# A fifty-task batch is hours long. One arm that produces no patch is a zero for
211+
# that arm, and an abort there discards every remaining cell in the run.
212+
RESUME_ROOT="$TMP/resume"
213+
mkdir -p "$RESUME_ROOT/runs/resume-test/pallets__flask-5014/A"
214+
jq -n '{instance_id:"pallets__flask-5014",model_name_or_path:"co-evolution-condition-A",
215+
model_patch:"diff --git a/a.py b/a.py\n"}' \
216+
> "$RESUME_ROOT/runs/resume-test/pallets__flask-5014/A/prediction.json"
217+
resume_out=$(CODE_BENCH_SUITE=swebench-verified-poc CODE_BENCH_RESULTS_ROOT="$RESUME_ROOT" \
218+
bash "$RUNNER" run-canary --run-id resume-test --task pallets__flask-5014 \
219+
--conditions A --max-claude-dispatches 1 2>&1)
220+
if printf '%s' "$resume_out" | grep -q 'SKIP: pallets__flask-5014/A already has a prediction' \
221+
&& printf '%s' "$resume_out" | grep -q '0 cell(s) generated, 1 reused, 0 failed'; then
222+
pass "a cell with a prediction is reused instead of rerun"
223+
else
224+
fail "a cell with a prediction is reused instead of rerun"
225+
fi
226+
227+
# Shards must partition the subset: every instance claimed by exactly one shard,
228+
# or a parallel run silently double-spends on some tasks and skips others.
229+
SHARD_ROOT="$TMP/shard"
230+
while IFS= read -r inst; do
231+
mkdir -p "$SHARD_ROOT/runs/shard-test/$inst/A"
232+
jq -n --arg id "$inst" '{instance_id:$id,model_name_or_path:"x",model_patch:"diff\n"}' \
233+
> "$SHARD_ROOT/runs/shard-test/$inst/A/prediction.json"
234+
done < <(jq -r '.instances[].instance_id' "$CODE_DIR/subsets/swebench-verified-canary.json" | tr -d '\r')
235+
shard_total=0
236+
shard_ok=true
237+
for s in 0 1; do
238+
out=$(CODE_BENCH_RESULTS_ROOT="$SHARD_ROOT" bash "$RUNNER" run-canary \
239+
--run-id shard-test --shard "$s/2" --task-limit 5 --conditions A \
240+
--max-claude-dispatches 5 2>&1)
241+
n=$(printf '%s' "$out" | grep -c '^SKIP: ')
242+
shard_total=$((shard_total + n))
243+
# 5 instances over 2 shards is 3 and 2; neither may be empty or take them all.
244+
if (( n == 0 || n == 5 )); then shard_ok=false; fi
245+
done
246+
if [[ "$shard_ok" == true ]] && (( shard_total == 5 )); then
247+
pass "shards partition the subset exactly once each"
248+
else
249+
fail "shards partition the subset exactly once each (total=$shard_total)"
250+
fi
251+
252+
if bash "$RUNNER" run-canary --run-id bad-shard --shard 3/2 --conditions A \
253+
--max-claude-dispatches 1 --dry-run >/dev/null 2>&1; then
254+
fail "a shard index outside its count is rejected"
255+
else
256+
pass "a shard index outside its count is rejected"
257+
fi
258+
210259
if jq -e 'all(.conditions[]; (.tier == "agentic") or (.tier == "single-shot"))' \
211260
"$CODE_DIR/conditions.json" >/dev/null 2>&1; then
212261
pass "every condition declares a tier"

benchmarks/site/aggregate.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
1010
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
1111
RESULTS_ROOT="${CODE_BENCH_RESULTS_ROOT:-$REPO_ROOT/benchmarks/results/code}"
1212
SUITE="swebench-verified-canary"
13+
RUN_LABEL=""
1314
OUTPUT="$RESULTS_ROOT/site/leaderboard.json"
1415

1516
while (( $# > 0 )); do
1617
case "$1" in
1718
--suite) SUITE="${2:?--suite needs a value}"; shift 2 ;;
19+
--run-label) RUN_LABEL="${2:?--run-label needs a value}"; shift 2 ;;
1820
--output) OUTPUT="${2:?--output needs a value}"; shift 2 ;;
1921
*) printf 'ERROR: unknown aggregate option: %s\n' "$1" >&2; exit 2 ;;
2022
esac
@@ -23,11 +25,15 @@ done
2325
command -v python >/dev/null 2>&1 || { printf 'ERROR: python is required\n' >&2; exit 1; }
2426
[[ -d "$RESULTS_ROOT/evaluation" ]] || { printf 'ERROR: no evaluation directory under %s\n' "$RESULTS_ROOT" >&2; exit 1; }
2527

28+
label_args=()
29+
[[ -n "$RUN_LABEL" ]] && label_args=(--run-label "$RUN_LABEL")
30+
2631
python "$SCRIPT_DIR/build-site-data.py" \
2732
--repo-root "$REPO_ROOT" \
2833
--results-root "$RESULTS_ROOT" \
2934
--suite "$SUITE" \
3035
--output "$OUTPUT" \
36+
${label_args[@]+"${label_args[@]}"} \
3137
--generated-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
3238

3339
python "$SCRIPT_DIR/render-page.py" --data "$OUTPUT" --output "${OUTPUT%%.json}.html"

0 commit comments

Comments
 (0)