research/tools/benchmarks/Equi-X/scripts/autotune_c_flags.sh

145 lines
7.2 KiB
Bash
Raw Normal View History

2026-07-30 14:45:21 +02:00
#!/usr/bin/env bash
# Auto-select the FASTEST optimization flags for the MAIN C runner on THIS
# machine, then install the winner as build/runners/c/equix_runner (the path the
# `equix-c` adapter uses). So the main benchmark always runs the fastest build
# this host can produce, rather than a hard-coded guess.
#
# It builds a few candidate flag sets with the default C compiler, benchmarks the
# JIT solve path (which dominates PoW cost) with enough reps for a stable median,
# ranks them, and copies the fastest binary over the main runner. Each rep solves
# a FRESH challenge derived from the seed (a SHA-256 chain), so tuning spans the
# same varied-challenge distribution the main solve benchmark measures — not one
# fixed challenge. The seed is deterministic, so every candidate sees the IDENTICAL
# challenge stream, keeping the flag comparison a fair apples-to-apples A/B.
# Idempotent: candidate build dirs are reused, so re-runs are cheap.
#
# Tunables (env):
# CC compiler to tune (default: cc)
# EQUIX_AUTOTUNE_REPS timed solves per candidate (default: 1000)
# EQUIX_AUTOTUNE_WARMUP warmup solves per candidate (default: 8)
# EQUIX_AUTOTUNE_SEED challenge SEED hex (default: deadbeef); each rep
# solves the next SHA-256-derived challenge in the chain
# EQUIX_AUTOTUNE_EPSILON if plain -O3 is within this fraction of the best,
# prefer -O3 (portable, no native/LTO lock-in on a
# sub-noise win). Default: 0.01 (1%). Set 0 for strict.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
# Ctrl+C mid-tune: exit cleanly (130) instead of a bare SIGINT kill. Nothing to
# roll back — candidate build dirs are reused and the main runner is only ever
# replaced by the atomic mv at the very end, so an interrupt leaves it untouched.
trap 'echo; echo "autotune: interrupted (Ctrl+C); main runner left unchanged." >&2; exit 130' INT
CC="${CC:-cc}"
REPS="${EQUIX_AUTOTUNE_REPS:-1000}"
WARMUP="${EQUIX_AUTOTUNE_WARMUP:-8}"
# Accept the legacy EQUIX_AUTOTUNE_CHALLENGE name as a fallback for the seed.
SEED="${EQUIX_AUTOTUNE_SEED:-${EQUIX_AUTOTUNE_CHALLENGE:-deadbeef}}"
EPS="${EQUIX_AUTOTUNE_EPSILON:-0.01}"
NPROC="$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
EQUIX_COMMIT="$(git -C vendored/equix rev-parse --short HEAD 2>/dev/null || echo unknown)"
command -v "$CC" >/dev/null 2>&1 || { echo "autotune: compiler '$CC' not found"; exit 1; }
command -v python3 >/dev/null 2>&1 || { echo "autotune: python3 required"; exit 1; }
# Candidate flag sets on the default compiler. -O0/-O1 are excluded on purpose
# (the flag sweep shows ~2x regression); these are the fast-tier contenders.
CANDIDATES=(
"o2|-O2 -DNDEBUG"
"o3|-O3 -DNDEBUG"
"o3-native|-O3 -march=native -DNDEBUG"
"o3-lto|-O3 -flto -DNDEBUG"
)
# Median solve wall-time (ns) for a runner binary, or non-zero exit if unusable.
# NB: the Python script is passed via -c so the runner's piped JSON reaches its
# stdin (a heredoc would be consumed as the script instead).
measure() {
printf '{"schema_version":1,"operation":"solve","runtime":"try-compile","repetitions":%d,"warmup":%d,"challenge_seed_hex":"%s"}' \
"$REPS" "$WARMUP" "$SEED" | "$1" 2>/dev/null | python3 -c '
import sys, json, statistics
try:
d = json.loads(sys.stdin.read().splitlines()[-1])
assert d.get("ok") and d.get("runs")
runs = d["runs"]
walls = [r["wall_ns"] for r in runs if r.get("wall_ns", 0) > 0]
# Sanity: every rep must be validly timed, and the build must really solve
# SOMETHING across the stream. Under the varied-challenge seed, individual
# derived challenges legitimately yield 0 solutions (~17% of them), so the
# guard is total-solutions>0, not the old per-rep all(>0) which this breaks.
assert len(walls) == len(runs)
assert sum(r.get("solutions", 0) for r in runs) > 0
print(int(statistics.median(walls)))
except BaseException: # incl. KeyboardInterrupt on Ctrl+C: exit quietly, no stray traceback
sys.exit(1)
'
}
# Parallel indexed arrays (macOS ships bash 3.2, which has no associative arrays).
echo "autotune: compiler=$CC reps=$REPS warmup=$WARMUP seed=$SEED (challenge varied per rep)"
# NB: candidates are built with the plain version "1.0.0" — the winner becomes
# the MAIN runner, and a tuning-suffixed version would leak machine-specific
# provenance into published results. The chosen flags are recorded separately
# in build/runners/c/equix_runner.flags and build/provenance.json.
NAMES=(); OKFLAGS=(); MEDIANS=()
for entry in "${CANDIDATES[@]}"; do
IFS='|' read -r name flags <<<"$entry"
bdir="build/autotune/$name"
log="build/autotune/$name.log"
mkdir -p build/autotune
# A copied/moved repo carries a CMake cache with old absolute paths; clean it.
if [ -f "$bdir/CMakeCache.txt" ]; then
recorded="$(sed -n 's/^CMAKE_CACHEFILE_DIR:INTERNAL=//p' "$bdir/CMakeCache.txt" | head -1)"
if [ -n "$recorded" ] && [ "$recorded" != "$(cd "$bdir" && pwd)" ]; then
rm -rf "$bdir"
fi
fi
if ! cmake -S runners/c -B "$bdir" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER="$CC" \
-DCMAKE_C_FLAGS_RELEASE="$flags" \
-DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
-DEQUIX_C_COMMIT="$EQUIX_COMMIT" \
-DEQUIX_C_VERSION="1.0.0" >/dev/null 2>"$log" \
|| ! cmake --build "$bdir" -j"$NPROC" --target equix_runner >>"$log" 2>&1; then
echo " skip $name ($flags): build failed (see $log)"
continue
fi
if ! median="$(measure "$bdir/equix_runner")"; then
echo " skip $name ($flags): benchmark failed / no solutions"
continue
fi
NAMES+=("$name"); OKFLAGS+=("$flags"); MEDIANS+=("$median")
printf ' %-10s %-26s median %s ms\n' "$name" "$flags" \
"$(python3 -c "print(f'{$median/1e6:.3f}')")"
done
[ "${#NAMES[@]}" -gt 0 ] || { echo "autotune: no candidate usable; keeping existing runner"; exit 1; }
# Pick the minimum-median candidate; find plain -O3's index for the tie-break.
best_i=0; o3_i=-1
for i in "${!NAMES[@]}"; do
[ "${MEDIANS[$i]}" -lt "${MEDIANS[$best_i]}" ] && best_i=$i
[ "${NAMES[$i]}" = "o3" ] && o3_i=$i
done
# Tie-break: if plain -O3 is within EPS of the winner, prefer it -- avoids
# locking the main runner into a native/LTO build over a sub-noise difference.
if [ "$o3_i" -ge 0 ] && [ "${NAMES[$best_i]}" != "o3" ] \
&& awk "BEGIN{exit !(${MEDIANS[$o3_i]} <= ${MEDIANS[$best_i]}*(1+$EPS))}"; then
echo " note: -O3 within ${EPS} of best (${NAMES[$best_i]}); preferring -O3 for portability"
best_i=$o3_i
fi
best_name="${NAMES[$best_i]}"; best_flags="${OKFLAGS[$best_i]}"; best_median="${MEDIANS[$best_i]}"
mkdir -p build/runners/c
# Install atomically: copy to a temp path, then rename over the live runner, so a
# Ctrl+C during the copy can never leave a half-written (corrupt) main binary.
cp -f "build/autotune/$best_name/equix_runner" build/runners/c/equix_runner.tmp
mv -f build/runners/c/equix_runner.tmp build/runners/c/equix_runner
printf '%s' "$best_flags" > build/runners/c/equix_runner.flags
echo "==> fastest flags on this machine: '$best_flags' ($best_name), median $(python3 -c "print(f'{$best_median/1e6:.3f}')") ms"
echo " installed as build/runners/c/equix_runner (used by the 'equix-c' main run)"