-
Notifications
You must be signed in to change notification settings - Fork 2
242 lines (230 loc) · 12.7 KB
/
Copy pathbench-verify.yml
File metadata and controls
242 lines (230 loc) · 12.7 KB
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
name: Bench verifier
# Manual-only (/bench-verify, or workflow_dispatch to test workflow changes on a
# branch — issue_comment always runs main's copy of this file, see profile-recursion.yml);
# separate from /bench so they never share the bench server.
on:
workflow_dispatch:
inputs:
pairs:
description: "ABBA pair count (2-40)"
required: false
default: "20"
issue_comment:
types: [created]
concurrency:
# Serialization is provided by the single self-hosted bench runner (jobs queue
# for it). Never cancel a running bench: real /bench-verify comments share the
# per-PR group and queue; any other comment (this workflow fires on every
# issue_comment) gets a unique throwaway group so it can't sit in — or evict —
# the real queue.
group: ${{ startsWith(github.event.comment.body, '/bench-verify') && format('bench-verify-{0}', github.event.issue.number) || format('bench-verify-ignore-{0}', github.run_id) }}
cancel-in-progress: false
permissions:
contents: read
pull-requests: write
issues: write
jobs:
verify:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/bench-verify') &&
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association))
runs-on: [self-hosted, bench]
# Job cap. On a cold runner the recursion BUILDS dominate: per ref a guest build, a
# prover-test build, and a measuring-CLI build (the CLI is built FROM each ref so it
# understands that ref's own guest syscalls). Cached in /tmp; build-std and the host
# cargo target are both per ref (see the recursion step's env), so a ref's second and
# later presets reuse its own compiled deps — but the two refs never share a dir.
# 125 rather than 90 to absorb the continuation arms: a 20-tx continuation prove per
# ref on each side of the verifier bench, plus a 20-tx (was 4-tx) block for the cycle
# comparison. Keep this ABOVE the sum of the step caps below (50 + 70 = 120) so a
# runaway step always trips its OWN timeout first: a step timeout still runs the
# `always()` Post result step, whereas hitting the job cap is a cancellation and is far
# less dependable about doing so — which would lose the comment entirely.
timeout-minutes: 125
steps:
- name: Acknowledge (react + occupancy notice)
if: github.event_name == 'issue_comment'
uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: context.payload.comment.id, content: 'eyes'
});
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number,
body: '⏳ **Benchmark started** on the bench server. Two verifier arms (monolithic + continuations over an ethrex 20-tx block), then the recursion-guest cycle comparison, which adds guest builds on top — longer on a cold runner. The bench server is occupied until it finishes.'
});
- name: Resolve PR head + pair count
id: cfg
env:
GH_TOKEN: ${{ github.token }}
PR_NUM: ${{ github.event.issue.number }}
COMMENT_BODY: ${{ github.event.comment.body }}
DISPATCH_PAIRS: ${{ github.event.inputs.pairs }}
run: |
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then
# Testing this workflow's own changes: bench the dispatched branch vs main.
HEAD_SHA="$GITHUB_SHA"
N="${DISPATCH_PAIRS:-20}"
else
# Head SHA (not branch name) so fork PRs resolve and a mid-run force-push can't race.
HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$GITHUB_REPOSITORY" --json headRefOid -q .headRefOid)
# Optional pair count "/bench-verify 32"; default 20.
N=$(echo "$COMMENT_BODY" | sed -n 's|^/bench-verify[[:space:]]*\([0-9]\+\).*|\1|p')
N=${N:-20}
fi
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
if ! [[ "$N" =~ ^[0-9]+$ ]]; then
echo "::warning::pair count '$N' is not a number; using 20"
N=20
fi
if [ "$N" -lt 2 ] || [ "$N" -gt 40 ]; then
echo "::warning::pair count $N out of range [2,40]; using 20"
N=20
fi
echo "pairs=$N" >> "$GITHUB_OUTPUT"
- name: Checkout (full history for ref resolution)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch PR head commit (works for fork PRs)
if: github.event_name == 'issue_comment'
env:
PR_NUM: ${{ github.event.issue.number }}
run: git fetch origin "pull/$PR_NUM/head" --quiet
- name: Add cargo to PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Run verifier benchmark
id: run
# Own cap so a hung continuation prove/verify can't eat the whole job budget and
# starve the recursion step. On timeout `always()` still posts the failure tail.
timeout-minutes: 50
env:
HEAD_SHA: ${{ steps.cfg.outputs.head_sha }}
PAIRS: ${{ steps.cfg.outputs.pairs }}
# The script defaults to the real block; this job pins the synthetic one.
# /bench-verify reports TWO arms and the real path runs only the continuation
# one (a real block does not fit monolithically), so inheriting the default
# would silently drop an arm and break comparability with every verify number
# recorded so far.
WORKLOAD: synthetic
run: |
export SYSROOT_DIR="$HOME/.lambda-vm-sysroot"
set -o pipefail
scripts/bench_verify.sh "$HEAD_SHA" origin/main "$PAIRS" 2>&1 | tee /tmp/verify_out.txt
sed -n '/<!-- verify-abba-report -->/,$p' /tmp/verify_out.txt > /tmp/verify_result.txt
# Additive: deterministic recursion-guest cycle+accelerator diff (PR vs main), in
# two regimes: `min` (cheap canary over the empty diagnostic program) and
# `blowup2-block` (the same verifier over a REAL ethrex 20-tx block proved with
# continuations, via the `continuation` guest). One exact `execute --cycles`
# reading per ref (no ABBA); blowup2-block's dumped blob is cached by ref SHA
# (bench_recursion_cycles.sh), so a repeat run skips re-proving.
# GUEST_TARGET_DIR and HOST_TARGET_DIR are BASE paths — bench_recursion_cycles.sh
# appends the ref SHA so each worktree owns its target dirs. Sharing one dir across
# refs made cargo mix rlibs from both worktrees: loudly on the guest side (broke
# every regime after the first) and SILENTLY on the host side (cargo declared the
# second ref fresh and the run measured the first ref's binary).
# continue-on-error + `!cancelled()` isolate this from the verifier bench above.
- name: Run recursion guest cycle benchmark
id: recursion
if: ${{ !cancelled() }}
continue-on-error: true
# Fail-fast under the job cap so a runaway build can't burn the whole job
# (continue-on-error absorbs the timeout; the verifier verdict still posts).
timeout-minutes: 70
env:
HEAD_SHA: ${{ steps.cfg.outputs.head_sha }}
# Base paths for the per-ref guest and host target dirs (`<base>_<sha8>`), both
# rooted under the script's /tmp cache dir: build-std and native deps survive
# across runs (and across presets within a run) instead of being rebuilt cold.
GUEST_TARGET_DIR: /tmp/recursion_cycles_run/guest_target
HOST_TARGET_DIR: /tmp/recursion_cycles_run/host_target
run: |
export SYSROOT_DIR="$HOME/.lambda-vm-sysroot"
.github/scripts/run_recursion_bench.sh "$HEAD_SHA"
- name: Post result
if: always()
uses: actions/github-script@v7
env:
HEAD_SHA: ${{ steps.cfg.outputs.head_sha }}
PAIRS: ${{ steps.cfg.outputs.pairs }}
OUTCOME: ${{ steps.run.outcome }}
RECURSION_OUTCOME: ${{ steps.recursion.outcome }}
with:
script: |
const fs = require('fs');
const read = (p) => { try { return fs.readFileSync(p, 'utf8').trim(); } catch { return ''; } };
// Bound any raw-log fallback so a future header rename (empty *_result.txt)
// can't dump the entire build log into the PR comment.
const tail = (s, n) => s.split('\n').slice(-n).join('\n');
const head = (process.env.HEAD_SHA || '').slice(0, 10), pairs = process.env.PAIRS;
let body = `## Verifier benchmark — \`${head}\` vs \`main\` (${pairs} pairs, monolithic + continuations)\n\n`;
if (process.env.OUTCOME === 'success') {
const res = read('/tmp/verify_result.txt') || tail(read('/tmp/verify_out.txt'), 30);
body += res + '\n';
// Scope this to the rows it actually describes: only the Verify-time rows are
// ABBA. It used to be a blanket claim, which was wrong for the proof sizes here
// and for every guest-cycle number in the section below.
body += '\n<sub>Verify-time rows only: drift-free interleaved A/B/B/A, with paired-t ';
body += 'and exact Wilcoxon — trust the verdict when the two agree. Proof sizes are ';
body += 'single exact readings (no averaging). - = PR faster.</sub>\n';
} else {
// A step timeout or OOM kill takes the whole script down, so the graceful
// CONT_SKIP path never runs. bench_verify.sh renders the monolithic report as
// soon as that arm finishes, so post it rather than throwing away a verdict
// that was already measured. Path is $WORK/result_mono.txt in that script.
const mono = read('/tmp/verify_run/result_mono.txt');
if (mono) {
body += '⚠️ Run did not complete — the monolithic arm had already finished, ';
body += 'so its result is below. The continuation arm is missing.\n\n' + mono + '\n';
}
body += `❌ Run failed. Last log lines:\n\n` + '```\n' + tail(read('/tmp/verify_out.txt'), 30) + '\n```\n';
}
// Additive recursion-guest cycle section, kept clearly separated from the
// verifier verdict above so a failure here can't change how the bench reads.
body += '\n---\n\n## Recursion guest cycles — verifier running INSIDE the VM (main vs PR)\n\n';
if (process.env.RECURSION_OUTCOME === 'success') {
const rec = read('/tmp/recursion_result.txt') || tail(read('/tmp/recursion_out.txt'), 20);
if (rec) {
body += rec + '\n';
} else {
body += '_No recursion comparison output was captured._\n';
}
} else {
const rtail = tail(read('/tmp/recursion_out.txt'), 20);
body += '⚠️ Recursion cycle bench did not complete (does not affect the verifier verdict above).';
body += rtail ? ' Last log lines:\n\n' + '```\n' + rtail + '\n```\n' : '\n';
}
// workflow_dispatch has no PR to comment on; write to the job summary instead.
if (context.eventName !== 'issue_comment') {
await core.summary.addRaw(body).write();
return;
}
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Verifier benchmark —'));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: existing.id, body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.issue.number, body
});
}
# continue-on-error above protects the posted verifier result, not the failure itself.
- name: Fail if recursion cycle bench didn't complete
if: always() && steps.recursion.outcome != 'success'
run: |
echo "::error::Recursion cycle bench step did not complete (outcome=${{ steps.recursion.outcome }}) — see its log and the posted result above."
exit 1