mirror of https://github.com/maderix/ANE.git
376 lines
12 KiB
Bash
Executable File
376 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
# run_community_benchmark.sh -- Standardized ANE benchmark for community submissions
|
|
#
|
|
# Runs a focused set of benchmarks and outputs a single JSON file that can be
|
|
# submitted to the community_benchmarks/ directory via PR or GitHub issue.
|
|
#
|
|
# Usage:
|
|
# bash scripts/run_community_benchmark.sh [--steps N] [--skip-training]
|
|
#
|
|
# Output:
|
|
# community_benchmarks/<chip>_<date>.json
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
TRAINING_DIR="$ROOT_DIR/training"
|
|
|
|
STEPS=20
|
|
SKIP_TRAINING=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--steps) STEPS="$2"; shift 2 ;;
|
|
--skip-training) SKIP_TRAINING=true; shift ;;
|
|
--help|-h)
|
|
echo "Usage: bash scripts/run_community_benchmark.sh [--steps N] [--skip-training]"
|
|
echo " --steps N Training steps (default: 20)"
|
|
echo " --skip-training Skip training benchmarks (useful if no training data)"
|
|
exit 0 ;;
|
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# ── Collect system info ──
|
|
|
|
CHIP=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "unknown")
|
|
MACHINE=$(sysctl -n hw.model 2>/dev/null || echo "unknown")
|
|
MACOS_VER=$(sw_vers -productVersion 2>/dev/null || echo "unknown")
|
|
MACOS_BUILD=$(sw_vers -buildVersion 2>/dev/null || echo "unknown")
|
|
NCPU=$(sysctl -n hw.ncpu 2>/dev/null || echo "0")
|
|
MEM_BYTES=$(sysctl -n hw.memsize 2>/dev/null || echo "0")
|
|
MEM_GB=$(echo "scale=0; $MEM_BYTES / 1073741824" | bc 2>/dev/null || echo "0")
|
|
NEURAL_CORES=$(sysctl -n hw.optional.ane.num_cores 2>/dev/null || echo "unknown")
|
|
DATE_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
DATE_SHORT=$(date +"%Y%m%d")
|
|
|
|
CHIP_SLUG=$(echo "$CHIP" | tr ' ' '_' | tr -d '()' | tr '[:upper:]' '[:lower:]')
|
|
|
|
echo "=== ANE Community Benchmark ==="
|
|
echo "Chip: $CHIP"
|
|
echo "Machine: $MACHINE"
|
|
echo "macOS: $MACOS_VER ($MACOS_BUILD)"
|
|
echo "Memory: ${MEM_GB} GB"
|
|
echo "CPUs: $NCPU"
|
|
echo "ANE cores: $NEURAL_CORES"
|
|
echo ""
|
|
|
|
# ── Prerequisites ──
|
|
|
|
if [[ "$(uname)" != "Darwin" ]]; then
|
|
echo "ERROR: macOS required"; exit 1
|
|
fi
|
|
if ! sysctl -n hw.optional.arm64 2>/dev/null | grep -q 1; then
|
|
echo "ERROR: Apple Silicon required"; exit 1
|
|
fi
|
|
if ! xcrun --find clang >/dev/null 2>&1; then
|
|
echo "ERROR: Xcode CLI tools required. Run: xcode-select --install"; exit 1
|
|
fi
|
|
|
|
CC="xcrun clang"
|
|
CFLAGS="-O2 -fobjc-arc -fstack-protector-strong -framework Foundation -framework CoreML -framework IOSurface -ldl"
|
|
|
|
# ── Ask for GitHub username (optional) ──
|
|
|
|
echo "Enter your GitHub username (optional, press Enter to skip):"
|
|
read -r GH_USERNAME
|
|
GH_USERNAME=$(echo "$GH_USERNAME" | tr -d '[:space:]' | sed 's/[^a-zA-Z0-9_-]//g' | cut -c1-39)
|
|
|
|
if [[ -n "$GH_USERNAME" ]]; then
|
|
echo "Username: $GH_USERNAME"
|
|
else
|
|
echo "Submitting anonymously"
|
|
fi
|
|
echo ""
|
|
|
|
# ── Temp file for collecting JSON fragments ──
|
|
|
|
TMPJSON=$(mktemp /tmp/ane_bench_XXXXXX.json)
|
|
trap "rm -f $TMPJSON" EXIT
|
|
|
|
# Start building the JSON result
|
|
USERNAME_LINE=""
|
|
if [[ -n "$GH_USERNAME" ]]; then
|
|
USERNAME_LINE="\"username\": \"$GH_USERNAME\","
|
|
fi
|
|
|
|
cat > "$TMPJSON" << HEADER
|
|
{
|
|
"schema_version": 1,
|
|
$USERNAME_LINE
|
|
"timestamp": "$DATE_ISO",
|
|
"system": {
|
|
"chip": "$CHIP",
|
|
"machine": "$MACHINE",
|
|
"macos_version": "$MACOS_VER",
|
|
"macos_build": "$MACOS_BUILD",
|
|
"cpu_cores": $NCPU,
|
|
"memory_gb": $MEM_GB,
|
|
"neural_engine_cores": "$NEURAL_CORES"
|
|
},
|
|
HEADER
|
|
|
|
# ── 1. SRAM Probe ──
|
|
|
|
echo "--- Running sram_probe ---"
|
|
SRAM_JSON="[]"
|
|
|
|
# Generate mlpackage models if needed
|
|
if ! ls /tmp/ane_sram_*ch_*sp.mlpackage >/dev/null 2>&1; then
|
|
echo " Generating mlpackage models..."
|
|
VENV_PYTHON=""
|
|
if [[ -x /tmp/ane_venv/bin/python3 ]]; then
|
|
VENV_PYTHON="/tmp/ane_venv/bin/python3"
|
|
else
|
|
for pyver in 3.12 3.13 3.11; do
|
|
PY="/opt/homebrew/opt/python@${pyver}/bin/python${pyver}"
|
|
if [[ -x "$PY" ]]; then
|
|
"$PY" -m venv /tmp/ane_venv && /tmp/ane_venv/bin/pip install -q coremltools numpy 2>/dev/null
|
|
VENV_PYTHON="/tmp/ane_venv/bin/python3"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
if [[ -n "$VENV_PYTHON" ]]; then
|
|
"$VENV_PYTHON" "$SCRIPT_DIR/gen_mlpackages.py" 2>/dev/null && echo " mlpackage models generated" || echo " WARNING: mlpackage generation failed"
|
|
fi
|
|
fi
|
|
|
|
if ls /tmp/ane_sram_*ch_*sp.mlpackage >/dev/null 2>&1; then
|
|
cd "$ROOT_DIR"
|
|
$CC $CFLAGS -o sram_probe sram_probe.m 2>/dev/null
|
|
|
|
SRAM_OUTPUT=$(./sram_probe 2>&1) || true
|
|
echo " sram_probe complete"
|
|
|
|
SRAM_JSON=$(echo "$SRAM_OUTPUT" | python3 -c "
|
|
import sys, json, re
|
|
results = []
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
m = re.match(r'\s*(\d+)\s+ch\s+([\d.]+)\s+([\d.]+)\s+ms\s+([\d.]+)\s+([\d.]+)', line)
|
|
if m:
|
|
results.append({
|
|
'channels': int(m.group(1)),
|
|
'weight_mb': float(m.group(2)),
|
|
'ms_per_eval': float(m.group(3)),
|
|
'tflops': float(m.group(4)),
|
|
'gflops_per_mb': float(m.group(5))
|
|
})
|
|
print(json.dumps(results))
|
|
" 2>/dev/null || echo "[]")
|
|
else
|
|
echo " SKIPPED: no mlpackage models"
|
|
fi
|
|
|
|
# ── 2. InMem Peak ──
|
|
|
|
echo "--- Running inmem_peak ---"
|
|
PEAK_JSON="[]"
|
|
|
|
cd "$ROOT_DIR"
|
|
$CC $CFLAGS -o inmem_peak inmem_peak.m 2>/dev/null
|
|
|
|
PEAK_OUTPUT=$(./inmem_peak 2>&1) || true
|
|
echo " inmem_peak complete"
|
|
|
|
PEAK_JSON=$(echo "$PEAK_OUTPUT" | python3 -c "
|
|
import sys, json, re
|
|
results = []
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
m = re.match(r'(\d+)x\s+conv\s+(\d+)ch\s+sp(\d+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+ms\s+([\d.]+)', line)
|
|
if m:
|
|
results.append({
|
|
'depth': int(m.group(1)),
|
|
'channels': int(m.group(2)),
|
|
'spatial': int(m.group(3)),
|
|
'weight_mb': float(m.group(4)),
|
|
'gflops': float(m.group(5)),
|
|
'ms_per_eval': float(m.group(6)),
|
|
'tflops': float(m.group(7))
|
|
})
|
|
print(json.dumps(results))
|
|
" 2>/dev/null || echo "[]")
|
|
|
|
# ── 3. Training (optional) ──
|
|
|
|
echo "--- Running training benchmark ($STEPS steps) ---"
|
|
TRAIN_CPU_JSON="{}"
|
|
TRAIN_ANE_JSON="{}"
|
|
|
|
if ! $SKIP_TRAINING; then
|
|
cd "$TRAINING_DIR"
|
|
|
|
# Build training binaries
|
|
make train_large train_large_ane 2>/dev/null || true
|
|
|
|
if [[ -x ./train_large ]]; then
|
|
TRAIN_OUTPUT=$(./train_large --steps "$STEPS" 2>&1) || true
|
|
echo " train_large complete"
|
|
|
|
TRAIN_CPU_JSON=$(echo "$TRAIN_OUTPUT" | python3 -c "
|
|
import sys, json, re
|
|
result = {}
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
if line.startswith('{\"type\":\"perf\"'):
|
|
d = json.loads(line)
|
|
result['ane_tflops'] = d.get('ane_tflops')
|
|
result['ane_util_pct'] = d.get('ane_util_pct')
|
|
m = re.match(r'Avg train:\s+([\d.]+)\s+ms/step', line)
|
|
if m: result['ms_per_step'] = float(m.group(1))
|
|
m = re.match(r'ANE TFLOPS:\s+([\d.]+)', line)
|
|
if m: result['ane_tflops_sustained'] = float(m.group(1))
|
|
m = re.match(r'Total TFLOPS:\s+([\d.]+)', line)
|
|
if m: result['total_tflops'] = float(m.group(1))
|
|
m = re.match(r'ANE utilization:\s+([\d.]+)%', line)
|
|
if m: result['ane_util_pct'] = float(m.group(1))
|
|
m = re.match(r'Compile time:\s+\d+\s+ms\s+\(([\d.]+)%\)', line)
|
|
if m: result['compile_pct'] = float(m.group(1))
|
|
m = re.match(r'Train time:\s+\d+\s+ms\s+\(([\d.]+)%\)', line)
|
|
if m: result['train_pct'] = float(m.group(1))
|
|
print(json.dumps(result))
|
|
" 2>/dev/null || echo "{}")
|
|
fi
|
|
|
|
if [[ -x ./train_large_ane ]]; then
|
|
TRAIN_ANE_OUTPUT=$(./train_large_ane --steps "$STEPS" 2>&1) || true
|
|
echo " train_large_ane complete"
|
|
|
|
TRAIN_ANE_JSON=$(echo "$TRAIN_ANE_OUTPUT" | python3 -c "
|
|
import sys, json, re
|
|
result = {}
|
|
for line in sys.stdin:
|
|
line = line.strip()
|
|
m = re.match(r'Avg train:\s+([\d.]+)\s+ms/step', line)
|
|
if m: result['ms_per_step'] = float(m.group(1))
|
|
m = re.match(r'ANE TFLOPS:\s+([\d.]+)', line)
|
|
if m: result['ane_tflops_sustained'] = float(m.group(1))
|
|
m = re.match(r'Total TFLOPS:\s+([\d.]+)', line)
|
|
if m: result['total_tflops'] = float(m.group(1))
|
|
m = re.match(r'ANE utilization:\s+([\d.]+)%', line)
|
|
if m: result['ane_util_pct'] = float(m.group(1))
|
|
m = re.match(r'Compile time:\s+\d+\s+ms\s+\(([\d.]+)%\)', line)
|
|
if m: result['compile_pct'] = float(m.group(1))
|
|
m = re.match(r'Train time:\s+\d+\s+ms\s+\(([\d.]+)%\)', line)
|
|
if m: result['train_pct'] = float(m.group(1))
|
|
print(json.dumps(result))
|
|
" 2>/dev/null || echo "{}")
|
|
fi
|
|
else
|
|
echo " SKIPPED (--skip-training)"
|
|
fi
|
|
|
|
# ── Assemble final JSON ──
|
|
|
|
OUTDIR="$ROOT_DIR/community_benchmarks"
|
|
mkdir -p "$OUTDIR"
|
|
OUTFILE="$OUTDIR/${CHIP_SLUG}_${DATE_SHORT}.json"
|
|
if [[ -f "$OUTFILE" ]]; then
|
|
i=2
|
|
while [[ -f "${OUTFILE%.json}_${i}.json" ]]; do i=$((i+1)); done
|
|
OUTFILE="${OUTFILE%.json}_${i}.json"
|
|
fi
|
|
|
|
python3 -c "
|
|
import json, sys
|
|
|
|
with open('$TMPJSON') as f:
|
|
partial = f.read()
|
|
|
|
sram = json.loads('''$SRAM_JSON''')
|
|
peak = json.loads('''$PEAK_JSON''')
|
|
train_cpu = json.loads('''$TRAIN_CPU_JSON''')
|
|
train_ane = json.loads('''$TRAIN_ANE_JSON''')
|
|
|
|
peak_tflops = max((r['tflops'] for r in peak), default=0)
|
|
sram_peak_eff = max((r['gflops_per_mb'] for r in sram), default=0)
|
|
sram_spill_ch = 0
|
|
prev_tflops = 0
|
|
for r in sorted(sram, key=lambda x: x['channels']):
|
|
if prev_tflops > 0 and r['tflops'] < prev_tflops * 0.6:
|
|
sram_spill_ch = r['channels']
|
|
break
|
|
prev_tflops = max(prev_tflops, r['tflops'])
|
|
|
|
result = json.loads(partial + '\"_\": 0}')
|
|
del result['_']
|
|
|
|
result['benchmarks'] = {
|
|
'sram_probe': sram,
|
|
'inmem_peak': peak,
|
|
'training_cpu_classifier': train_cpu,
|
|
'training_ane_classifier': train_ane
|
|
}
|
|
|
|
result['summary'] = {
|
|
'peak_tflops': round(peak_tflops, 2),
|
|
'sram_peak_efficiency_gflops_per_mb': round(sram_peak_eff, 1),
|
|
'sram_spill_start_channels': sram_spill_ch,
|
|
'training_ms_per_step_cpu': train_cpu.get('ms_per_step'),
|
|
'training_ms_per_step_ane': train_ane.get('ms_per_step'),
|
|
'training_ane_tflops': train_ane.get('ane_tflops_sustained') or train_cpu.get('ane_tflops_sustained'),
|
|
'training_ane_util_pct': train_ane.get('ane_util_pct') or train_cpu.get('ane_util_pct')
|
|
}
|
|
|
|
with open('$OUTFILE', 'w') as f:
|
|
json.dump(result, f, indent=2)
|
|
f.write('\n')
|
|
|
|
print(json.dumps(result['summary'], indent=2))
|
|
"
|
|
|
|
echo ""
|
|
echo "=== Benchmark complete ==="
|
|
echo "Results saved to: $OUTFILE"
|
|
echo ""
|
|
|
|
# ── Optional: submit to community database ──
|
|
|
|
DASHBOARD_URL="${ANE_DASHBOARD_URL:-https://web-lac-sigma-61.vercel.app}"
|
|
SUBMIT_URL="$DASHBOARD_URL/api/submit"
|
|
|
|
echo "Would you like to submit your results to the ANE community benchmark database? (y/N)"
|
|
read -r SUBMIT_ANSWER
|
|
|
|
if [[ "$SUBMIT_ANSWER" =~ ^[Yy]$ ]]; then
|
|
echo "Submitting to $SUBMIT_URL ..."
|
|
|
|
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
-X POST "$SUBMIT_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d @"$OUTFILE" 2>/dev/null) || true
|
|
|
|
HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
|
|
HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -1)
|
|
|
|
case "$HTTP_CODE" in
|
|
201)
|
|
SUBMIT_ID=$(echo "$HTTP_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || echo "")
|
|
echo "Submitted successfully! (ID: $SUBMIT_ID)"
|
|
echo "View results at: $DASHBOARD_URL"
|
|
;;
|
|
409)
|
|
echo "Already submitted (duplicate detected within the last hour)."
|
|
echo "View results at: $DASHBOARD_URL"
|
|
;;
|
|
429)
|
|
echo "Rate limited -- too many submissions. Try again later."
|
|
echo "You can also submit via GitHub PR instead (see below)."
|
|
;;
|
|
*)
|
|
echo "Submission failed (HTTP $HTTP_CODE). You can submit manually instead."
|
|
;;
|
|
esac
|
|
echo ""
|
|
fi
|
|
|
|
echo "Alternative submission methods:"
|
|
echo " 1. Fork https://github.com/maderix/ANE"
|
|
echo " 2. Add $OUTFILE to your fork"
|
|
echo " 3. Open a Pull Request"
|
|
echo ""
|
|
echo "Or paste the contents of $OUTFILE in a GitHub issue."
|