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.
110 lines
5.0 KiB
Bash
Executable File
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"
|
|
}
|