Frank: "shift frank-zealot to 2 parallels of unquantized kv". Shape: CTX_SIZE 786432 -> 524288, PARALLEL 3 -> 2, CACHE_TYPE_K/V q8_0 -> f16. The per-slot window is UNCHANGED at 262,144 (the native trained window, and models.yml contextWindow), so what the exact cache buys is fidelity and what it pays is one concurrent conversation. The POOL is the term that had to move: exact KV bills 32,768 B/token at this geometry (8 full-attention layers x 4 KV heads x 256 dim, 2 B, K and V), so the old 786,432 pool asks 24,576 MiB against the 24,123 MiB the driver leaves addressable. 524,288 positions = 16,384 MiB, projected ~21,200 used / ~2,900 free from the 13:19 boot ledger (only the KV pool and one recurrent-state cell move). Also corrects a repo-wide arithmetic error: "f16 would bill 26,112 MiB" doubled the q8_0 ledger figure INCLUDING its 1/32 scale overhead. q8_0 is 1.0625 B/element, f16 exactly 2.0, so the true ratio is 1.882x. The wall stands; the number was wrong. And it corrects a false claim about the drafter: this build DOES expose --spec-draft-type-k / --spec-draft-type-v (verified against llama-server --help), so the draft cache is quantizable -- the exact-KV-plus-MTP exclusion is a measurement gap, not a law, and the 1,088 MiB quantized-draft arm is recorded as unmeasured. Seam: tests/frank_zealot_unit.bats re-pins the shape contract (2 slots, 524,288 pool, 262,144 per slot) and adds a VRAM fit gate that computes dtype x pool + the measured non-KV ledger terms against 24,123 MiB with >=1 GiB of slack -- and proves the gate REFUSES exact KV at the old pool, so passing means something. 18/18 green.
411 lines
19 KiB
Bash
Executable File
411 lines
19 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: TWO 262,144-token
|
||
# conversations (--ctx-size 524288 --parallel 2) over an UNQUANTIZED f16 KV
|
||
# cache. Exact KV bills 32,768 B/token here, so the pool is what pays for it —
|
||
# the same dtype at the old 786,432 pool is 24,576 MiB against the 24,123 MiB
|
||
# the driver leaves addressable, and the slot count is the term that moved. The
|
||
# publisher's own Q8_0 vision projector rides along (the BF16 one can crash on
|
||
# concurrent images), and MTP stays OFF: the head is present, but exact KV
|
||
# leaves no room for a draft context that bills f16 KV over the whole pool. 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
|
||
}
|
||
|
||
# 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 "two 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. Exact KV costs the SLOT COUNT, not the window: every
|
||
# conversation still gets the model's TRAINED 262,144, and the harness
|
||
# contextWindow must quote that number — the pool and the slot count are
|
||
# neither, and a longer prompt is a 400, never a truncation.
|
||
[ "$(argv_value --parallel)" -eq 2 ]
|
||
[ "$(argv_value --ctx-size)" -eq 524288 ]
|
||
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 f16/f16: exact attention state, and the
|
||
# slot count is what makes it affordable — this payload bills 8,192 elements
|
||
# per token per cache (8 full-attention layers × 4 KV heads × 256 dim), so
|
||
# exact KV is 32,768 B/token = 16,384.00 MiB at the 524,288 pool, where the
|
||
# q8_0 pair this file used to ship billed 17,408 B/token = 13,056.00 MiB at
|
||
# the old 786,432 pool (ledger-exact both times). 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: the quantized-V fallback 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 this ubatch), plus one ~51 MiB recurrent-state cell
|
||
# per slot. 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
|
||
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")"
|
||
rate_k="$(kv_bytes_per_token "$env_k")"
|
||
rate_v="$(kv_bytes_per_token "$env_v")"
|
||
kv="$(( pool * (rate_k + rate_v) / 1048576 ))"
|
||
total="$(( kv + 4749 + 51 * slots ))"
|
||
# 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: the same exact KV at the pool this
|
||
# shape left behind is 24,576 MiB of cache alone — a boot that dies before
|
||
# /health ever answers. Proving the gate bites is what makes passing one
|
||
# meaningful.
|
||
local over
|
||
over="$(( 786432 * (rate_k + rate_v) / 1048576 + 4749 + 51 * 3 ))"
|
||
[ "$over" -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 "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 × 524,288 = 2,048 MiB plus
|
||
# buffers — on a trunk that already projects 21,234 MiB with exact KV. The
|
||
# MTP arms in frank-zealot.env are all q8_0-era measurements; the doors back
|
||
# to the drafter are one window per conversation, or KV back to q8_0 with
|
||
# UBATCH_SIZE=1024 (213.5 t/s). 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 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"
|
||
}
|