Files
serverAppConfigs/tests/frank_zealot_unit.bats
Frank 466fd477d8 feat(frank-zealot): Bonsai-2-9B PQ2_0 with the MTP head at 3 x 262,144
Swap the 3090 endpoint from Ternary-Bonsai-2-27B PQ2_0 (2 x 200,704,
fork build 10754) to Ternary-Bonsai-2-9B PQ2_0 with the MTP head IN the
file, three conversations at the trained 262,144 window
(--ctx-size 786432 --parallel 3), on vendor fork build 10777 -- the
minimum build that pairs MTP with a quantized V cache. q8_0 K + q8_0 V
is the only KV that fits: 13,056 MiB against a 26,112 MiB f16 pair.
Settled boot 17,876 used / 6,249 free.

MTP is armed and then DECLINED by measurement: the draft context bills
its own f16 KV over the whole pool (3,072 MiB) and at UBATCH_SIZE=2048
that halves decode (167.2 -> 109.2 t/s). The documented MTP arm
(SPEC_TYPE=draft-mtp + UBATCH_SIZE=1024, 213.5 t/s) is recorded in the
env header rather than left to rediscovery.

Two bugs the trial caught: ${SPEC_PRESENCE_PENALTY:=0.0} was a
default-assignment, so the env value always won and the no-presence-penalty
MTP arm was unreachable (now ${VAR=0.0}); and the Q8_0 mmproj is the
publisher's requirement for concurrent image requests, which matters at
--parallel 3.

contextWindow is now 262144 -- the widest in the fleet -- so :7771 is the
compaction hub. 27B runbook re-banned RETIRED with its bench-window
section closed; indexes, homepage card, OpenWebUI doc and the worklog
re-keyed.
2026-10-18 13:15:18 -07:00

