#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 David (Corey) Herrera # # run_guarded.sh — run a model-loading test without risking the machine. # # On 2026-08-08 an unguarded run of tests/test_qwen35moe_metal_forward against the # 15 GB Q3_K_M GGUF kernel-panicked this Mac: 38 GB of page-ins on a 24 GB box whose # disk was 96% full, so the compressor could not spill to swap, reclaim returned zero # pages, watchdogd starved for 90 s and AppleARMWatchdogTimer fired. The whole system # went down and took five agents' uncommitted work with it. # # The failure is not gradual. Once free RAM and swap headroom are both gone the box is # already unrecoverable, so this script watches from OUTSIDE the process and kills the # child while the machine can still recover. # # Usage: metal/run_guarded.sh ./tests/test_qwen35moe_metal_forward [args...] # # Env: # GUARD_MIN_DISK_GB refuse to start below this much free disk (default 25) # GUARD_MIN_FREE_GB refuse to start below free+inactive+purgeable RAM (default 12) # GUARD_KILL_FREE_MB kill the child below this much reclaimable RAM (default 900) # GUARD_KILL_SWAP_MB kill the child once swap USED exceeds this (default 4096) # GUARD_POLL_SEC sample interval (default 2) # GUARD_TIMEOUT_SEC hard wall-clock cap on the child (default 1800) # set -uo pipefail MIN_DISK_GB=${GUARD_MIN_DISK_GB:-25} MIN_FREE_GB=${GUARD_MIN_FREE_GB:-12} KILL_FREE_MB=${GUARD_KILL_FREE_MB:-900} KILL_SWAP_MB=${GUARD_KILL_SWAP_MB:-4096} POLL_SEC=${GUARD_POLL_SEC:-2} TIMEOUT_SEC=${GUARD_TIMEOUT_SEC:-1800} if [ $# -lt 1 ]; then echo "usage: $0 [args...]" >&2 exit 2 fi page_size=$(sysctl -n hw.pagesize) # Reclaimable RAM in MiB: free + inactive + purgeable + speculative. Inactive pages are # mostly clean file cache, which the kernel can drop for free; excluding them would make # a healthy machine look starved. reclaimable_mb() { vm_stat | awk -v ps="$page_size" ' /Pages free/ { f = $3 } /Pages inactive/ { i = $3 } /Pages purgeable/ { p = $3 } /Pages speculative/ { s = $3 } END { gsub(/\./, "", f); gsub(/\./, "", i); gsub(/\./, "", p); gsub(/\./, "", s); printf "%d", (f + i + p + s) * ps / 1048576 }' } # "total = 0.00M used = 1.50G free = ..." -> MiB. The unit suffix must be read # BEFORE stripping it; getting this backwards silently under-reports GiB by 1024x # and the swap cap then never fires. swap_used_mb() { sysctl -n vm.swapusage | awk '{ for (i = 1; i <= NF; i++) if ($i == "used") { v = $(i + 2) unit = substr(v, length(v), 1) gsub(/[A-Za-z]/, "", v) if (unit == "G") printf "%d", v * 1024 else if (unit == "K") printf "%d", v / 1024 else printf "%d", v exit } }' } free_disk_gb() { df -g /System/Volumes/Data | awk 'NR == 2 { print $4 }' } # ---------------------------------------------------------------- preflight disk_gb=$(free_disk_gb) free_mb=$(reclaimable_mb) free_gb=$((free_mb / 1024)) echo "guard: preflight — ${free_gb} GiB reclaimable RAM, ${disk_gb} GiB free disk" if [ "$disk_gb" -lt "$MIN_DISK_GB" ]; then cat >&2 <&2 </dev/null; } trap cleanup INT TERM killed="" elapsed=0 peak_rss_mb=0 while kill -0 "$child" 2>/dev/null; do sleep "$POLL_SEC" elapsed=$((elapsed + POLL_SEC)) free_mb=$(reclaimable_mb) swap_mb=$(swap_used_mb) swap_mb=${swap_mb:-0} rss_mb=$(ps -o rss= -p "$child" 2>/dev/null | awk '{ printf "%d", $1 / 1024 }') rss_mb=${rss_mb:-0} [ "$rss_mb" -gt "$peak_rss_mb" ] && peak_rss_mb=$rss_mb if [ "$free_mb" -lt "$KILL_FREE_MB" ]; then killed="reclaimable RAM fell to ${free_mb} MiB (floor ${KILL_FREE_MB} MiB)" elif [ "$swap_mb" -gt "$KILL_SWAP_MB" ]; then killed="swap in use hit ${swap_mb} MiB (cap ${KILL_SWAP_MB} MiB)" elif [ "$elapsed" -ge "$TIMEOUT_SEC" ]; then killed="wall-clock timeout after ${elapsed}s" fi if [ -n "$killed" ]; then echo "" >&2 echo "guard: KILLING pid $child — $killed" >&2 case "$killed" in wall-clock*) ;; *) echo "guard: the machine was heading for the 2026-08-08 panic; the test died instead." >&2 ;; esac kill -KILL "$child" 2>/dev/null wait "$child" 2>/dev/null echo "guard: peak child RSS ${peak_rss_mb} MiB over ${elapsed}s" >&2 exit 137 fi done wait "$child" status=$? echo "guard: child exited ${status}; peak RSS ${peak_rss_mb} MiB over ${elapsed}s" exit "$status"