Frank's order, 2026-10-19: --parallel 3 at 200,704 a conversation (pool 602,112), K at the engine default (f16), V to q8_0, batch 1024x6 / ubatch 1024, and the MTP drafter armed explicitly. The three requests are one budget equation. At 8,192 elements per token per cache the pair now bills 25,088 B/token = 14,406.00 MiB over the 602,112 pool, and the armed draft context adds its own f16 KV over that whole pool (4,096 B/position = 2,352.00 MiB) plus buffers and recurrent cells. Dropping V to the 8-bit class frees 4,410 MiB against exact V and the ubatch drop hands back ~1,306 - that is what pays for the third slot AND the drafter, neither of which exact V could fund. The serve script's written-down fallbacks move with the env so a missing env file cannot boot the retired shape, and the seam now: asserts the per-slot contract (divides evenly, <= the trained 262,144), bills an armed drafter in the VRAM fit gate, proves the gate still REFUSES both boots this shape can walk into, pins the shipped MTP set (and the absence of --spec-draft-model, which would load the trunk twice), and pins SPEC_TYPE=none as the rollback that strips the whole set.
440 lines
21 KiB
Bash
Executable File
440 lines
21 KiB
Bash
Executable File
# 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 ONLY loader that registers the payload's custom
|
||
# tensor type 142 and honours its prism.hadamard.* metadata: THREE 200,704-token
|
||
# conversations (--ctx-size 602112 --parallel 3) over an f16 K + q8_0 V cache —
|
||
# 25,088 B/token, 14,406.00 MiB at this pool — with the MTP drafter ARMED
|
||
# (--spec-type draft-mtp). The asymmetry IS the shape: exact V (32,768 B/token)
|
||
# leaves nothing for a draft context that bills its own f16 KV over the WHOLE
|
||
# pool (4,096 B/position, 2,352.00 MiB here), and the 786,432 pool this shape
|
||
# replaced cannot carry a drafter at any dtype inside the 24,123 MiB the driver
|
||
# leaves addressable. The publisher's own Q8_0 vision projector rides along (the
|
||
# BF16 one can crash on concurrent images). Log
|
||
# 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
|
||
}
|
||
|
||
# kv_bytes_per_token DTYPE — bytes ONE cache costs per token at this payload's
|
||
# geometry: 8 full-attention layers × 4 KV heads × 256 dim = 8,192 elements
|
||
# (measured from the GGUF header: n_embd_k_gqa = n_embd_v_gqa = 1024). f16 is
|
||
# 2 B/element; q8_0 is 1.0625 (32 codes plus one 2-byte scale per 32-block);
|
||
# q4_0 is 0.625 (codes + scale + min per 32-block). Unknown dtype is a FAILURE,
|
||
# never a silent zero — the fit gate below is only honest if it cannot be
|
||
# satisfied by a rung it does not understand.
|
||
kv_bytes_per_token() {
|
||
case "$1" in
|
||
f16|bf16) echo 16384 ;;
|
||
q8_0) echo 8704 ;;
|
||
q4_0) echo 5120 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# 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 200704 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: harness contextWindow, Prometheus dashboards and every
|
||
# consumer's compaction trigger read THAT number, and a longer prompt is a
|
||
# 400, never a truncation. Two invariants the pool/slot split has to keep:
|
||
# it divides evenly (llama.cpp floors it otherwise) and a slot never exceeds
|
||
# the model's TRAINED window (there is no YaRN here, so the engine would be
|
||
# quoting a window it cannot fill).
|
||
[ "$(argv_value --parallel)" -eq 3 ]
|
||
[ "$(argv_value --ctx-size)" -eq 602112 ]
|
||
local pool slots window
|
||
pool="$(argv_value --ctx-size)"; slots="$(argv_value --parallel)"
|
||
[ $((pool % slots)) -eq 0 ]
|
||
window=$((pool / slots))
|
||
[ "$window" -eq 200704 ]
|
||
[ "$window" -le 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 ASYMMETRIC — f16 K (the engine's own
|
||
# default) plus q8_0 V: this payload bills 8,192 elements per token per cache
|
||
# (8 full-attention layers × 4 KV heads × 256 dim), so K costs 16,384 B/token
|
||
# and V 8,704 (q8_0 is 1.0625 B/element; there is no true fp8 KV dtype in
|
||
# this build) = 25,088 B/token = 14,406.00 MiB at the 602,112 pool. Exact V
|
||
# would bill 32,768 B/token and the all-q8_0 pair this file used to ship
|
||
# billed 17,408 — the halfway rung is what pays for the third slot AND the
|
||
# armed drafter (the fit gate below prices both). The 24 DeltaNet layers
|
||
# bill a fixed recurrent state per slot (~50.25 MiB a cell), which is why
|
||
# pool arithmetic alone under-prices the shape. flash-attn is not
|
||
# decoration: quantized V is only legal with it, so it stays explicit and a
|
||
# dtype retune 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 "the shipped KV dtype and pool actually fit the card this boots on" {
|
||
# No health gate can see this mistake: llama.cpp sizes the KV pool as
|
||
# rate × pool-positions and cudaMalloc fails at boot ON THE LIVE ENDPOINT.
|
||
# The fit is arithmetic off the payload geometry plus the non-KV terms
|
||
# measured off the boot ledger (weights 2,036.71 + compute buffers 1,409.02
|
||
# + projector 595 + 248.10 + CUDA context ≈ 460 = 4,749 MiB, dtype- and
|
||
# slot-independent at ubatch 2048), plus one ~51 MiB recurrent-state cell per
|
||
# slot. That constant is deliberately NOT credited with the ~1,306 MiB the
|
||
# shipped ubatch 1024 hands back: the gate stays conservative.
|
||
# An ARMED drafter is billed, because it is the term this shape pays
|
||
# CACHE_TYPE_V=q8_0 to afford: the draft context is built against the
|
||
# TARGET's whole pool at its own f16 rate (measured 4,096 B/position at
|
||
# blk.32) plus buffers and its own recurrent cells — the measured spec-ON
|
||
# against spec-OFF delta at the 786,432 / ubatch-1024 arm was 3,524 MiB
|
||
# beside 3,072 MiB of draft KV, i.e. ~452 MiB, doubled here for the third
|
||
# slot's extra cells. 24,123 MiB is what the driver leaves addressable
|
||
# (nvidia-smi reports it as used + free; its total column reads 24,576).
|
||
local env_k env_v pool slots rate_k rate_v kv total spec f16 exact_pool exact_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")"
|
||
pool="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||
slots="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||
spec="$(sed -nE 's/^SPEC_TYPE=(.+)$/\1/p' "$ENV_FILE")"
|
||
rate_k="$(kv_bytes_per_token "$env_k")"
|
||
rate_v="$(kv_bytes_per_token "$env_v")"
|
||
f16="$(kv_bytes_per_token f16)"
|
||
kv="$(( pool * (rate_k + rate_v) / 1048576 ))"
|
||
total="$(( kv + 4749 + 51 * slots ))"
|
||
if [ "$spec" != "none" ]; then
|
||
total="$(( total + pool * 4096 / 1048576 + 768 ))"
|
||
fi
|
||
# The shipped arm has to leave real room: the projector's worst case is
|
||
# charged AFTER the engine's own fit check, so a projection that lands on
|
||
# the ceiling is a crash waiting for the first image request.
|
||
[ "$total" -le $(( 24123 - 1024 )) ]
|
||
|
||
# And the gate must be able to REFUSE, or passing above means nothing. Two
|
||
# boots this shape can walk straight into: the retired dtype pair at the
|
||
# retired pool (exact f16 at 786,432 = 24,576 MiB of cache alone, which died
|
||
# at cudaMalloc), and the shipped pool with V moved back to f16 while the
|
||
# drafter stays armed — 18,816 MiB of cache plus the draft's 2,352. Both die
|
||
# before /health ever answers, and no health gate can see either.
|
||
exact_pool="$(( 786432 * (rate_k + f16) / 1048576 + 4749 + 51 * 3 ))"
|
||
[ "$exact_pool" -gt 24123 ]
|
||
exact_v="$(( pool * (rate_k + f16) / 1048576 + 4749 + 51 * slots
|
||
+ pool * 4096 / 1048576 + 768 ))"
|
||
[ "$exact_v" -gt 24123 ]
|
||
}
|
||
|
||
@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 "the armed drafter ships the whole MTP set and no second copy of the trunk" {
|
||
# Frank, 2026-10-19: the drafter is ON. The head is IN this file
|
||
# (qwen35.nextn_predict_layers = 1, blk.32.nextn.*) and it accepted well
|
||
# when it was measured off (0.798/0.617 per position, mean accepted length
|
||
# 2.41); what it now pays for is CACHE_TYPE_V=q8_0 plus UBATCH_SIZE=1024,
|
||
# which is why the dtype pair and the batch pair are pinned next door.
|
||
# The observable contract is the SET: --spec-type plus every --spec-draft-*
|
||
# the env names, and NO --spec-draft-model — the MTP draft rides the MAIN
|
||
# model's weights, so a draft file would load the trunk a second time.
|
||
local n_max
|
||
n_max="$(sed -nE 's/^SPEC_DRAFT_N_MAX=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||
serve_argv
|
||
[ "$status" -eq 0 ]
|
||
[ "$(argv_value --spec-type)" = "draft-mtp" ]
|
||
[ "$(argv_value --spec-draft-n-max)" = "$n_max" ]
|
||
[ "$(argv_value --spec-draft-n-min)" = "0" ]
|
||
[ "$(argv_value --spec-draft-p-min)" = "0" ]
|
||
! grep -qxF -- '--spec-draft-model' <<<"$output"
|
||
}
|
||
|
||
@test "the rollback knob strips the entire spec set" {
|
||
# SPEC_TYPE=none is the documented retreat (and the shape this endpoint
|
||
# shipped before 2026-10-19). The law it exists to obey: the flags move as a
|
||
# SET — a no-spec boot that keeps --spec-draft-n-max, or a baseline arm that
|
||
# hands over --model-draft with no --spec-type, has segfaulted at load with
|
||
# no diagnostic. So the rollback is proven by ABSENCE of all of them.
|
||
serve_argv
|
||
sed -i 's/^SPEC_TYPE=.*/SPEC_TYPE=none/' "$STAGE/test.env"
|
||
run_stage
|
||
[ "$status" -eq 0 ]
|
||
! grep -qxF -- '--spec-type' <<<"$output"
|
||
! grep -qxF -- '--spec-draft-n-max' <<<"$output"
|
||
! grep -qxF -- '--spec-draft-n-min' <<<"$output"
|
||
! grep -qxF -- '--spec-draft-p-min' <<<"$output"
|
||
! grep -qxF -- '--spec-draft-model' <<<"$output"
|
||
}
|
||
|
||
@test "an external drafter still reaches the engine as a complete set" {
|
||
# The other arm the ROCmFPX-era crash was about: a named draft FILE must
|
||
# arrive alongside --spec-type and the depth recipe, never alone. This
|
||
# endpoint does not use it (the head is in the trunk), so the knob ships
|
||
# empty and only this test sets it.
|
||
serve_argv
|
||
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" ]
|
||
[ "$(argv_value --spec-type)" = "draft-mtp" ]
|
||
}
|
||
|
||
@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 2 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"
|
||
}
|