Files
serverAppConfigs/tests/frank_small_unit.bats
T
Frank e5893dc30d frank-small: double the compute-buffer pair to 4096/2048
BATCH_SIZE 2048 -> 4096, UBATCH_SIZE 1024 -> 2048.

Why the ubatch move is right on THIS card: the prefill curve is
card-specific. On the 2x R9700 layer-split sibling it peaks at ubatch 512
and every step up costs GiB for no speed; on this CUDA 3090 it climbs
monotonically to 2048 (710 -> 1188 t/s on the 35B MoE sweep, 1944 -> 2423
t/s on the Ornith 8k sweep) with decode flat to under 1 t/s on every rung.
The logical batch is free either way: two sweeps agree that at a fixed
ubatch, 2048/4096/8192 give identical prefill speed and identical peak
VRAM, because the compute buffers are sized by the ubatch alone.

The risk is all in the ubatch, and it is documented rather than assumed:
the step cost ~1,244 MiB of peak graph VRAM on the 35B MoE, the shape this
replaces is 2,815 MiB free, and the Q5_K_M predecessor recorded "ubatch
2048 failed the scratch boot" with MORE free than that. Compute buffers
are allocated at graph capture during boot, so a failure is loud and fast;
the step-down rungs (4096/1536, then 4096/1024) are named in the env.

The seam stops hard-coding a rung: serve_argv now carries BATCH_SIZE and
UBATCH_SIZE the way it already carried CTX_SIZE et al., and the pair test
asserts the argv follows the shipped env plus the llama.cpp invariant
ubatch <= batch. Verified both ways: retuning the env to 8192/4096 keeps
the seam green, inverting the pair to 2048/4096 turns it red.
2026-10-05 15:10:15 -07:00

