OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scripts / generator-budget-sweep.sh

106 lines · bash ref:
#!/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