362 lines
16 KiB
Bash
Executable File
Raw Permalink 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.
# Seam: the frank-zealot llama.cpp root unit on dankclaw.
# frank-zealot is the RTX 3090 endpoint; since 2026-10-18 it serves
# prism-ml/Ternary-Bonsai-2-9B-gguf at PQ2_0 — the ternary Bonsai-2-9B, a
# qwen35 hybrid with 32 layers (8 full-attention + 24 Gated DeltaNet) and the
# MTP draft head INSIDE the file — via the PrismML fork build of llama.cpp
# (vendor build 10777, the minimum that pairs MTP with a quantized V cache),
# the ONLY loader that registers the payload's custom tensor type 142 and
# honours its prism.hadamard.* metadata: THREE 262,144-token conversations
# (--ctx-size 786432 --parallel 3), q8_0 K + q8_0 V under flash attention (f16
# KV would bill 26,112 MiB of a 24,123 MiB card), the publisher's own Q8_0
# vision projector (the BF16 one can crash on concurrent images), and MTP OFF
# — measured, not because the head is missing: the draft context bills f16 KV
# over the whole pool and halves decode at this shape. Log verbosity 4 ships so
# the VRAM-fit forensics land in the journal.
# This seam runs the serve script against a stub engine and asserts on the
# argv it actually execs, so what is pinned is the command the operator gets.
load helpers
UNIT="$REPO_ROOT/scripts/frank-zealot.service"
SERVE="$REPO_ROOT/scripts/serve-frank-zealot.sh"
ENV_FILE="$REPO_ROOT/scripts/frank-zealot.env"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
ENV_LARGE="$REPO_ROOT/scripts/frank-dragoon.env"
setup() {
STAGE="$(mktemp -d)"
RUNTIME_DIR="$STAGE/llama"
MODEL_DIR="$STAGE/models"
mkdir -p "$RUNTIME_DIR/bin" "$MODEL_DIR"
cat > "$RUNTIME_DIR/bin/llama-server" <<'STUB'
#!/usr/bin/env bash
printf '%s\n' "$@"
STUB
chmod +x "$RUNTIME_DIR/bin/llama-server" "$RUNTIME_DIR/bin" 2>/dev/null || chmod +x "$RUNTIME_DIR/bin/llama-server"
# The stub payload files carry the SAME basenames as the shipped env, so
# the seam exercises the real paths, and a payload rename moves one file
# (the env) instead of silently forking the test's filename constants.
MODEL_STUB="$(basename "$(sed -nE 's/^MODEL_FILE=(.+)$/\1/p' "$ENV_FILE")")"
MM_STUB="$(basename "$(sed -nE 's/^MMPROJ_FILE=(.+)$/\1/p' "$ENV_FILE")")"
DRAFT_STUB="draft-assistant.gguf"
touch "$MODEL_DIR/$MODEL_STUB" "$MODEL_DIR/$MM_STUB" "$MODEL_DIR/$DRAFT_STUB"
}
teardown() {
rm -rf "$STAGE"
}
# serve_argv [env-file] — run the serve script and capture the engine's
# argv, one token per line, in $output.
serve_argv() {
local envf="${1:-$STAGE/test.env}"
cat > "$envf" <<EOF
LLAMA_BIN=$RUNTIME_DIR/bin/llama-server
MODEL_FILE=$MODEL_DIR/$MODEL_STUB
MMPROJ_FILE=$MODEL_DIR/$MM_STUB
ALIAS=frank-zealot
BIND_ADDR=0.0.0.0
LISTEN_PORT=7771
CTX_SIZE=$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")
PARALLEL=$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")
BATCH_SIZE=$(sed -nE 's/^BATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")
UBATCH_SIZE=$(sed -nE 's/^UBATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")
CACHE_TYPE_K=$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")
CACHE_TYPE_V=$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")
SPEC_TYPE=$(sed -nE 's/^SPEC_TYPE=(.+)$/\1/p' "$ENV_FILE")
SPEC_DRAFT_N_MAX=$(sed -nE 's/^SPEC_DRAFT_N_MAX=([0-9]+)$/\1/p' "$ENV_FILE")
VERBOSITY=$(sed -nE 's/^VERBOSITY=([0-9]+)$/\1/p' "$ENV_FILE")
METRICS=$(sed -nE 's/^METRICS=([0-9]+)$/\1/p' "$ENV_FILE")
NGL=$(sed -nE 's/^NGL=([0-9]+)$/\1/p' "$ENV_FILE")
EOF
run env FRANK_ZEALOT_ENV_FILE="$envf" nvidia-modprobe=true bash "$SERVE"
}
argv_value() {
grep -A1 -xF -- "$1" <<<"$output" | tail -1
}
# run_stage — exec the CURRENT $STAGE/test.env as-is. serve_argv REWRITES
# that file from the shipped env, so any test that mutates it (drop a line,
# append a drafter) must exec through this, not serve_argv — which silently
# undoes the mutation and makes the assertion a tautology.
run_stage() {
run env FRANK_ZEALOT_ENV_FILE="$STAGE/test.env" nvidia-modprobe=true bash "$SERVE"
}
@test "frank-zealot unit, serve script and env exist in scripts/" {
[ -f "$UNIT" ]
[ -f "$SERVE" ]
[ -f "$ENV_FILE" ]
}
@test "frank-zealot is boot-persistent, self-healing and unprivileged" {
grep -q '^WantedBy=multi-user.target$' "$UNIT"
grep -q '^Restart=' "$UNIT"
grep -q '^RestartSec=' "$UNIT"
grep -q '^User=dank$' "$UNIT"
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-zealot.sh$' "$UNIT"
grep -q 'FRANK_ZEALOT_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-zealot.env' "$UNIT"
}
@test "the shipped env points at the fork runtime and the Bonsai-2 9B MTP pair" {
# The payload path IS the endpoint's identity, and the runtime is part of
# it here: the file's custom tensor type 142 + prism.hadamard.* metadata
# make the PrismML build the only loader, and this build must be >= b10776
# because MTP beside q8_0 V is illegal below it. A retarget that moves the
# model but leaves the old build behind ships an endpoint that cannot load
# (type 142) or cannot run the documented MTP arm (the build gate).
grep -qE '^LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server$' "$ENV_FILE"
grep -qE '^MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0\.gguf$' "$ENV_FILE"
grep -qE '^MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0\.gguf$' "$ENV_FILE"
}
@test "the deployer gates on exactly the payload paths the env file ships" {
# Anti-drift: the deploy handler checks files by its own defaults. If
# those ever differ from what the unit actually loads, a green deploy
# would be gating on a file nobody reads — and a missing projector would
# ship a blind endpoint.
local env_v dep_v path pair
for pair in "MODEL_FILE LLM_ZEALOT_MODEL_FILE" "MMPROJ_FILE LLM_ZEALOT_MMPROJ"; do
env_v="${pair%% *}"
dep_v="${pair##* }"
path="$(sed -nE "s/^${env_v}=(.+)$/\1/p" "$ENV_FILE")"
grep -qF -- "${dep_v}=\"\${${dep_v}:-${path}}\"" "$REMOTE_DEPLOY"
done
}
@test "three conversations share the pool at a 262144 window each" {
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --alias)" = "frank-zealot" ]
[ "$(argv_value --port)" = "7771" ]
[ "$(argv_value --host)" = "0.0.0.0" ]
[ "$(argv_value --model)" = "$MODEL_DIR/$MODEL_STUB" ]
[ "$(argv_value --n-gpu-layers)" -gt 0 ]
# llama.cpp divides --ctx-size across slots, so the observable contract
# is the per-slot window. The 9B swap exists precisely so every
# conversation gets the model's TRAINED 262,144 (the 27B had to be cut to
# 200,704 to fund two), and the harness contextWindow must quote 262144 —
# the pool and the slot count are neither, and a longer prompt is a 400.
# Three is also the ceiling: a fourth slot would push each window under the
# fleet's worst-case compaction input, which is this endpoint's job.
[ "$(argv_value --parallel)" -eq 3 ]
[ "$(argv_value --ctx-size)" -eq 786432 ]
local pool slots
pool="$(argv_value --ctx-size)"; slots="$(argv_value --parallel)"
[ $((pool % slots)) -eq 0 ]
[ $((pool / slots)) -eq 262144 ]
}
@test "the KV dtype pair follows the env file and flash-attn stays pinned on" {
# Asserted against the env rather than a literal so a retune moves one
# file, not two. The shipped pair is q8_0/q8_0: this payload bills
# 8 full-attention layers × 4 KV heads × 256 dim = 8,192 elements per token
# per cache, so f16/f16 would need 26,112 MiB of the 24,123 MiB card while
# q8_0 bills 17,408 B/token = 13,056.00 MiB at the 786,432 pool — the
# ledger said exactly that, and it is the referee. The 24 DeltaNet layers
# bill a fixed recurrent state per slot (452.25 MiB for three cells), which
# is why pool arithmetic alone under-prices the shape. flash-attn is not
# decoration: quantized V is only legal with it, and it must stay explicit
# so a future quant swap cannot silently fall back.
local env_k env_v
env_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
env_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --cache-type-k)" = "$env_k" ]
[ "$(argv_value --cache-type-v)" = "$env_v" ]
[ "$(argv_value --flash-attn)" = "on" ]
}
@test "log verbosity follows the env file" {
serve_argv
[ "$status" -eq 0 ]
# The shipped 4 (debug) is what makes VRAM-fit forensics possible from
# the journal (KV layout, buffer sizes, graph capture) — the only way to
# prove the q8_0 KV contract and the draft-KV bill from a log.
[ "$(argv_value --log-verbosity)" = "4" ]
# And an env that names no verbosity still ships an explicit value: the
# serve script's written-down default is 4 too, never a blank.
sed -i '/^VERBOSITY=/d' "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --log-verbosity)" = "4" ]
}
@test "the Prometheus metrics endpoint is served unless opted out" {
# Output contract, not a config read: what matters to the operator is that
# the engine is exec'd with --metrics, because llama.cpp answers 501 on
# /metrics without it and the fleet scrape then reports the endpoint DOWN
# (the oldest retained sample of this target's scrape, 2026-10-11, is a 0).
serve_argv
[ "$status" -eq 0 ]
grep -qxF -- '--metrics' <<<"$output"
# And the opt-out must be real: METRICS=0 ships no /metrics at all.
sed -i 's/^METRICS=.*/METRICS=0/' "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
! grep -qxF -- '--metrics' <<<"$output"
}
@test "the compute-buffer pair follows the env file and stays self-consistent" {
# The contract is: whatever rung the repo carries is the rung the engine
# is exec'd with — the number a benchmark attributes a prefill rate to
# must be a repo value, never an engine default nobody wrote down.
# Asserted against the env rather than a literal so a retune moves one
# file, not two.
local env_batch env_ubatch
env_batch="$(sed -nE 's/^BATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
env_ubatch="$(sed -nE 's/^UBATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --batch-size)" = "$env_batch" ]
[ "$(argv_value --ubatch-size)" = "$env_ubatch" ]
# llama.cpp refuses ubatch > batch, so a retune that inverts the pair has
# to fail here instead of at boot on a live endpoint.
[ "$env_batch" -ge "$env_ubatch" ]
# And an env that names neither flag still ships an explicit pair: the
# serve script's fallbacks are written-down values, not blanks the engine
# fills in silently.
sed -i '/^BATCH_SIZE=/d;/^UBATCH_SIZE=/d' "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
argv_value --batch-size | grep -qE '^[0-9]+$'
argv_value --ubatch-size | grep -qE '^[0-9]+$'
}
@test "speculation is off: no drafter flag reaches the argv" {
# Frank, 2026-10-18: this file DOES carry the MTP head (the boot arms prove
# it loads and drafts, acceptance 0.798/0.617 per position, mean accepted
# length 2.41) and it is still off, because the draft context bills its own
# f16 KV over the WHOLE pool — 4,096 B/position × 786,432 = 3,072 MiB plus
# buffers — and at --ubatch-size 2048 that squeeze halves decode: 167.2 →
# 109.2 t/s. The MTP arm is SPEC_TYPE=draft-mtp + UBATCH_SIZE=1024 (213.5
# t/s), documented in frank-zealot.env. The flags move as a set:
# SPEC_TYPE=none strips --spec-type, --spec-draft-n-max AND
# --spec-draft-model together, so a no-spec boot cannot carry half a spec
# pair (the ROCmFPX-era baseline arm segfaulted at load doing that).
serve_argv
[ "$status" -eq 0 ]
! grep -qxF -- '--spec-type' <<<"$output"
! grep -qxF -- '--spec-draft-n-max' <<<"$output"
! grep -qxF -- '--spec-draft-model' <<<"$output"
grep -qE '^SPEC_TYPE=none$' "$ENV_FILE"
}
@test "naming a drafter ships the MTP set, with or without a separate draft file" {
# The documented MTP arm (frank-zealot.env): SPEC_TYPE=draft-mtp ALONE is
# legal here because the head is IN the checkpoint and the draft context
# rides the main model's weights — so --spec-draft-model must NOT appear,
# or the trunk would be loaded twice.
serve_argv
{
echo 'SPEC_TYPE=draft-mtp'
echo 'SPEC_DRAFT_N_MAX=2'
} >> "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --spec-type)" = "draft-mtp" ]
[ "$(argv_value --spec-draft-n-max)" = "2" ]
! grep -qxF -- '--spec-draft-model' <<<"$output"
# And an external drafter still reaches the engine as a complete set — the
# pair the ROCmFPX-era crash was about.
echo "SPEC_DRAFT_FILE=$MODEL_DIR/$DRAFT_STUB" >> "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --spec-draft-model)" = "$MODEL_DIR/$DRAFT_STUB" ]
}
@test "vision ships by default and MMPROJ_FILE empty opts out of the image path" {
# The Bonsai-2 text file carries no vision tensors — the publisher's Q8_0
# projector argument IS the vision capability: no flag, no eyes. Q8_0 (not
# BF16) is the publisher's own requirement for concurrent image requests,
# which is what --parallel 3 means in practice.
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --mmproj)" = "$MODEL_DIR/$MM_STUB" ]
# The documented opt-out must reach the engine as ABSENCE of the flag,
# not as an argument pointing at an empty string.
sed -i 's|^MMPROJ_FILE=.*|MMPROJ_FILE=|' "$STAGE/test.env"
run env FRANK_ZEALOT_ENV_FILE="$STAGE/test.env" nvidia-modprobe=true bash "$SERVE"
[ "$status" -eq 0 ]
! grep -qxF -- '--mmproj' <<<"$output"
}
@test "the card sampler's only non-metadata term reaches the argv" {
# general.sampling.* (temperature / top_p / top_k / min_p / repeat_penalty)
# rides in the checkpoint and the engine reads it itself; presence penalty
# is the ONE recipe value with no GGUF metadata key, so it must reach argv
# or the recipe is silently incomplete. Distinctive value first: proof the
# flag is wired to the env and not merely to the script's default.
serve_argv
{ echo 'PRESENCE_PENALTY=0.5'; } >> "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --presence-penalty)" = "0.5" ]
# The shipped recipe value reaches the engine too.
local pp
pp="$(sed -nE 's/^PRESENCE_PENALTY=(.+)$/\1/p' "$ENV_FILE")"
sed -i "s/^PRESENCE_PENALTY=.*/PRESENCE_PENALTY=$pp/" "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --presence-penalty)" = "$pp" ]
# The documented opt-out is absence of the flag, not an empty argument —
# which is why the default uses ${VAR=default}, not ${VAR:=default}.
sed -i "s/^PRESENCE_PENALTY=.*/PRESENCE_PENALTY=/" "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
! grep -qxF -- '--presence-penalty' <<<"$output"
}
@test "the fleet reasoning contract reaches the argv" {
serve_argv
[ "$status" -eq 0 ]
# deepseek format = thinking lands in reasoning_content, the field the
# harness reads on this endpoint; --jinja is what makes
# chat_template_kwargs reach the template at all.
[ "$(argv_value --reasoning-format)" = "deepseek" ]
argv_value --jinja >/dev/null
grep -qxF -- '--jinja' <<<"$output"
}
@test "the serve script refuses a missing runtime, GGUF or projector" {
rm -f "$MODEL_DIR/$MM_STUB"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"frank-zealot: missing"* ]]
rm -f "$RUNTIME_DIR/bin/llama-server"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"frank-zealot: missing"* ]]
}
@test "the deploy handler gates on binary, GGUF and projector, then MainPID-hardens health" {
grep -q 'deploy_llm_zealot' "$REMOTE_DEPLOY"
grep -q 'LLM_ZEALOT_RUNTIME' "$REMOTE_DEPLOY"
grep -q 'LLM_ZEALOT_MODEL_FILE' "$REMOTE_DEPLOY"
grep -q 'LLM_ZEALOT_MMPROJ' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-zealot' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-zealot.service' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
grep -q 'frank-zealot.service has no MainPID' "$REMOTE_DEPLOY"
}
@test "the two endpoints keep separate ports and the large handler never pkills llama-server" {
# frank-dragoon's sweeps must not take down frank-zealot, whose
# process is llama-server on the SAME host.
grep -q '^LISTEN_PORT=7771$' "$ENV_FILE"
local large
large="$(sed -n '/^deploy_llm_dragoon() {/,/^}/p' "$REMOTE_DEPLOY")"
! grep -q 'pkill -x llama-server' <<<"$large"
}