Replaces the never-executed "LiteRT-LM on UNO Q 4GB" spike plan with a measured record of the local-fallback path we actually run. Measured on board 65301572 (Qwen2.5-0.5B-Instruct, -c 8192, 4 threads), using llama-server's own timings rather than wall clock: prefill ~17-20 tok/s (linear), decode ~6-11 tok/s (degrades with KV) warm prefix-cached tool call: 3.8s, 6/6 correct structured calls Two findings that changed the deployment: 1. The board had drifted onto Qwen2.5-Coder-1.5B - larger and tuned for the wrong task. Reverting to the repo's 0.5B made tool calls ~6x faster (24s -> 3.8s) and freed ~700MB. The repo was right. 2. The harness, not the model, was the bottleneck. The default agent profile sent a 4718-token prompt (~4.6 min prefill) and the client cancelled before the model could answer. A lean runtime profile cuts that to 706 tokens, lifts prefix-cache match 0.435 -> 0.966, and completes a full agentic turn with a real tool call in 11s warm. The ZeroClaw text parser was never at fault. Prompt cost model for budgeting profiles: ~706 base (1 tool), ~244/additional tool, +315 for uno_q_flash (schema + flash imperative), ~53/skill in compact mode. Also standardises context on -c 8192 across all three provisioning paths (a 16k window costs ~16 min to fill at this speed and doubles KV for nothing), and fixes stale references to the deleted src/lib/harness.ts. Adds bench-prefill.sh and bench-tools.py as reproducible baselines. Co-Authored-By: Claude Opus 4.8 <[email protected]>
87 lines
3.4 KiB
Bash
Executable File
87 lines
3.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# zeroclaw-supervisor.sh — no-root service watchdog for an APESS Uno Q node.
|
||
#
|
||
# Keeps llama-server (:8083) and the zeroclaw daemon (:8080) alive by polling
|
||
# their /health ENDPOINTS — a wedged process passes `pgrep` but fails here — and
|
||
# restarting whatever is down. This is the fallback for boards where systemd
|
||
# isn't usable (expired account / no root / no user session bus, which is the
|
||
# case on the workshop dev board). On a properly-imaged board with root, prefer
|
||
# the systemd units in ./systemd/ instead.
|
||
#
|
||
# Children are started with `setsid` so they survive the shell that launched the
|
||
# supervisor closing — that is exactly the property plain `nohup … &` inside an
|
||
# adb shell does NOT give you, and why services died on the last disconnect.
|
||
#
|
||
# Launch once: setsid nohup /home/arduino/zeroclaw-supervisor.sh >/dev/null 2>&1 < /dev/null &
|
||
# Boot persist: crontab -l | { cat; echo '@reboot /home/arduino/zeroclaw-supervisor.sh'; } | crontab -
|
||
set -u
|
||
|
||
# cron's @reboot env is minimal — guarantee the tools we shell out to are found.
|
||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}
|
||
|
||
HOME_DIR=/home/arduino
|
||
LLAMA_DIR="$HOME_DIR/llama"
|
||
MODEL="$HOME_DIR/models/qwen.gguf"
|
||
ZC="$HOME_DIR/zeroclaw"
|
||
LOG="$HOME_DIR/zc-supervisor.log"
|
||
LOCK="$HOME_DIR/.zc-supervisor.lock"
|
||
INTERVAL="${INTERVAL:-15}"
|
||
LLAMA_WARMUP="${LLAMA_WARMUP:-300}" # cold GGUF load is 3–5 min; don't reap it mid-load
|
||
DAEMON_WARMUP="${DAEMON_WARMUP:-20}"
|
||
|
||
log() { echo "[$(date '+%F %T')] $*" >> "$LOG"; }
|
||
now() { date +%s; }
|
||
healthy() { curl -sf --max-time 4 "http://127.0.0.1:$1/health" >/dev/null 2>&1; }
|
||
|
||
# single-instance guard (no flock dependency). Pid-reuse-safe: a stale lock
|
||
# whose pid was recycled by an unrelated process (common right after a reboot)
|
||
# must NOT block startup — so require the live pid to actually be a supervisor.
|
||
if [ -f "$LOCK" ]; then
|
||
OLDPID=$(cat "$LOCK" 2>/dev/null)
|
||
if [ -n "$OLDPID" ] && kill -0 "$OLDPID" 2>/dev/null \
|
||
&& grep -qa zeroclaw-supervisor "/proc/$OLDPID/cmdline" 2>/dev/null; then
|
||
log "supervisor already running (pid $OLDPID) — exiting"
|
||
exit 0
|
||
fi
|
||
fi
|
||
echo $$ > "$LOCK"
|
||
trap 'rm -f "$LOCK"' EXIT
|
||
|
||
llama_ok_after=0
|
||
daemon_ok_after=0
|
||
|
||
# `setsid sh -c '… exec …'` detaches into a new session AND replaces the wrapper
|
||
# shell with the target — so no stray bash lingers per restart (a plain
|
||
# `( … & )` wrapper leaks one shell each time).
|
||
start_llama() {
|
||
log "starting llama-server :8083"
|
||
setsid sh -c "cd '$LLAMA_DIR' && LD_LIBRARY_PATH='$LLAMA_DIR' exec ./llama-server \
|
||
-m '$MODEL' --host 127.0.0.1 --port 8083 -np 1 -c 8192 --jinja --mlock" \
|
||
>> "$HOME_DIR/llama8083.log" 2>&1 < /dev/null &
|
||
llama_ok_after=$(( $(now) + LLAMA_WARMUP ))
|
||
}
|
||
|
||
start_daemon() {
|
||
log "starting zeroclaw daemon :8080"
|
||
setsid sh -c "cd '$HOME_DIR' && TMPDIR=/tmp exec '$ZC' daemon" \
|
||
>> "$HOME_DIR/zc-daemon.log" 2>&1 < /dev/null &
|
||
daemon_ok_after=$(( $(now) + DAEMON_WARMUP ))
|
||
}
|
||
|
||
log "supervisor up (pid $$, interval ${INTERVAL}s)"
|
||
while true; do
|
||
if ! healthy 8083; then
|
||
if [ "$(now)" -ge "$llama_ok_after" ]; then
|
||
log "llama :8083 unhealthy past grace — restarting"
|
||
pkill -f llama-server 2>/dev/null; sleep 1; start_llama
|
||
fi
|
||
fi
|
||
if ! healthy 8080; then
|
||
if [ "$(now)" -ge "$daemon_ok_after" ]; then
|
||
log "daemon :8080 unhealthy past grace — restarting"
|
||
pkill -f "zeroclaw daemon" 2>/dev/null; sleep 1; start_daemon
|
||
fi
|
||
fi
|
||
sleep "$INTERVAL"
|
||
done
|