gem/scripts/run-bq8-bk128-gate.sh
2026-08-30 18:24:55 -06:00

177 lines
8 KiB
Bash
Executable file

#!/bin/sh
# Quiet-host, one-axis BQ8/BK64 versus BQ8/BK128 operator gate.
set -eu
[ "$#" -eq 1 ] || {
echo "usage: $0 OUTPUT_DIRECTORY" >&2
exit 2
}
output=$1
build_dir=${GEM_BQ8_GATE_BUILD:-build}
profile="$build_dir/gem-long-attention-profile"
counter_info="$build_dir/gem-metal-counter-info"
max_load_per_cpu=${GEM_BQ8_GATE_MAX_LOAD_PER_CPU:-1.0}
max_short_window_busy_cpu=${GEM_BQ8_GATE_MAX_SHORT_WINDOW_BUSY_CPU_PERCENT:-10.0}
repetitions=10
warmup=10
[ ! -e "$output" ] || { echo "gem: output already exists: $output" >&2; exit 2; }
[ -x "$profile" ] || { echo "gem: required profiler is missing: $profile" >&2; exit 2; }
[ -x "$counter_info" ] || {
echo "gem: required Metal inventory tool is missing: $counter_info" >&2
exit 2
}
case $max_load_per_cpu in
''|*[!0-9.]*) echo "gem: invalid maximum load per CPU" >&2; exit 2 ;;
esac
awk -v maximum="$max_load_per_cpu" 'BEGIN {exit !(maximum > 0)}' || {
echo "gem: maximum load per CPU must be positive" >&2
exit 2
}
awk -v maximum="$max_short_window_busy_cpu" 'BEGIN {exit !(maximum > 0)}' || {
echo "gem: maximum short-window busy CPU must be positive" >&2
exit 2
}
if [ "$(uname -s)" != Darwin ] || ! command -v sysctl >/dev/null 2>&1 ||
! command -v pmset >/dev/null 2>&1 || ! command -v caffeinate >/dev/null 2>&1 ||
! command -v top >/dev/null 2>&1; then
echo "gem: BK128 performance gate requires macOS host-state tools" >&2
exit 2
fi
if command -v pgrep >/dev/null 2>&1 && pgrep -f 'build/gem serve' >/dev/null 2>&1; then
echo "gem: refusing to overlap a production server Metal workload" >&2
exit 1
fi
mkdir -p "$output"
date -u '+%Y-%m-%dT%H:%M:%SZ' > "$output/run-start-utc.txt"
sw_vers > "$output/macos-version.txt"
shasum -a 256 "$profile" "$counter_info" src/metal.m src/long_attention_profile.c \
scripts/run-bq8-bk128-gate.sh scripts/analyze-bq8-bk128-gate.sh \
> "$output/sha256.txt"
otool -L "$profile" "$counter_info" > "$output/native-linkage.txt"
if grep -Eiq 'lib(llama|ggml)' "$output/native-linkage.txt"; then
echo "gem: profiler unexpectedly links llama/ggml" >&2
exit 1
fi
"$counter_info" > "$output/metal-device.json"
jq -e '
.schema_version == 1 and .workload == "metal-counter-info" and
.device.name == "Apple M4 Max" and .device.has_unified_memory == true and
.device.gpu_families.apple9 == true and
.device.max_threadgroup_memory_bytes >= 20544
' "$output/metal-device.json" >/dev/null
jq -n --arg build_dir "$build_dir" --argjson maximum "$max_load_per_cpu" \
--argjson max_short_window_busy_cpu "$max_short_window_busy_cpu" \
--argjson repetitions "$repetitions" --argjson warmup "$warmup" \
'{schema_version: 2, engine: "gem-native-c-metal",
axis: {name: "cache_tile_width", baseline: 64, candidate: 128},
positions: [4064, 65504, 131040], rows: 32, kv_type: "f32",
repetitions: $repetitions, warmup: $warmup, seed_prefix: true,
max_load_per_logical_cpu: $maximum,
max_short_window_busy_cpu_percent: $max_short_window_busy_cpu,
requires_ac_power: true, requires_no_thermal_warning: true,
build_directory: $build_dir}' \
> "$output/gate-config.json"
host_receipt()
{
phase=$1
tokens=$2
destination="$output/host-$phase-$tokens.json"
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
load_average=$(sysctl -n vm.loadavg)
load_1m=$(printf '%s\n' "$load_average" | awk '{print $2}')
logical_cpus=$(sysctl -n hw.logicalcpu)
load_per_cpu=$(awk -v load="$load_1m" -v cpus="$logical_cpus" \
'BEGIN {printf "%.6f", load / cpus}')
recent_process_cpu_percent=$(ps -Ao pcpu= |
awk '{sum += $1} END {printf "%.3f", sum}')
short_window_busy_cpu=$(LC_ALL=C top -l 2 -s 1 -n 0 -stats pid,cpu,command |
awk -F'[:,%]' '/^CPU usage:/ {idle=$6} END {
gsub(/^[[:space:]]+|[[:space:]]+$/, "", idle)
if (idle !~ /^[0-9]+([.][0-9]+)?$/) exit 1
printf "%.3f", 100 - idle
}') || {
echo "gem: could not sample short-window macOS CPU state" >&2
return 1
}
power_state=$(LC_ALL=C pmset -g batt 2>&1 || true)
thermal_state=$(LC_ALL=C pmset -g therm 2>&1 || true)
ac_power=false
case $power_state in
*"Now drawing from 'AC Power'"*) ac_power=true ;;
esac
no_thermal_warning=false
if printf '%s\n' "$thermal_state" |
grep -Fq 'No thermal warning level has been recorded' &&
printf '%s\n' "$thermal_state" |
grep -Fq 'No performance warning level has been recorded'; then
no_thermal_warning=true
elif printf '%s\n' "$thermal_state" |
grep -Eq 'CPU_Scheduler_Limit[[:space:]]*=[[:space:]]*100' &&
printf '%s\n' "$thermal_state" |
grep -Eq 'CPU_Speed_Limit[[:space:]]*=[[:space:]]*100'; then
no_thermal_warning=true
fi
status=pass
awk -v actual="$load_per_cpu" -v maximum="$max_load_per_cpu" \
'BEGIN {exit !(actual <= maximum)}' || status=fail
awk -v actual="$short_window_busy_cpu" -v maximum="$max_short_window_busy_cpu" \
'BEGIN {exit !(actual <= maximum)}' || status=fail
[ "$ac_power" = true ] || status=fail
[ "$no_thermal_warning" = true ] || status=fail
assertions=$(pmset -g assertions 2>&1 || true)
top_processes=$(ps -Ao pid,pcpu,pmem,etime,comm -r | sed -n '1,24p')
jq -n --arg timestamp "$timestamp" --arg phase "$phase" --arg status "$status" \
--arg load_average "$load_average" --argjson load_1m "$load_1m" \
--argjson logical_cpus "$logical_cpus" --argjson load_per_cpu "$load_per_cpu" \
--argjson maximum "$max_load_per_cpu" --arg power_state "$power_state" \
--argjson recent_process_cpu_percent "$recent_process_cpu_percent" \
--argjson short_window_busy_cpu "$short_window_busy_cpu" \
--argjson max_short_window_busy_cpu "$max_short_window_busy_cpu" \
--argjson ac_power "$ac_power" \
--argjson no_thermal_warning "$no_thermal_warning" \
--arg thermal_state "$thermal_state" --arg assertions "$assertions" \
--arg top_processes "$top_processes" \
'{schema_version: 2, timestamp_utc: $timestamp, phase: $phase, status: $status,
load_average: $load_average, load_1m: $load_1m,
logical_cpus: $logical_cpus, load_per_logical_cpu: $load_per_cpu,
max_load_per_logical_cpu: $maximum, power_state: $power_state,
ac_power: $ac_power,
recent_aggregate_process_cpu_percent: $recent_process_cpu_percent,
short_window_busy_cpu_percent: $short_window_busy_cpu,
max_short_window_busy_cpu_percent: $max_short_window_busy_cpu,
thermal_state: $thermal_state, no_thermal_warning: $no_thermal_warning,
assertions: $assertions,
top_processes: $top_processes}' > "$destination"
[ "$status" = pass ]
}
for tokens in 4096 65536 131072; do
position=$((tokens - 32))
if ! host_receipt preflight "$tokens"; then
echo "gem: BK128 gate rejected busy host before $tokens-token screen" >&2
exit 1
fi
temporary="$output/seeded-$tokens.json.tmp"
caffeinate -i "$profile" --shape 31b-full --position "$position" --rows 32 \
--repetitions "$repetitions" --warmup "$warmup" --seed-prefix \
--variant compare-bq8-bk128 > "$temporary"
jq -e --argjson position "$position" --argjson capacity "$tokens" '
.schema_version == 1 and .engine == "gem-native-c-metal" and
.shape == "31b-full" and .kv_type == "f32" and
.position == $position and .rows == 32 and .cache_capacity == $capacity and
.repetitions == 10 and .warmup == 10 and .seed_prefix == true and
(.variants["online-bq8"].samples | length) == 10 and
(.variants["online-bq8-bk128"].samples | length) == 10
' "$temporary" >/dev/null
mv "$temporary" "$output/seeded-$tokens.json"
if ! host_receipt postflight "$tokens"; then
echo "gem: BK128 gate rejected changed host after $tokens-token screen" >&2
exit 1
fi
done
scripts/analyze-bq8-bk128-gate.sh "$output" "$output/decision.json"
jq . "$output/decision.json"