#!/usr/bin/env bash
# generator-budget-sweep — how many cores may the background generator take?
#
# Produces a BUDGET CURVE, not a yes/no. For each thread cap it measures the
# generator's actual core usage and the CPU mesher's throughput under it, so the
# output is "N cores costs X% of meshing" rather than "it seemed fine".
#
# WHY A CAP IS NEEDED AT ALL. terrain_diffusion's api.py never calls
# torch.set_num_threads and sets no OMP_NUM_THREADS — MEASURED, the service ran
# 39 threads and one single request consumed 6.10 of 16 cores (38%). So
# "--jobs 2" in the earlier test was not two cores, it was about twelve, and
# raising the job count would only have queued work behind an already-saturated
# service. Until the cap exists there is no budget to tune.
#
# ALTERNATES clean/loaded per cap and discards a warm-up, for the same reason as
# mesher-contention-test.sh: the first run reads high on cold caches and an
# unclamped turbo clock, and leaving it in inflated the noise floor to 22%.
#
# scripts/generator-budget-sweep.sh [--caps "1 2 4 8"] [--rounds 4]
set -uo pipefail
CAPS="1 2 4 8"; ROUNDS=4; CHUNKS=200; PORT=8831
TD=/storage/projects/code/godot/terrain-diffusion
BENCH=/tmp/bench_dc
OUT=/tmp/generator-budget.csv
while [ $# -gt 0 ]; do
case "$1" in
--caps) CAPS="$2"; shift 2 ;;
--rounds) ROUNDS="$2"; shift 2 ;;
--chunks) CHUNKS="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
*) echo "unknown arg: $1" >&2; exit 2 ;;
esac
done
[ -x "$BENCH" ] || { echo "FATAL: build $BENCH first" >&2; exit 1; }
run_bench () {
(cd "$(dirname "$BENCH")" && "$BENCH" "$CHUNKS" 2>/dev/null) \
| grep -oE "\| [0-9]+ chunks/s" | tail -1 | grep -oE "[0-9]+"
}
cores_of () { # pid, seconds -> cores consumed
local p=$1 s=$2 a b
a=$(awk '{print ($14+$15)/100}' /proc/$p/stat 2>/dev/null) || { echo 0; return; }
sleep "$s"
b=$(awk '{print ($14+$15)/100}' /proc/$p/stat 2>/dev/null) || { echo 0; return; }
echo "scale=2; ($b-$a)/$s" | bc
}
echo "cap,round,phase,chunks_per_s,gen_cores" > "$OUT"
echo "generator-budget-sweep caps='$CAPS' rounds=$ROUNDS chunks=$CHUNKS"
printf " warm-up (discarded) "; run_bench
for cap in $CAPS; do
echo "--- OMP_NUM_THREADS=$cap"
( cd "$TD" && OMP_NUM_THREADS=$cap MKL_NUM_THREADS=$cap setsid nohup \
.venv/bin/python -m terrain_diffusion.inference.api --device cpu \
--no-compile --seed 1234 --port "$PORT" > /tmp/td-cap-$cap.log 2>&1 < /dev/null & )
until curl -s -m 3 -o /dev/null "http://127.0.0.1:$PORT/coarse?i1=0&j1=0&i2=2&j2=2" 2>/dev/null; do sleep 5; done
# ⚠ pgrep -f MATCHES THE `bash -c` WRAPPER TOO, and picking it gives a pid
# that exits immediately — /proc/<pid>/stat then vanishes mid-measurement.
# Select by comm so only the real interpreter is considered.
SPID=""
for cand in $(pgrep -f "inference.api.*$PORT"); do
case "$(cat /proc/$cand/comm 2>/dev/null)" in
python*) SPID=$cand ;;
esac
done
[ -n "$SPID" ] || { echo "FATAL: no python service pid for port $PORT" >&2; exit 1; }
for r in $(seq 1 "$ROUNDS"); do
c=$(run_bench); echo "$cap,$r,clean,$c," >> "$OUT"
tx=$((7000 + r * 31))
curl -s -m 900 -o /dev/null \
"http://127.0.0.1:$PORT/terrain?i1=${tx}00&j1=0&i2=${tx}01&j2=256&scale=1" &
CURL=$!
sleep 8
gc=$(cores_of "$SPID" 6)
l=$(run_bench); echo "$cap,$r,loaded,$l,$gc" >> "$OUT"
printf " round %d clean %5s loaded %5s gen %5s cores\n" "$r" "$c" "$l" "$gc"
kill "$CURL" 2>/dev/null; wait "$CURL" 2>/dev/null
done
kill "$SPID" 2>/dev/null; sleep 4
done
echo
python3 - "$OUT" <<'PY'
import sys, csv, statistics as st
rows=list(csv.DictReader(open(sys.argv[1])))
caps=sorted({int(r["cap"]) for r in rows})
print(f"{'cap':>5} {'gen cores':>10} {'clean':>8} {'loaded':>8} {'mesher drop':>12}")
prev=None
for c in caps:
cl=[float(r["chunks_per_s"]) for r in rows if int(r["cap"])==c and r["phase"]=="clean" and r["chunks_per_s"]]
ld=[float(r["chunks_per_s"]) for r in rows if int(r["cap"])==c and r["phase"]=="loaded" and r["chunks_per_s"]]
gc=[float(r["gen_cores"]) for r in rows if int(r["cap"])==c and r["gen_cores"]]
if len(cl)<2 or len(ld)<2: continue
mc,ml=st.median(cl),st.median(ld)
print(f"{c:>5} {st.median(gc) if gc else 0:>10.2f} {mc:>8.0f} {ml:>8.0f} "
f"{100*(mc-ml)/mc:>11.1f}%")
print("\nThe budget is the largest cap whose mesher drop stays inside the clean-run")
print("spread. Below that the generator is free; above it, terrain arrives late.")
PY