Files
miner/.gitea/workflows/bench.yaml
T
grenadeandClaude Fable 5.1 ca6642960c
ci / fmt (pull_request) Successful in 31s
bench / build (pull_request) Successful in 1m1s
ci / clippy (pull_request) Successful in 1m55s
bench / measure (pull_request) Successful in 2m59s
ci / test (pull_request) Successful in 9m7s
ci / doc (pull_request) Successful in 1m42s
bench: serialise measurements per host with a flock, per-run file names
Gitea's concurrency group did not serialise two runs on benjy: the second
run's scp failed with ETXTBSY on the binary the first was executing (safe,
it happened before the miner was touched, but the run was lost). The pause,
lock, measure and resume sequence now lives in bench-on-host.sh, executed
over one ssh call under flock on the host, with per-run binary and record
names so staging never collides.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBgs2nSi4H2mdh8kD8vMX5
2026-09-03 13:06:45 +03:00

143 lines
5.5 KiB
YAML

---
# GPU hashrate and parity measurement on a fleet mining host (quantus/miner#2).
#
# Runner containers on the GPU hosts have no device passthrough (gongfoo gives
# them `devices: None`), so the benchmark cannot run inside the runner. The
# binary is built on a runner and executed on the host over ssh as gitea_ci,
# which has the GPU device nodes (0666) and the Vulkan ICD, exactly like the
# miner. The host's quantus-miner.service is stopped for the window and started
# again afterwards, under a trap, so a failed run cannot leave the host idle.
#
# Benjy (4090, dedicated) is the reference card; beast (2x 5090) also serves
# inference and is only benchmarked by manual dispatch.
name: bench
on:
workflow_dispatch:
inputs:
host:
description: mining host to measure
default: benjy.hanzalova.internal
type: choice
options:
- benjy.hanzalova.internal
- quadbrat.hanzalova.internal
- beast.hanzalova.internal
duration_secs:
description: seconds per timed window
default: "30"
runs:
description: timed windows (median reported)
default: "5"
batch_size:
description: nonces per GPU batch
default: "1000000"
workers:
description: worker threads (one per card)
default: "1"
pull_request:
paths:
- crates/engine-gpu/**
- crates/engine-cpu/**
- crates/pow-core/**
- crates/miner-service/**
- crates/bench-harness/**
- Cargo.lock
- .gitea/workflows/bench.yaml
env:
CARGO_TERM_COLOR: always
BENCH_HOST: ${{ github.event.inputs.host || 'benjy.hanzalova.internal' }}
DURATION: ${{ github.event.inputs.duration_secs || '30' }}
RUNS: ${{ github.event.inputs.runs || '5' }}
BATCH: ${{ github.event.inputs.batch_size || '1000000' }}
WORKERS: ${{ github.event.inputs.workers || '1' }}
# One measurement per host at a time; a second one would share the card.
concurrency:
group: bench-${{ github.event.inputs.host || 'benjy.hanzalova.internal' }}
cancel-in-progress: false
jobs:
build:
# cuda-13.0, not rust: the rust image is Fedora 44 and the mining hosts are
# Fedora 43, so a binary built there needs a glibc the hosts do not have
# (observed: "GLIBC_2.43 not found"). cuda-13.0 is Fedora 43 based, matches
# the hosts, and is what the deploy in #8 builds on anyway.
runs-on: cuda-13.0
steps:
- uses: actions/checkout@v4
- name: build quantus-bench
env:
MINER_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: cargo build --release --locked -p bench-harness
- uses: actions/upload-artifact@v3
with:
name: quantus-bench
path: target/release/quantus-bench
measure:
runs-on: fedora-43
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with: { name: quantus-bench, path: _bin }
- name: write ssh key
run: |
set -euo pipefail
install -d -m 0700 ~/.ssh
printf '%s\n' "${{ secrets.RSYNC_SSH_KEY }}" > ~/.ssh/id_gitea_ci
chmod 0600 ~/.ssh/id_gitea_ci
- name: measure on ${{ env.BENCH_HOST }}
run: |
set -euo pipefail
SSHOPTS="-i $HOME/.ssh/id_gitea_ci -o StrictHostKeyChecking=accept-new"
run() { ssh $SSHOPTS gitea_ci@"$BENCH_HOST" "$@"; }
# Per-run file names: two runs may overlap on the host until the
# host-side flock serialises them, and scp over a running binary
# fails with ETXTBSY.
id="${{ github.run_id }}"
dir=/var/lib/gitea_ci/bench
run "install -d -m 0750 $dir"
scp $SSHOPTS -q _bin/quantus-bench gitea_ci@"$BENCH_HOST":$dir/quantus-bench.$id
scp $SSHOPTS -q crates/bench-harness/bench-on-host.sh gitea_ci@"$BENCH_HOST":$dir/bench-on-host.$id.sh
run "chmod 0755 $dir/quantus-bench.$id"
cleanup() { ssh $SSHOPTS gitea_ci@"$BENCH_HOST" "rm -f $dir/quantus-bench.$id $dir/bench-on-host.$id.sh $dir/record.$id.json"; }
trap cleanup EXIT
label="${{ github.event.pull_request.number && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}"
# The host script pauses the miner, holds the per-host lock, measures,
# and resumes the miner under its own trap.
run "bash $dir/bench-on-host.$id.sh $dir/quantus-bench.$id $DURATION $RUNS $BATCH $WORKERS $label $dir/record.$id.json" | tee bench.log
# The summary is everything from the harness's markdown header on.
sed -n '/^## quantus-bench/,$p' bench.log > bench.md
scp $SSHOPTS -q gitea_ci@"$BENCH_HOST":$dir/record.$id.json record.json
{ echo; cat bench.md; } >> "$GITHUB_STEP_SUMMARY"
- uses: actions/upload-artifact@v3
if: always()
with:
name: bench-record-${{ env.BENCH_HOST }}
path: |
record.json
bench.md
- name: comment on the pull request
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
body=$(python3 - <<'PY'
import json, pathlib
print(json.dumps({"body": pathlib.Path("bench.md").read_text()}))
PY
)
curl -fsS -X POST \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Content-Type: application/json" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${{ github.event.pull_request.number }}/comments" \
-d "$body" > /dev/null