Files
serverAppConfigs/tests/frank_small_unit.bats
T
Frank 4b7de59dc8 frank-small: Gemma4-12B Q4_K_M, MTP sidecar + ngram-mod, q8 KV, --fit ctx
The Qwen3.8-27B IQ3_S file carried its own MTP head; Gemma4's ships as a
separate assistant file (mtp-gemma-4-12B-it.gguf, arch gemma4-assistant,
4 nextn layers) that loads via --model-draft and shares the target's last
layers' KV. The draftless ngram-mod matcher chains behind it in the same
--spec-type value — two context-backed drafters cannot share one process,
so this chain is the only shape that carries both.

KV is q8_0 (16 KiB/token/layer vs 32 at f16) and CTX_SIZE is unset so
--fit sizes the pool against what the 7.4 GiB of payloads leave on the
24 GiB card. build-cuda was rebuilt with -DGGML_CUDA_FA_ALL_QUANTS=ON,
which quantized KV on CUDA requires. The three seams that pinned the Qwen
contract (MTP-in-file, the Qwen mmproj name, the f16 KV arithmetic) now
pin the Gemma4 contract instead, and the deploy handler's presence check
points at the new payload.
2026-09-17 01:59:10 -07:00

110 lines
5.0 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: the frank-small mainline-llama.cpp root unit on dankclaw.
# frank-small is the RTX 3090 endpoint created on 2026-09-16, when frank-large
# went back to the halogen engine on the iGPU. This seam pins what the flags
# alone do not say: boot persistence for a NATIVE runtime (no generated quadlet
# is involved), the repo-relative paths the unit executes, the separate client
# port, the MTP contract (the head is a separate sidecar file loaded as the
# draft model, chained behind ngram-mod), the vision handoff from frank-large,
# and the KV budget: q8_0 quantized KV whose context --fit sizes to the card.
load helpers
UNIT="$REPO_ROOT/scripts/frank-small.service"
SERVE="$REPO_ROOT/scripts/serve-frank-small.sh"
ENV_FILE="$REPO_ROOT/scripts/frank-small.env"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
QUADLET_LARGE="$REPO_ROOT/dankclaw/frank-large/quadlet/frank-large.container"
@test "frank-small unit, serve script and env exist in scripts/" {
[ -f "$UNIT" ]
[ -f "$SERVE" ]
[ -f "$ENV_FILE" ]
}
@test "frank-small 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"
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
}
@test "frank-small runs the repo's own serve script with the repo's env" {
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-small.sh$' "$UNIT"
grep -q 'FRANK_SMALL_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-small.env' "$UNIT"
}
@test "frank-small offloads every layer to the 3090 with flash attention" {
grep -q 'llama.cpp-mainline/build-cuda/bin/llama-server' "$ENV_FILE"
grep -q '^NGL=999$' "$ENV_FILE"
grep -q -- '--gpu-layers "$NGL"' "$SERVE"
grep -q '^FLASH_ATTN=on$' "$ENV_FILE"
grep -q -- '--flash-attn "$FLASH_ATTN"' "$SERVE"
# Prometheus scrapes this endpoint; without --metrics there is no /metrics.
grep -q -- '--metrics' "$SERVE"
}
@test "frank-small drafts with the MTP sidecar chained behind ngram-mod" {
grep -q '^SPEC_TYPE=draft-mtp,ngram-mod$' "$ENV_FILE"
grep -q -- '--spec-type "$SPEC_TYPE"' "$SERVE"
# The Gemma4 MTP head is a separate assistant file (gemma4-assistant, 4
# nextn layers); it loads as --model-draft and shares the target's last
# layers' KV. ngram-mod is draftless, so the chain is legal.
grep -q '^DRAFT_FILE=mtp-gemma-4-12B-it\.gguf$' "$ENV_FILE"
grep -q -- '--model-draft "$MODEL_DIR/$DRAFT_FILE"' "$SERVE"
}
@test "frank-small carries the fleet's image path, and only it" {
grep -q '^MMPROJ_PATH=.*mmproj-Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced-BF16\.gguf$' "$ENV_FILE"
grep -q -- '--mmproj "$MMPROJ_PATH"' "$SERVE"
! grep -q 'HALOGEN_VISION_TOWER' "$QUADLET_LARGE"
}
@test "frank-small quantizes KV to q8 and lets --fit size the context" {
local parallel cache_k cache_v ctx
parallel="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
cache_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
cache_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
[ -n "$parallel" ] && [ -n "$cache_k" ] && [ -n "$cache_v" ]
[ "$parallel" -ge 1 ]
# q8_0 KV is what fits the 24 GiB card beside 7.4 GiB of Q4_K_M weights +
# mmproj + sidecar: Gemma4's 16 kv heads x 512 dim cost 16 KiB per token
# per layer at q8, and the model's sliding-window pattern keeps the pool
# far below full-ctx x all-layers. The engine itself sizes the context:
# CTX_SIZE is unset, so the serve script passes --fit on.
[ -z "$ctx" ]
[ "$cache_k" = "q8_0" ] && [ "$cache_v" = "q8_0" ]
grep -q -- '--fit on' "$SERVE"
# Quantized KV on CUDA needs the GGML_CUDA_FA_ALL_QUANTS build option,
# without which prefill collapses about 12x (see the CUDA lesson in
# AGENTS.md). The build-cuda tree must carry it.
grep -q '^FLASH_ATTN=on$' "$ENV_FILE"
}
@test "frank-small runs on its own port beside frank-large's" {
grep -q '^LISTEN_PORT=7771$' "$ENV_FILE"
grep -q '^ALIAS=frank-small$' "$ENV_FILE"
grep -q -- '--port "$LISTEN_PORT"' "$SERVE"
grep -q -- '--host "$BIND_ADDR"' "$SERVE"
# The two endpoints cannot share a port, and only the halogen unit publishes
# 7770.
grep -q '^PublishPort=7770:8731$' "$QUADLET_LARGE"
! grep -q 'PublishPort=.*7771' "$QUADLET_LARGE"
}
@test "the deploy handler installs the root unit and refuses a missing model" {
grep -q 'deploy_llm_small' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-small' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-small.service' "$REMOTE_DEPLOY"
grep -q 'systemctl restart frank-small.service' "$REMOTE_DEPLOY"
grep -q 'Gemma4-12B-QAT' "$REMOTE_DEPLOY"
grep -q 'refusing to start a unit that cannot load' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
}
@test "frank-small's serve script refuses to start on a half-finished download" {
grep -q 'frank-small: missing' "$SERVE"
}