247 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bats
# Seam: the frank-small llama.cpp root unit on dankclaw.
# frank-small is the RTX 3090 endpoint; since 2026-10-01 night it serves
# bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF:Q6_K via a CUDA (sm_86) build of
# llama.cpp master, chosen because it is the only runtime here that moves K and
# V independently. Since 2026-10-03 it runs TWO unquantized contexts of
# 200,704 tokens each and carries a vision projector, and the card must keep
# 2-3 GiB free. 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-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"
ENV_LARGE="$REPO_ROOT/scripts/frank-large.env"
TARGET_STUB="MiMo-V2.6-Distill-Qwen-9B-Q6_K.gguf"
# Same basename as the shipped projector: the test env repoints it into $STAGE.
MM_STUB="mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf"
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"
touch "$MODEL_DIR/$TARGET_STUB" "$MODEL_DIR/$MM_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}"
local mmproj
mmproj="$(sed -nE 's/^MMPROJ_FILE=(.+)$/\1/p' "$ENV_FILE")"
cat > "$envf" <<EOF
LLAMA_BIN=$RUNTIME_DIR/bin/llama-server
MODEL_FILE=$MODEL_DIR/$TARGET_STUB
MMPROJ_FILE=$MODEL_DIR/${mmproj##*/}
ALIAS=frank-small
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")
NGL=$(sed -nE 's/^NGL=([0-9]+)$/\1/p' "$ENV_FILE")
EOF
run env FRANK_SMALL_ENV_FILE="$envf" nvidia-modprobe=true bash "$SERVE"
}
argv_value() {
grep -A1 -xF -- "$1" <<<"$output" | tail -1
}
@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"
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 "the shipped env points at the llama.cpp runtime and the Q6_K GGUF" {
grep -qE '^LLAMA_BIN=/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server$' "$ENV_FILE"
grep -qE "^MODEL_FILE=/home/dank/models/MiMo-Q6K/$TARGET_STUB$" "$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_SMALL_MODEL_FILE" "MMPROJ_FILE LLM_SMALL_MMPROJ"; do
read -r env_v dep_v <<<"$pair"
path="$(sed -nE "s/^${env_v}=(.+)\$/\1/p" "$ENV_FILE" | tail -1)"
[ -n "$path" ]
grep -qF -- "${dep_v}=\"\${${dep_v}:-${path}}\"" "$REMOTE_DEPLOY"
done
}
@test "the shipped env serves the fleet shape: two 200704-token contexts" {
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --alias)" = "frank-small" ]
[ "$(argv_value --port)" = "7771" ]
[ "$(argv_value --host)" = "0.0.0.0" ]
[ "$(argv_value --model)" = "$MODEL_DIR/$TARGET_STUB" ]
[ "$(argv_value --n-gpu-layers)" -gt 0 ]
# llama.cpp divides --ctx-size across slots and a mamba hybrid cannot
# share one cache, so the observable contract is the per-slot window.
[ "$(argv_value --parallel)" -eq 2 ]
ctx="$(argv_value --ctx-size)"
par="$(argv_value --parallel)"
[ $((ctx % par)) -eq 0 ]
[ $((ctx / par)) -eq 200704 ]
[ $((ctx / par)) -le 262144 ]
}
@test "the KV contract is unquantized both halves, flash-attn on" {
serve_argv
[ "$status" -eq 0 ]
[ "$(argv_value --cache-type-k)" = "f16" ]
# Frank, 2026-10-03: the values are unquantized too. The flag pair is what
# makes this observable — a build without the FA path silently falls back,
# so both must appear in the argv the supervisor runs.
[ "$(argv_value --cache-type-v)" = "f16" ]
[ "$(argv_value --flash-attn)" = "on" ]
}
@test "the KV budget leaves 2-3 GiB of the 3090 free" {
# Rate, from the GGUF geometry: 8 full-attention layers x 4 kv heads x
# 256 dim x 2 B = 16 KiB/token for K and the same for V => 32 KiB/token
# (budgeted 33 with slack). The non-KV half is MEASURED on this card on
# 2026-10-02: with the projector loaded the engine reported 22,972 MiB
# used against 14,113 MiB of KV, so weights + vision tower + DeltaNet
# state + compute buffers + CUDA graphs = 8,859 MiB => 9 GiB. Two asserts,
# both about the output the operator sees: the pool must still fit with
# 2 GiB spare, and it must be large enough that 5 GiB spare is NOT
# reachable (an undersized pool is as wrong as an oversized one).
# Arithmetic, not a boot log: change CTX_SIZE and this bites before the
# deploy does. The same pool with the OLD 24.5 KiB/token rate would leave
# ~6 GiB free — i.e. the unquantized V is what puts this shape in the band.
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
kv_gib=$((ctx * 33 / 1048576))
[ $((kv_gib + 9 + 2)) -le 24 ]
[ $((kv_gib + 9 + 5)) -gt 24 ]
# And the ceiling that sets the per-slot window: two NATIVE windows cannot
# be paid for in full precision on this card.
native=$((2 * 262144))
[ $((native * 33 / 1048576 + 9)) -gt 24 ]
}
@test "the compute-buffer pair follows the env file and stays self-consistent" {
# This endpoint ran on llama.cpp's own defaults until 2026-10-03, so 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"
serve_argv
[ "$status" -eq 0 ]
argv_value --batch-size | grep -qE '^[0-9]+$'
argv_value --ubatch-size | grep -qE '^[0-9]+$'
}
@test "no speculative draft ships by default" {
# The qwen35 GGUF has no MTP tensors, and the cross-model DFlash draft
# is a measured decision the harness makes later, not a default.
! grep -q -- '--model-draft' "$SERVE"
! grep -q -- '--spec-type' "$SERVE"
! grep -q '^DRAFT' "$ENV_FILE"
}
@test "vision ships by default and MMPROJ_FILE empty opts out of the image path" {
# The Q6_K GGUF carries no `v.*` tensors, so the projector argument IS
# the vision capability: no flag, no eyes.
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_SMALL_ENV_FILE="$STAGE/test.env" nvidia-modprobe=true bash "$SERVE"
[ "$status" -eq 0 ]
! grep -qxF -- '--mmproj' <<<"$output"
}
@test "the fleet reasoning contract survives the reslice to two wide contexts" {
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-small: missing"* ]]
rm -f "$RUNTIME_DIR/bin/llama-server"
serve_argv
[ "$status" -ne 0 ]
[[ "$output" == *"frank-small: missing"* ]]
}
@test "the deploy handler gates on binary, GGUF and projector, then MainPID-hardens health" {
grep -q 'deploy_llm_small' "$REMOTE_DEPLOY"
grep -q 'LLM_SMALL_RUNTIME' "$REMOTE_DEPLOY"
grep -q 'LLM_SMALL_MODEL_FILE' "$REMOTE_DEPLOY"
grep -q 'LLM_SMALL_MMPROJ' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-small' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-small.service' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
grep -q 'frank-small.service has no MainPID' "$REMOTE_DEPLOY"
}
@test "the two endpoints keep separate ports and the large handler never pkills llama-server" {
# frank-large's gufo sweep must not take down frank-small, whose
# process is llama-server on the SAME host since this swap.
grep -q '^LISTEN_PORT=7771$' "$ENV_FILE"
local large
large="$(sed -n '/^deploy_llm_large() {/,/^}/p' "$REMOTE_DEPLOY")"
! grep -q 'pkill -x llama-server' <<<"$large"
}