181 lines
6.3 KiB
Bash
181 lines
6.3 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# scripts/bench-ml.sh — behavioural benchmark for the NISPS control mapping.
|
||
|
|
#
|
||
|
|
# Sibling to bench-engines.sh, and deliberately the same shape: it builds
|
||
|
|
# tests/cpp/ml_bench.cpp twice from ONE source (native via CMake, WASM via
|
||
|
|
# emcc with the flags build-wasm.sh uses), runs both, and reports.
|
||
|
|
#
|
||
|
|
# NOTHING HERE FAILS. Behaviour is not a threshold — a "cliff index of 4.9" is
|
||
|
|
# neither pass nor fail, it is a description. Regressions are noticed by
|
||
|
|
# running with --compare against a previous report, which prints per-metric Δ.
|
||
|
|
# Invariants that genuinely must hold are ctest assertions in
|
||
|
|
# tests/cpp/test_ml_behaviour.cpp, not here.
|
||
|
|
#
|
||
|
|
# WHY BOTH TARGETS
|
||
|
|
# ----------------
|
||
|
|
# Firmware and browser must feel the same. Native-vs-WASM here is a BEHAVIOURAL
|
||
|
|
# comparison, which parity-check.sh's 1e-5 bit-equivalence does not give you:
|
||
|
|
# parity proves two builds of the SAME commit agree, and says nothing about
|
||
|
|
# whether a mapping is playable or whether a gesture does anything.
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# scripts/bench-ml.sh # native + wasm
|
||
|
|
# scripts/bench-ml.sh --native-only
|
||
|
|
# scripts/bench-ml.sh --smoke # fast, proves it runs
|
||
|
|
# scripts/bench-ml.sh --shape 2,16,16,16,8 # any net shape
|
||
|
|
# scripts/bench-ml.sh --sweep-shape # the architecture sweep
|
||
|
|
# scripts/bench-ml.sh --scenario A4_negative_once
|
||
|
|
# scripts/bench-ml.sh --compare old.json
|
||
|
|
# scripts/bench-ml.sh --out bench-ml-2026-07-25.json
|
||
|
|
#
|
||
|
|
# Env:
|
||
|
|
# NISPS_BUILD_DIR default nisps/build
|
||
|
|
# EMCC emcc path (same convention as build-wasm.sh)
|
||
|
|
#
|
||
|
|
# Exit codes: 0 on a completed run, 2 on bad args/missing artifacts, 3 on build
|
||
|
|
# failure.
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||
|
|
BUILD_DIR="${NISPS_BUILD_DIR:-$ROOT/nisps/build}"
|
||
|
|
BENCH_DIR="$BUILD_DIR/bench-ml"
|
||
|
|
NATIVE_BIN="$BUILD_DIR/nisps_ml_bench"
|
||
|
|
SRC="$ROOT/tests/cpp/ml_bench.cpp"
|
||
|
|
REPORT="$ROOT/tests/cpp/ml_bench_report.mjs"
|
||
|
|
EMCC="${EMCC:-emcc}"
|
||
|
|
|
||
|
|
NATIVE_ONLY=0
|
||
|
|
SMOKE=""
|
||
|
|
SHAPE=""
|
||
|
|
SCENARIO=""
|
||
|
|
COMPARE=""
|
||
|
|
OUT=""
|
||
|
|
SWEEP_SHAPE=0
|
||
|
|
SEED=""
|
||
|
|
|
||
|
|
while [[ $# -gt 0 ]]; do
|
||
|
|
case "$1" in
|
||
|
|
--native-only) NATIVE_ONLY=1; shift ;;
|
||
|
|
--smoke) SMOKE="--smoke"; shift ;;
|
||
|
|
--shape) SHAPE="$2"; shift 2 ;;
|
||
|
|
--seed) SEED="$2"; shift 2 ;;
|
||
|
|
--scenario) SCENARIO="$2"; shift 2 ;;
|
||
|
|
--compare) COMPARE="$2"; shift 2 ;;
|
||
|
|
--out) OUT="$2"; shift 2 ;;
|
||
|
|
--sweep-shape) SWEEP_SHAPE=1; shift ;;
|
||
|
|
-h|--help) sed -n '2,40p' "$0"; exit 0 ;;
|
||
|
|
*) echo "bench-ml: unknown arg '$1'" >&2; exit 2 ;;
|
||
|
|
esac
|
||
|
|
done
|
||
|
|
|
||
|
|
mkdir -p "$BENCH_DIR"
|
||
|
|
|
||
|
|
bench_args() {
|
||
|
|
local a=()
|
||
|
|
[[ -n "$SMOKE" ]] && a+=("$SMOKE")
|
||
|
|
[[ -n "$SHAPE" ]] && a+=(--shape "$SHAPE")
|
||
|
|
[[ -n "$SEED" ]] && a+=(--seed "$SEED")
|
||
|
|
[[ -n "$SCENARIO" ]] && a+=(--scenario "$SCENARIO")
|
||
|
|
printf '%s\n' "${a[@]:-}"
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# native
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
echo "==> building native ml_bench" >&2
|
||
|
|
if ! cmake -S "$ROOT/nisps" -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1; then
|
||
|
|
echo "bench-ml: cmake configure failed" >&2; exit 3
|
||
|
|
fi
|
||
|
|
if ! cmake --build "$BUILD_DIR" --target nisps_ml_bench >/dev/null 2>&1; then
|
||
|
|
echo "bench-ml: native build failed" >&2; exit 3
|
||
|
|
fi
|
||
|
|
|
||
|
|
mapfile -t ARGS < <(bench_args)
|
||
|
|
NATIVE_JSON="$BENCH_DIR/native.json"
|
||
|
|
if [[ ${#ARGS[@]} -gt 0 && -n "${ARGS[0]}" ]]; then
|
||
|
|
"$NATIVE_BIN" "${ARGS[@]}" > "$NATIVE_JSON"
|
||
|
|
else
|
||
|
|
"$NATIVE_BIN" > "$NATIVE_JSON"
|
||
|
|
fi
|
||
|
|
echo "==> native report: $NATIVE_JSON" >&2
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# wasm — one source, second compiler. Same flags build-wasm.sh uses for the
|
||
|
|
# shipped module, so the comparison is against what actually ships.
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
WASM_JSON=""
|
||
|
|
if [[ $NATIVE_ONLY -eq 0 ]]; then
|
||
|
|
if ! command -v "$EMCC" >/dev/null 2>&1; then
|
||
|
|
echo "bench-ml: emcc not found; skipping wasm (use --native-only to silence)" >&2
|
||
|
|
else
|
||
|
|
echo "==> building wasm ml_bench" >&2
|
||
|
|
WASM_JS="$BENCH_DIR/ml_bench.mjs"
|
||
|
|
if ! "$EMCC" "$SRC" -o "$WASM_JS" \
|
||
|
|
-std=gnu++20 -O3 \
|
||
|
|
-s ENVIRONMENT=node -s EXIT_RUNTIME=1 -s ALLOW_MEMORY_GROWTH=1 \
|
||
|
|
-s SINGLE_FILE=1 >/dev/null 2>&1; then
|
||
|
|
echo "bench-ml: wasm build failed" >&2; exit 3
|
||
|
|
fi
|
||
|
|
WASM_JSON="$BENCH_DIR/wasm.json"
|
||
|
|
if [[ ${#ARGS[@]} -gt 0 && -n "${ARGS[0]}" ]]; then
|
||
|
|
node "$WASM_JS" "${ARGS[@]}" > "$WASM_JSON"
|
||
|
|
else
|
||
|
|
node "$WASM_JS" > "$WASM_JSON"
|
||
|
|
fi
|
||
|
|
echo "==> wasm report: $WASM_JSON" >&2
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# architecture sweep — the "how does net shape change the UX" instrument.
|
||
|
|
# Runs the corpus at a ladder of shapes and emits one combined report.
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
if [[ $SWEEP_SHAPE -eq 1 ]]; then
|
||
|
|
SWEEP_JSON="$BENCH_DIR/sweep-shape.json"
|
||
|
|
echo "==> shape sweep" >&2
|
||
|
|
{
|
||
|
|
echo "["
|
||
|
|
first=1
|
||
|
|
for s in \
|
||
|
|
"2,4,4,4,8" \
|
||
|
|
"2,8,8,8,8" \
|
||
|
|
"2,16,16,16,8" \
|
||
|
|
"2,32,32,32,8" \
|
||
|
|
"2,64,64,64,8" \
|
||
|
|
"1,16,16,16,8" \
|
||
|
|
"4,16,16,16,8" \
|
||
|
|
"8,16,16,16,8" \
|
||
|
|
"32,16,16,16,8" \
|
||
|
|
"2,16,16,16,1" \
|
||
|
|
"2,16,16,16,4" \
|
||
|
|
"2,16,16,16,16" \
|
||
|
|
"2,16,16,16,33"
|
||
|
|
do
|
||
|
|
[[ $first -eq 0 ]] && echo ","
|
||
|
|
first=0
|
||
|
|
"$NATIVE_BIN" --shape "$s" ${SMOKE:+$SMOKE}
|
||
|
|
done
|
||
|
|
echo "]"
|
||
|
|
} > "$SWEEP_JSON"
|
||
|
|
echo "==> sweep report: $SWEEP_JSON" >&2
|
||
|
|
fi
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
# format + diff
|
||
|
|
# ---------------------------------------------------------------------------
|
||
|
|
if [[ -f "$REPORT" ]] && command -v node >/dev/null 2>&1; then
|
||
|
|
if [[ -n "$COMPARE" ]]; then
|
||
|
|
node "$REPORT" "$NATIVE_JSON" ${WASM_JSON:+"$WASM_JSON"} --compare "$COMPARE"
|
||
|
|
else
|
||
|
|
node "$REPORT" "$NATIVE_JSON" ${WASM_JSON:+"$WASM_JSON"}
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
echo "(no formatter; raw JSON is in $BENCH_DIR)" >&2
|
||
|
|
fi
|
||
|
|
|
||
|
|
if [[ -n "$OUT" ]]; then
|
||
|
|
command cp -f "$NATIVE_JSON" "$OUT"
|
||
|
|
echo "==> saved $OUT" >&2
|
||
|
|
fi
|