Merge infra/bench-variance (#54)
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
# Measure the CI runner's own benchmark variance.
|
||||||
|
#
|
||||||
|
# #54 asks whether benchmark regressions can be gated. The threshold is the
|
||||||
|
# whole problem: too tight and CI goes red on noise, which trains people to
|
||||||
|
# re-run until green; too loose and it never fires. Which of those is possible
|
||||||
|
# depends on a number nobody has measured — how much this runner's results move
|
||||||
|
# between identical runs.
|
||||||
|
#
|
||||||
|
# So: run one unchanged benchmark ten times and report the spread. If it is
|
||||||
|
# ~15%, a fixed-threshold gate is dead and the answer is a tracker; if it is
|
||||||
|
# ~2%, a gate at 10% is meaningful.
|
||||||
|
#
|
||||||
|
# Manual only. It takes ten benchmark runs and answers a question that is asked
|
||||||
|
# once, not every push.
|
||||||
|
name: Benchmark variance
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
runs:
|
||||||
|
description: How many repeats
|
||||||
|
required: false
|
||||||
|
default: "10"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
variance:
|
||||||
|
name: runner variance on one benchmark
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
# `joint_factorise_480_appearances` is the right probe: ~9 ms, so it is
|
||||||
|
# long enough not to be dominated by timer overhead, and it is the
|
||||||
|
# measurement this crate most wants protected — the dense factorisation
|
||||||
|
# #52 is about replacing.
|
||||||
|
- name: Warm up
|
||||||
|
run: cargo bench --bench joint -- joint_factorise_480_appearances --warm-up-time 1 --measurement-time 3
|
||||||
|
|
||||||
|
- name: Repeat the same benchmark
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for i in $(seq 1 "${{ inputs.runs || '10' }}"); do
|
||||||
|
echo "== run $i =="
|
||||||
|
cargo bench --bench joint -- \
|
||||||
|
joint_factorise_480_appearances --warm-up-time 1 --measurement-time 3 \
|
||||||
|
2>&1 | tee -a raw.txt
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Report the spread
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Criterion prints `time: [lo mid hi]` with a unit after each. Take
|
||||||
|
# the midpoints. `sort -n` rather than awk's `asort`, which is a gawk
|
||||||
|
# extension the runner's mawk does not have — that failed on the
|
||||||
|
# first try here.
|
||||||
|
grep -oE 'time:[[:space:]]+\[[^]]+\]' raw.txt \
|
||||||
|
| sed -E 's/.*\[[^ ]+ [^ ]+ ([0-9.]+) ([^ ]+).*/\1 \2/' > mids.txt
|
||||||
|
echo "--- midpoints ---"
|
||||||
|
cat mids.txt
|
||||||
|
# Criterion picks a unit per run, so mixed units would have us
|
||||||
|
# comparing 9 ms against 9 us as if they were the same number — the
|
||||||
|
# plausible-looking wrong answer this crate keeps removing. Refuse.
|
||||||
|
if [ "$(cut -d' ' -f2 mids.txt | sort -u | wc -l)" -ne 1 ]; then
|
||||||
|
echo "runs reported different units; the spread would be meaningless"
|
||||||
|
cut -d' ' -f2 mids.txt | sort | uniq -c
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sort -n mids.txt | awk '{ v[NR]=$1; u=$2; s+=$1 }
|
||||||
|
END {
|
||||||
|
if (NR == 0) { print "no samples parsed - see the raw.txt artifact"; exit 1 }
|
||||||
|
printf "n = %d\n", NR
|
||||||
|
printf "min = %.4f %s\n", v[1], u
|
||||||
|
printf "median = %.4f %s\n", v[int((NR+1)/2)], u
|
||||||
|
printf "max = %.4f %s\n", v[NR], u
|
||||||
|
printf "mean = %.4f %s\n", s/NR, u
|
||||||
|
printf "spread = %.2f%% (max-min)/min\n", 100*(v[NR]-v[1])/v[1]
|
||||||
|
print ""
|
||||||
|
print "Read it against #54: a spread near 15% kills both"
|
||||||
|
print "fixed-threshold options and the answer is a tracker;"
|
||||||
|
print "a spread near 2% makes a gate at 10% meaningful."
|
||||||
|
}'
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: bench-variance-raw
|
||||||
|
path: |
|
||||||
|
raw.txt
|
||||||
|
mids.txt
|
||||||
+7
-1
@@ -18,8 +18,14 @@ fn fitted() -> History<String> {
|
|||||||
.beta(1.0)
|
.beta(1.0)
|
||||||
.score_sigma(2.0)
|
.score_sigma(2.0)
|
||||||
.drift(ConstantDrift::new(0.05))
|
.drift(ConstantDrift::new(0.05))
|
||||||
|
// `max_iter: 30` was here, and this fixture needs more: `converge`
|
||||||
|
// reported `NotConverged { iterations: 30, final_step: (4.5e-4, 0.0) }`
|
||||||
|
// once it stopped returning short fits silently. The benchmark measures
|
||||||
|
// the factorisation, whose cost depends on the fit's *shape* rather
|
||||||
|
// than its exactness — but measuring it on an unconverged fit is still
|
||||||
|
// measuring something nobody would run.
|
||||||
.convergence(ConvergenceOptions {
|
.convergence(ConvergenceOptions {
|
||||||
max_iter: 30,
|
max_iter: trueskill_tt::ITERATIONS,
|
||||||
epsilon: 1e-10,
|
epsilon: 1e-10,
|
||||||
alpha: 1.0,
|
alpha: 1.0,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user