Files
apress/deploy/uno-q/zeroclaw-supervisor.sh
T
Omar SobhandClaude Opus 4.8 a23a525aef docs(uno-q): measure on-board inference; retire the LiteRT spike
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]>
2026-07-19 15:39:10 -07:00

87 lines
3.4 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 35 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