frank-small: llama.cpp CUDA (master, sm_86) on MiMo Q6_K GGUF, 2 x 262144, asymmetric KV (f16 K + q8_0 V, flash-attn pinned for the V quant)
339 lines
15 KiB
Bash
Executable File
339 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# Seam: the frank-large root unit on dankclaw.
|
|
#
|
|
# Since 2026-09-28 this endpoint is served by gufo (gufo-org/gufo), the
|
|
# Strix Halo inference engine built from the pinned v0.1.1 tag, serving
|
|
# Swift-1.5-Qwen3.8-27B-Uncensored (abliterated) UD-Q8_K_XL with the z-lab
|
|
# DFlash2 Q4_K_M drafter. Two changes landed on this endpoint that day: the
|
|
# engine (from the stock llama.cpp prebuilt) and the payload (from
|
|
# unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL). Port, alias and client contract did
|
|
# not move.
|
|
#
|
|
# Most of 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 rather than the text of the script: a tunable renamed,
|
|
# dropped, or silently defaulted away fails here the way it would fail a
|
|
# deployment.
|
|
load helpers
|
|
|
|
ENV_FILE="$REPO_ROOT/scripts/frank-large.env"
|
|
UNIT="$REPO_ROOT/scripts/frank-large.service"
|
|
SERVE="$REPO_ROOT/scripts/serve-frank-large.sh"
|
|
FETCH="$REPO_ROOT/scripts/download-frank-large-q4.sh"
|
|
DRAFTER_FETCH="$REPO_ROOT/scripts/download-frank-large.sh"
|
|
BUILD="$REPO_ROOT/scripts/build-gufo.sh"
|
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|
# The deployed target's filename, kept here so a rename in the env file and a
|
|
# rename in this stub cannot drift apart unnoticed.
|
|
TARGET_STUB="Qwen3.8-27B-UD-Q4_K_XL.gguf"
|
|
setup() {
|
|
STAGE="$(mktemp -d)"
|
|
RUNTIME_DIR="$STAGE/gufo"
|
|
MODEL_DIR="$STAGE/models"
|
|
DRAFTER_DIR="$STAGE/draft"
|
|
mkdir -p "$RUNTIME_DIR/bin" "$MODEL_DIR" "$DRAFTER_DIR"
|
|
# A stub engine: the serve script execs it, so $@ is exactly what gufo
|
|
# would be invoked with.
|
|
cat > "$RUNTIME_DIR/bin/gufo" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf '%s\n' "$@"
|
|
STUB
|
|
chmod +x "$RUNTIME_DIR/bin/gufo"
|
|
# The target and the drafter live in SEPARATE directories since the
|
|
# 2026-09-28 payload swap: the target came from a different repository,
|
|
# the DFlash2 drafter did not move. A stub that colocated them would
|
|
# never notice DRAFTER_DIR going stale.
|
|
: > "$MODEL_DIR/$TARGET_STUB"
|
|
: > "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
|
|
STUB_ENV="$STAGE/stub.env"
|
|
cat > "$STUB_ENV" <<EOF
|
|
RUNTIME_DIR=$RUNTIME_DIR
|
|
MODEL_DIR=$MODEL_DIR
|
|
DRAFTER_DIR=$DRAFTER_DIR
|
|
EOF
|
|
}
|
|
|
|
teardown() {
|
|
if [ -n "${STAGE:-}" ]; then
|
|
rm -rf "$STAGE"
|
|
fi
|
|
}
|
|
|
|
# serve_argv [env-file] — run the serve script and capture the engine's argv,
|
|
# one token per line, in $output. No argument exercises the built-in defaults
|
|
# with the stub's paths.
|
|
serve_argv() {
|
|
local envfile="${1:-$STUB_ENV}"
|
|
if [ -n "$envfile" ]; then
|
|
FRANK_LARGE_ENV_FILE="$envfile" run bash "$SERVE"
|
|
else
|
|
run env FRANK_LARGE_ENV_FILE="$STAGE/absent.env" bash "$SERVE"
|
|
fi
|
|
}
|
|
|
|
# argv_has <token> — exact token match, so --port never satisfies a test
|
|
# looking for a different flag.
|
|
argv_has() {
|
|
grep -qxF -- "$1" <<<"$output"
|
|
}
|
|
|
|
# argv_value <flag> — the token following <flag>, empty if the flag is absent.
|
|
argv_value() {
|
|
local i
|
|
i="$(grep -nx -- "$1" <<<"$output" | cut -d: -f1)"
|
|
[ -n "$i" ] || return 0
|
|
sed -n "$((i + 1))p" <<<"$output"
|
|
}
|
|
|
|
@test "the frank-large unit, env, serve and build files exist where the deployer installs them from" {
|
|
[ -f "$ENV_FILE" ]
|
|
[ -f "$UNIT" ]
|
|
[ -x "$SERVE" ]
|
|
[ -x "$BUILD" ]
|
|
}
|
|
|
|
@test "frank-large is boot-persistent, self-healing and unprivileged" {
|
|
grep -q '^WantedBy=multi-user.target$' "$UNIT"
|
|
grep -q '^Restart=on-failure$' "$UNIT"
|
|
grep -q '^RestartSec=' "$UNIT"
|
|
grep -q '^User=dank$' "$UNIT"
|
|
# The engine pins buffers; the memlock budget must be unbounded or the
|
|
# load fails on the RLIMIT.
|
|
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
|
|
# The unit runs the repo's own serve script with the repo's env.
|
|
grep -q 'ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh' "$UNIT"
|
|
grep -q 'FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env' "$UNIT"
|
|
}
|
|
|
|
@test "the fleet endpoint and alias survive the cutover" {
|
|
serve_argv
|
|
[ "$status" -eq 0 ]
|
|
# frank-large at :7770, whatever engine answers it.
|
|
[ "$(argv_value --served-model-name)" = "frank-large" ]
|
|
[ "$(argv_value --port)" = "7770" ]
|
|
[ "$(argv_value --host)" = "0.0.0.0" ]
|
|
}
|
|
|
|
@test "the built-in defaults are one session of the full 262144 window with DFlash2" {
|
|
serve_argv
|
|
[ "$status" -eq 0 ]
|
|
# 262144 is the model's native window (the operator's call, 2026-09-21).
|
|
[ "$(argv_value --context)" -eq 262144 ]
|
|
# One session is the single stream every prior frank-large served.
|
|
[ "$(argv_value --sessions)" -eq 1 ]
|
|
[ "$(argv_value --speculative)" = "dflash2" ]
|
|
# A draft ceiling of 0 would silently serve the autoregressive rate.
|
|
[ "$(argv_value --draft-tokens)" -gt 0 ]
|
|
}
|
|
|
|
@test "the shipped env file, not just the defaults, serves the deployed shape" {
|
|
# The defaults are a floor; systemd sources the env file. Only the
|
|
# on-host paths are redirected at the stub, so every fleet value in the
|
|
# shipped file is the one under test. MODEL_FILE and DRAFTER_FILE are
|
|
# absolute in the shipped file, so they need redirecting too — a sed that
|
|
# only rewrote MODEL_DIR would leave them pointing at /home/dank and the
|
|
# test would pass for the wrong reason (or fail on a box that has them).
|
|
sed -e "s#^RUNTIME_DIR=.*#RUNTIME_DIR=$RUNTIME_DIR#" \
|
|
-e "s#^MODEL_DIR=.*#MODEL_DIR=$MODEL_DIR#" \
|
|
-e "s#^MODEL_FILE=.*#MODEL_FILE=$MODEL_DIR/$TARGET_STUB#" \
|
|
-e "s#^DRAFTER_DIR=.*#DRAFTER_DIR=$DRAFTER_DIR#" \
|
|
-e "s#^DRAFTER_FILE=.*#DRAFTER_FILE=$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf#" \
|
|
"$ENV_FILE" > "$STAGE/shipped.env"
|
|
serve_argv "$STAGE/shipped.env"
|
|
[ "$status" -eq 0 ]
|
|
[ "$(argv_value --served-model-name)" = "frank-large" ]
|
|
[ "$(argv_value --host)" = "0.0.0.0" ]
|
|
[ "$(argv_value --port)" = "7770" ]
|
|
# 2026-10-01 night: Frank rolled frank-large back to ONE session at the
|
|
# full native window (gufo Q4_K_XL, f16 KV — gufo has no KV quant lever).
|
|
[ "$(argv_value --context)" -eq 262144 ]
|
|
[ "$(argv_value --sessions)" -eq 1 ]
|
|
[ "$(argv_value --speculative)" = "dflash2" ]
|
|
[ "$(argv_value --draft-tokens)" -gt 0 ]
|
|
[ "$(argv_value --draft-policy)" = "adaptive" ]
|
|
# The shipped file must name the target the fleet actually serves, not a
|
|
# path that merely happens to exist.
|
|
[ "$(argv_value --model)" = "$MODEL_DIR/$TARGET_STUB" ]
|
|
[ "$(argv_value --dflash-model)" = "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf" ]
|
|
}
|
|
|
|
@test "the engine is pointed at the payload that is actually on disk" {
|
|
serve_argv
|
|
[ "$status" -eq 0 ]
|
|
[ -f "$(argv_value --model)" ]
|
|
# A drafter path that does not exist would load the target and decode at
|
|
# the autoregressive rate, which reads as "the new runtime is slow".
|
|
[ -f "$(argv_value --dflash-model)" ]
|
|
[ "$(argv_value --model)" != "$(argv_value --dflash-model)" ]
|
|
}
|
|
|
|
@test "frank-large stays text-only" {
|
|
# The payload has no mmproj, so an --mmproj would point at nothing. The
|
|
# fleet's image path is frank-small: its SGLang MiMo payload carries the
|
|
# vision tower inside the checkpoint (no projector file at all), and
|
|
# models.yml declares frank-large text-only.
|
|
serve_argv
|
|
! argv_has --mmproj
|
|
grep -q 'MiMo-Q6K' "$REPO_ROOT/scripts/frank-small.env"
|
|
}
|
|
|
|
@test "the engine is asked for greedy decoding and for preserved prior reasoning" {
|
|
# Greedy is what this endpoint has always served; a silent switch to
|
|
# sampled output would be a model-behaviour change disguised as a
|
|
# runtime swap. Prior thinking blocks must survive across turns or a
|
|
# multi-turn agent loses its own reasoning between tool calls.
|
|
serve_argv
|
|
[ "$(argv_value --temperature)" = "0" ]
|
|
[ "$(argv_value --preserve-thinking)" = "on" ]
|
|
}
|
|
|
|
@test "the serve script refuses a missing runtime with the build pointer" {
|
|
rm -f "$RUNTIME_DIR/bin/gufo"
|
|
serve_argv
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"runtime not found"* ]]
|
|
[[ "$output" == *"build-gufo.sh"* ]]
|
|
# A restart loop would be the alternative; nothing may be exec'd.
|
|
! argv_has serve
|
|
}
|
|
|
|
@test "the serve script refuses a missing drafter with a fetch pointer" {
|
|
# The drafter lives in its own directory now, so the target can be present
|
|
# and this still has to be caught.
|
|
rm -f "$DRAFTER_DIR/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
|
|
serve_argv
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"missing asset"* ]]
|
|
[[ "$output" == *"Qwen3.8-27B-DFlash2-Q4_K_M.gguf"* ]]
|
|
[[ "$output" == *"download-frank-large"* ]]
|
|
! argv_has serve
|
|
}
|
|
|
|
@test "the serve script refuses a missing target and names the file" {
|
|
rm -f "$MODEL_DIR/$TARGET_STUB"
|
|
serve_argv
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"missing asset"* ]]
|
|
[[ "$output" == *"$TARGET_STUB"* ]]
|
|
}
|
|
|
|
@test "both payload files are fetched resumably and verified against declared sizes + sha256" {
|
|
# The target and the drafter come from DIFFERENT repositories, fetched by
|
|
# two scripts. Anchor the repo checks to the URL ASSIGNMENT, not to the
|
|
# file: the same repo string also appears in each script's header comment,
|
|
# so a plain grep stayed green after the URL was repointed at another
|
|
# repository -- the script would then download something the serve script
|
|
# never asked for.
|
|
grep -qE '^URL="https://huggingface\.co/unsloth/Qwen3\.8-27B-GGUF/resolve/main/' "$FETCH"
|
|
grep -qE 'https://huggingface\.co/z-lab/Qwen3\.8-27B-DFlash2-GGUF/resolve/main/' "$DRAFTER_FETCH"
|
|
for f in "$FETCH" "$DRAFTER_FETCH"; do
|
|
# -f so an HTTP error is a failure rather than a small "artifact";
|
|
# -C - so an interrupted 29 GiB run resumes.
|
|
grep -q -- '-fL -C -' "$f"
|
|
# The size and the sha256 are compared after the transfer, not trusted.
|
|
grep -q 'SHA256 MISMATCH' "$f"
|
|
grep -q 'SIZE MISMATCH' "$f"
|
|
done
|
|
}
|
|
|
|
@test "the serve script and the fetch scripts agree on the payload filenames" {
|
|
# The serve script's MODEL_FILE and the fetch script's REL are two
|
|
# independent spellings of one filename, and a repo is not required to
|
|
# keep them in step: a re-upload under a new name, or an env edited
|
|
# without the fetch, leaves the endpoint unable to start with a
|
|
# "missing asset" that no test would have predicted. Bind them here.
|
|
local served fetched drafter_served drafter_fetched
|
|
served="$(basename "$(grep '^MODEL_FILE=' "$ENV_FILE" | cut -d= -f2-)")"
|
|
fetched="$(sed -n 's/^REL="\(.*\)"$/\1/p' "$FETCH")"
|
|
[ -n "$served" ]
|
|
[ "$served" = "$fetched" ]
|
|
drafter_served="$(basename "$(grep '^DRAFTER_FILE=' "$ENV_FILE" | cut -d= -f2-)")"
|
|
drafter_fetched="$(sed -n 's/^ *"\(Qwen3\.8-27B-DFlash2[^ ]*\.gguf\) .*/\1/p' "$DRAFTER_FETCH")"
|
|
[ -n "$drafter_served" ]
|
|
[ "$drafter_served" = "$drafter_fetched" ]
|
|
}
|
|
|
|
@test "the drafter is still fetched by the script that carries its Hub repo" {
|
|
# Guards a future "simplification" that folds the drafter into the
|
|
# payload-fetch script and silently orphans the artifact the serve script
|
|
# still points at via DRAFTER_DIR. The literal drafter name must not
|
|
# appear in that script AT ALL, comments included.
|
|
grep -q 'Qwen3.8-27B-UD-Q8_K_XL' "$DRAFTER_FETCH"
|
|
! grep -q 'DFlash2' "$FETCH"
|
|
}
|
|
|
|
@test "the build script refuses a source tree that is not the pinned commit" {
|
|
# gufo ships no prebuilt Linux binaries, so the runtime is built here.
|
|
# A tag that moves under us would silently install different kernels
|
|
# under the same RUNTIME_DIR, so the pin is enforced, not documented.
|
|
local src="$STAGE/not-the-pinned-commit"
|
|
mkdir -p "$src"
|
|
git -C "$src" init -q
|
|
git -C "$src" -c user.email=t@t -c user.name=t commit -q --allow-empty -m drift
|
|
run env GUFO_SRC_DIR="$src" GUFO_PREFIX="$STAGE/absent-prefix" bash "$BUILD" --verify
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"GUFO_COMMIT mismatch"* ]]
|
|
}
|
|
|
|
@test "the build script reports a runtime that was never built" {
|
|
# --verify on a host with no install must say so rather than exit 0 on
|
|
# an empty check.
|
|
run env GUFO_SRC_DIR="$STAGE/no-source-tree" GUFO_PREFIX="$STAGE/absent-prefix" bash "$BUILD" --verify
|
|
[ "$status" -ne 0 ]
|
|
[[ "$output" == *"no gufo at"* ]]
|
|
}
|
|
|
|
@test "the deploy handler gates on the runtime, the payload and the port" {
|
|
grep -q 'relink_unit frank-large' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl enable frank-large.service' "$REMOTE_DEPLOY"
|
|
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
|
|
grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY"
|
|
grep -q 'LLM_LARGE_RUNTIME' "$REMOTE_DEPLOY"
|
|
grep -q 'LLM_LARGE_MODEL' "$REMOTE_DEPLOY"
|
|
grep -q 'LLM_LARGE_DRAFT' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the deploy handler will not stop a serving endpoint it cannot replace" {
|
|
# The runtime gate must be live (a real conditional on an executable
|
|
# path, not a branch that never fires) and it must return BEFORE the
|
|
# stop, or a host whose gufo was never built takes frank-large down on
|
|
# its way to reporting the failure.
|
|
local large gate bail stop
|
|
large="$(sed -n '/^deploy_llm_large() {/,/^}/p' "$REMOTE_DEPLOY")"
|
|
[[ "$large" == *'build-gufo.sh'* ]]
|
|
gate="$(grep -n 'if \[\[ ! -x "\$LLM_LARGE_RUNTIME" \]\]' <<<"$large" | head -1 | cut -d: -f1)"
|
|
stop="$(grep -n 'systemctl stop frank-large' <<<"$large" | head -1 | cut -d: -f1)"
|
|
[ -n "$gate" ]
|
|
[ -n "$stop" ]
|
|
bail="$(awk -v g="$gate" 'NR > g && /return 1/ { print NR; exit }' <<<"$large")"
|
|
[ -n "$bail" ]
|
|
[ "$bail" -lt "$stop" ]
|
|
}
|
|
|
|
@test "the stray-run sweep keys on the drafter and cannot take frank-small down" {
|
|
# The sweep used to key on the TARGET filename, which the 2026-09-28
|
|
# payload swap moved to a different repository. It would have stopped
|
|
# matching the live process, and a stray run would then hold 7770 past the
|
|
# port gate and fail the deploy. The drafter is the part that did NOT
|
|
# move, and every frank-large command line carries it via --dflash-model.
|
|
grep -q "pkill -f 'Qwen3.8-27B-DFlash2-Q4_K_M'" "$REMOTE_DEPLOY"
|
|
# frank-small serves a different GGUF on the 3090 with no DFlash2 drafter,
|
|
# so a binary-name sweep is the only thing that would endanger it.
|
|
local large
|
|
large="$(sed -n '/^deploy_llm_large() {/,/^}/p' "$REMOTE_DEPLOY")"
|
|
! grep -q 'pkill -x llama-server' <<<"$large"
|
|
! grep -q 'pkill -x gufo' <<<"$large"
|
|
}
|
|
|
|
@test "the deploy handler retires the halogen quadlet without deleting it" {
|
|
grep -q 'retire_halogen_large_quadlet' "$REMOTE_DEPLOY"
|
|
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
|
|
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?.*frank-large' "$REMOTE_DEPLOY"
|
|
# Nothing live ships for this endpoint as a container any more. The
|
|
# archived copy is host state in an ignored .AGENTTRASH — deliberately
|
|
# untracked, and present only in a primary checkout — so search the
|
|
# tracked tree, not the filesystem.
|
|
[ ! -e "$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container" ]
|
|
! git -C "$REPO_ROOT" grep -q 'Image=.*halogen' -- dankclaw
|
|
}
|