feat(frank-medium): cut the endpoint over to radiance TP=2

The conversation battery (scripts/bench-conversation.py, salted, same 10
prompts, 2026-09-20) measured three arms on the two R9700s:

  arm I  llama.cpp layer split 32:33 + DFlash2, Q8_0  geo decode  45.7 t/s, TTFT 0.72 s, burst-4  71.7 t/s
  arm L  llama.cpp tensor split 1:1 (RCCL), no drafter geo decode 30.5 t/s, TTFT 0.71 s, burst-4  54.3 t/s
  arm R  radiance vLLM TP=2 + DFlash2-FP8, MXFP4      geo decode  95.8 t/s, TTFT 0.21 s, burst-4 259.4 t/s

Radiance wins every axis, so it takes the alias. The board swap is what made
arm R possible: TP=2 was archived on 2026-09-16 because the second card sat
behind B650 chipset bridges with AtomicOpsCap: Routing-, and the ProArt
B850-Creator put both cards on CPU GPP root ports (Gen5 x8, Routing+, ReqEn+).
The R4D one-shot all-reduce needs no RCCL collective anyway.

  - un-retire dankserver/frank-medium/: the TP=2 quadlet (banner rewritten —
    it is the deployed shape now), radiance-entrypoint.sh, the healthcheck.
    The replica pair and its affinity proxy stay archived; one process owns
    both cards now.
  - retire scripts/frank-medium.{service,env} + serve-frank-medium.sh and
    bench-spec-ab.py to scripts/.AGENTTRASH/20260920-llama-layersplit/ as the
    rollback path. build-llama-rdna4.sh and download-frank-medium-gguf.sh
    stay: they are the tooling that rollback needs.
  - deploy-remote.sh: deploy_llm_medium now gates on the image (pull once,
    re-tag to the localhost name the quadlet resolves), the MXFP4 checkpoint,
    the drafter, the pinned patch clone and the patched libr4d; retires the
    llama root unit into the home trash instead of relinking it; then
    install_quadlet + wait_container_healthy 1800s (first start compiles).
    retire_vllm_medium_quadlets becomes retire_vllm_replica_quadlets — the
    a/b/proxy trio only, since frank-medium is installed again.
  - planner + test-changed map dankserver/frank-medium/** to frank-medium
    and its seam; the scripts-trio mapping is gone with the files.
  - tests/frank_medium_unit.bats rewritten for the container contract: boot
    persistence of a GENERATED unit (no enable on this host's v1 generator),
    TP=2 with the atomics-era NCCL workarounds kept out, the DFlash2 depth vs
    the MXFP4 decode-kernel row band, the KV pin's coupling to the shape it
    was calibrated at plus pool/VRAM arithmetic, the endpoint/tool/reasoning
    contract, and the archive invariants.
  - homepage card + docs updated, and they were stale twice over: they still
    described the ajgazin Swift 35:30 / 393k payload, not the deployed shape.

Known regression, documented in the unit: vision. The MXFP4 checkpoint is
language_model_only and the launcher loads no projector, so images are a 400
here; the harness's vision role already routes to frank-small.
This commit is contained in:
Frank committed 2026-09-20 18:15:27 -07:00
1 parent bcdd898750
commit c2116658f8
15 files changed
+345 -433

No files matched your search

+165 -323
View File
@@ -1,351 +1,193 @@
#!/usr/bin/env bats
# Seam: the frank-medium mainline-llama.cpp root unit on dankserver.
# frank-medium became a native llama.cpp service on 2026-09-16, replacing the
# vllm-radiance replica pair + routing proxy that served this endpoint for a
# day. What this seam pins is what the flags alone do not say: boot persistence
# for a NATIVE runtime (no generated quadlet is involved), that BOTH cards work
# on ONE model through a layer split with no collective in the path, that the
# DFlash2 drafter is wired with a depth its block size actually allows, that the
# runtime and payload are pinned rather than floated, and that one slot's KV
# still fits a 32 GiB card beside the weights it holds.
# Seam: the frank-medium vllm-radiance container on dankserver.
# frank-medium became a CONTAINER again on 2026-09-20, when the board swap put
# both R9700s on CPU root ports and a measured three-arm comparison retired the
# mainline-llama.cpp layer split (scripts/.AGENTTRASH/20260920-llama-layersplit/).
# What this seam pins is what the flags alone do not say: boot persistence for a
# GENERATED unit (this host's v1 quadlet generator cannot enable units, so the
# [Install] section is the only thing that survives a reboot), that BOTH cards
# work on ONE model through tensor parallelism — the thing the pre-2026-09-20
# hardware could not do — the KV pin's coupling to the batch shape it was
# calibrated at, and that the pool still holds one full-window request.
load helpers
UNIT="$REPO_ROOT/scripts/frank-medium.service"
SERVE="$REPO_ROOT/scripts/serve-frank-medium.sh"
ENV_FILE="$REPO_ROOT/scripts/frank-medium.env"
BUILD="$REPO_ROOT/scripts/build-llama-rdna4.sh"
FETCH="$REPO_ROOT/scripts/download-frank-medium-gguf.sh"
UNIT="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium.container"
ENTRY="$REPO_ROOT/dankserver/frank-medium/radiance-entrypoint.sh"
HEALTH="$REPO_ROOT/dankserver/frank-medium/frank-medium-healthcheck.py"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
LLAMA_TRASH="$REPO_ROOT/scripts/.AGENTTRASH/20260920-llama-layersplit"
REPLICA_TRASH="$REPO_ROOT/dankserver/.AGENTTRASH/20260916-vllm-radiance"
# Measured constants (rocm-smi / HF blob sizes), used by the fit arithmetic.
CARD_BYTES=34208743424 # one R9700 as rocm-smi reports it
MODEL_BYTES=29047084416 # orcarouter Q8_0 target
DRAFT_BYTES=1143006816 # DFlash2 Q4_K_M drafter
@test "frank-medium's unit, serve script, env and its build/fetch scripts exist" {
[ -f "$UNIT" ]
[ -f "$SERVE" ]
[ -f "$ENV_FILE" ]
[ -f "$BUILD" ]
[ -f "$FETCH" ]
# Measured constants, used by the fit arithmetic.
CARD_BYTES=34208743424 # one R9700 as rocm-smi reports it (32624 MiB)
KV_BYTES_PER_TOKEN=70656 # --kv-cache-memory 18563072000 => "262,720 tokens"
env_value() {
sed -nE "s/^Environment=$1=(-?[0-9]+)$/\1/p" "$UNIT" | head -n1
}
# GPU_UTIL is a fraction, not a count; the integer reader would drop it.
env_ratio() {
sed -nE "s/^Environment=$1=([0-9.]+)$/\1/p" "$UNIT" | head -n1
}
@test "frank-medium is boot-persistent, self-healing and unprivileged" {
grep -q '^WantedBy=multi-user.target$' "$UNIT"
@test "frank-medium's unit, entrypoint and healthcheck exist" {
[ -f "$UNIT" ]
[ -x "$ENTRY" ]
[ -f "$HEALTH" ]
}
@test "frank-medium is boot-persistent and self-healing" {
# Generated units cannot be enabled on this host, so WantedBy in the file is
# the whole boot story (AGENTS.md: Boot Persistence & Self-Healing).
grep -q '^WantedBy=default.target$' "$UNIT"
grep -q '^Restart=on-failure$' "$UNIT"
grep -q '^RestartSec=' "$UNIT"
grep -q '^User=dank$' "$UNIT"
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
# First start compiles Triton/inductor kernels before the engine loads ~9.5
# GiB per card; a shorter timeout kills a healthy boot.
grep -q '^TimeoutStartSec=1800$' "$UNIT"
grep -q '^HealthCmd=python3 /usr/local/bin/frank-medium-healthcheck.py$' "$UNIT"
grep -q '^HealthStartPeriod=1800s$' "$UNIT"
# Boots must not depend on the registry.
grep -q '^Pull=never$' "$UNIT"
}
@test "frank-medium runs the repo's own serve script with the repo's env" {
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-medium.sh$' "$UNIT"
grep -q 'FRANK_MEDIUM_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-medium.env' "$UNIT"
@test "both R9700s serve ONE model as one tensor-parallel process" {
grep -q '^Environment=TP=2$' "$UNIT"
grep -q '^Environment=ROCR_VISIBLE_DEVICES=0,1$' "$UNIT"
grep -q '^Environment=HIP_VISIBLE_DEVICES=0,1$' "$UNIT"
grep -q -- '--tensor-parallel-size "$TP"' "$ENTRY"
# The iGPU (index 2, 2 GiB) must stay out of the visible set: a third rank
# would take layers onto system RAM.
! grep -q '^Environment=.*VISIBLE_DEVICES=0,1,2' "$UNIT"
# TP needs the all-reduce to actually run. R4D is the collective-free path
# this host measured; the entrypoint must still select it, and the RCCL
# fallback must not be re-disabled by the pre-board-swap workarounds.
grep -q 'RADIANCE_USE_R4D_AR' "$ENTRY"
grep -q -- '--attention-backend "$ATTN"' "$ENTRY"
! grep -q 'NCCL_P2P_DISABLE' "$UNIT"
! grep -q 'RCCL_MSCCL_ENABLE' "$UNIT"
}
@test "both R9700s serve ONE model, layer-split inside one process" {
grep -q '^SPLIT_MODE=layer$' "$ENV_FILE"
grep -q '^DEVICES=ROCm0,ROCm1$' "$ENV_FILE"
# The split is a layer PAIR: the shares add up to the model's 65 blocks (one
# -ts unit per layer). The operator asked for the tensors BALANCED across
# the two cards (2026-09-19), which for 65 layers is 32:33 -- the most even
# split that exists, with ROCm1 (the last-layers card, which also carries
# the drafter and the output layers) holding the extra one. WHICH pair
# balances the cards is the fit test's job, which recomputes it from this
# file; pinning a literal here would only mean that re-measuring the
# balance breaks a test about parallelism.
local split_hi split_lo
split_hi="$(sed -nE 's/^TENSOR_SPLIT=([0-9]+),([0-9]+)$/\1/p' "$ENV_FILE")"
split_lo="$(sed -nE 's/^TENSOR_SPLIT=([0-9]+),([0-9]+)$/\2/p' "$ENV_FILE")"
[ -n "$split_hi" ] && [ -n "$split_lo" ]
[ $(( split_hi + split_lo )) -eq 65 ]
[ $(( split_lo - split_hi )) -le 1 ]
grep -q -- '--split-mode "$SPLIT_MODE"' "$SERVE"
grep -q -- '--device "$DEVICES"' "$SERVE"
grep -q -- '--tensor-split "$TENSOR_SPLIT"' "$SERVE"
# Tensor split is the one shape this host cannot run: it needs a cross-card
# collective, and the second card cannot dispatch one (see the env file).
! grep -q '^SPLIT_MODE=tensor$' "$ENV_FILE"
@test "HIP never reads one of these tunables as its own setting" {
# HIP reads bare TEMP, HOST and GPU_MAX_HEAP_SIZE (2026-08-13 incident:
# TEMP=1.0 sent the JIT cache to path "1.0" and segfaulted every load).
# Serve scripts export env files with `set -a`; a container inherits the
# same hazard from Environment=.
! grep -qE '^Environment=(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$UNIT"
}
@test "the iGPU is hidden so no layer can land on system RAM" {
grep -q '^HIP_VISIBLE_DEVICES=0,1$' "$ENV_FILE"
grep -q '^export HIP_VISIBLE_DEVICES$' "$SERVE"
@test "the drafter is DFlash2 at the measured depth and the kernel band covers the batch" {
grep -q '^Environment=SPEC_METHOD=dflash$' "$UNIT"
local maxseqs spec chunk
spec="$(env_value SPEC)"
maxseqs="$(env_value MAXSEQS)"
chunk="$(env_value CHUNK)"
[ -n "$spec" ] && [ -n "$maxseqs" ] && [ -n "$chunk" ]
# DFlash2 drafts at most 7 tokens per step; deeper is a different drafter's
# contract and costs acceptance, not just VRAM.
[ "$spec" -eq 7 ]
# The MXFP4 decode kernel band covers MAXSEQS x (SPEC+1) rows, and the
# shipped default covers 64. Exceeding it without the entrypoint's >8 bump
# is a launch failure at the worst moment (first concurrent request).
[ $(( maxseqs * (spec + 1) )) -le 64 ]
grep -q 'RADIANCE_MXFP4_DECODE_MAX_M' "$ENTRY"
# The chunk is the AR size gate's input (CHUNK x hidden x 2 bytes); a chunk
# larger than this row's silently changes which all-reduce path runs.
[ "$chunk" -le 8192 ]
}
@test "the drafter is the DFlash2 sidecar at the measured depth" {
# The DFlash2 sidecar is the deployed drafter (operator call 2026-09-19).
# n_max 7 is the measured DFlash2 winner -- 43.5 tok/s at 41 % acceptance
# against 32.4 for the in-file MTP head at n=3 (the env's table; identical
# greedy text at every depth, so this is pure speed). The target file
# also carries its own MTP head (nextn_predict_layers=1), so draft-mtp is
# one SPEC_TYPE change away; the drafter file stays on disk for that.
grep -qE '^SPEC_TYPE=draft-dflash$' "$ENV_FILE"
grep -q -- '--spec-type "$SPEC_TYPE"' "$SERVE"
local n_max
n_max="$(sed -nE 's/^SPEC_DRAFT_N_MAX=([0-9]+)$/\1/p' "$ENV_FILE")"
[ -n "$n_max" ] && [ "$n_max" -eq 7 ]
# The Q4_K_M rung is the operator's requested drafter quant -- the
# drafter's job is throughput, not output quality, and the target
# verifies every drafted token. ngram-mod's window is retained in the env
# for the chain, so it may not be dropped.
grep -q '^DRAFT_MODEL_FILE=Qwen3\.8-27B-DFlash2-Q4_K_M\.gguf$' "$ENV_FILE"
grep -q '^SPEC_NGRAM_N_MATCH=24$' "$ENV_FILE"
# ...and neither is loaded: ngram-mod's flags only go out when the type
# contains ngram, so a pure dflash configuration is not handed them.
python3 -c "
lines = open('$SERVE').read().splitlines()
guard = next(i for i, l in enumerate(lines) if '*ngram*' in l)
indent = len(lines[guard]) - len(lines[guard].lstrip())
end = next(i for i in range(guard + 1, len(lines))
if lines[i].strip() == 'fi' and len(lines[i]) - len(lines[i].lstrip()) == indent)
assert '--spec-ngram-mod-n-match' in chr(10).join(lines[guard:end]), 'ngram flags must sit in the ngram branch'
"
@test "the KV pin is valid only at the shape it was calibrated for" {
# The pin is the upstream kv-profiles.tsv row for sig 2x7551-32624 at
# maxseqs=8 chunk=8192 maxlen=262144 spec=dflash — exactly this host. It is
# not a general-purpose budget: changing MAXSEQS, CHUNK or SPEC_METHOD
# invalidates it, so the shape has to move with it or the pin goes to 0
# (let the image profile the shape).
local kv_mem maxseqs chunk maxlen spec
kv_mem="$(env_value KV_MEM)"
maxseqs="$(env_value MAXSEQS)"
chunk="$(env_value CHUNK)"
maxlen="$(env_value MAXLEN)"
spec="$(env_value SPEC)"
[ -n "$kv_mem" ] && [ -n "$maxseqs" ] && [ -n "$chunk" ] && [ -n "$maxlen" ]
grep -q 'kv-profiles.tsv' "$UNIT"
[ "$maxseqs" -eq 8 ] && [ "$chunk" -eq 8192 ] && [ "$spec" -eq 7 ]
grep -q '^Environment=SPEC_METHOD=dflash$' "$UNIT"
# Pool arithmetic, from the engine's own boot line: 18563072000 bytes
# reserved => "GPU KV cache size: 262,720 tokens". The pool must hold at
# least ONE full-window request or the endpoint refuses its own
# --max-model-len, and it must fit on one card beside the weights.
[ $(( kv_mem / KV_BYTES_PER_TOKEN )) -ge "$maxlen" ]
# 9.5 GiB of MXFP4 weights per card + the KV pool, under the 0.98 util cap.
local weights=10200547328 # ~9.5 GiB per card of the 19 GB MXFP4 checkpoint
local util util_pct floor
util="$(env_ratio GPU_UTIL)"
[ -n "$util" ]
util_pct="${util#0.}"
floor=$(( 1024 * 1024 * 1024 )) # 1 GiB per card
[ $(( weights + kv_mem / 2 )) -le $(( CARD_BYTES * util_pct / 100 - floor )) ]
}
@test "a draft MODEL is passed only for DFlash types, never for MTP" {
# MTP builds its context against the TARGET model (its head lives in that
# GGUF). Passing --spec-draft-model alongside it points the draft context at
# the drafter's file, which has no MTP layers, and the server dies at load:
# context type MTP requested but model doesn't contain MTP layers
# failed to create MTP context
# So the flag must appear exactly once, inside the dflash branch.
python3 -c "
lines = open('$SERVE').read().splitlines()
guard = next(i for i, l in enumerate(lines) if '*dflash*' in l)
indent = len(lines[guard]) - len(lines[guard].lstrip())
end = next(i for i in range(guard + 1, len(lines))
if lines[i].strip() == 'fi' and len(lines[i]) - len(lines[i].lstrip()) == indent)
block = chr(10).join(lines[guard:end])
assert '--spec-draft-model' in block, 'the dflash branch must pass the draft model'
assert sum(l.count('--spec-draft-model') for l in lines) == 1, 'only that branch may pass it'
print('draft-model wiring ok')
"
@test "the fleet endpoint contract survives the runtime swap" {
grep -q '^Environment=PORT=7770$' "$UNIT"
grep -q '^Environment=SERVED_NAMES=frank-medium$' "$UNIT"
grep -q -- '--port "$PORT"' "$ENTRY"
grep -q -- '--served-model-name $SERVED_NAMES' "$ENTRY"
# Host network: vLLM binds the port itself, so no PublishPort (and no
# pasta bind, which is what made the retired proxy's port replacement
# fail at container create).
grep -q -- '--network=host' "$UNIT"
! grep -q '^PublishPort=' "$UNIT"
# Tools and reasoning are part of the harness contract; both parsers must
# be selected, and thinking must land in its own field.
grep -q -- '--enable-auto-tool-choice' "$ENTRY"
grep -q -- '--tool-call-parser qwen3_coder' "$ENTRY"
grep -q -- '--reasoning-parser qwen3' "$ENTRY"
grep -q -- '--enable-prefix-caching' "$ENTRY"
# Requests that send no sampler fields get the predecessor's defaults.
grep -q '"temperature":0.7' "$ENTRY"
grep -q '"top_p":0.95' "$ENTRY"
}
@test "the runtime is pinned to a commit and to gfx1201 alone" {
# A floating ref would silently move the service between engine versions,
# and an unpinned arch would build for a sibling GPU that loads and then
# segfaults on these cards.
grep -Eq '^LLAMA_REF="\$\{LLAMA_REF:-[0-9a-f]{40}\}"$' "$BUILD"
grep -Eq '^HIP_ARCH="\$\{HIP_ARCH:-gfx1201\}"$' "$BUILD"
grep -q 'GGML_HIP=ON' "$BUILD"
grep -q 'frank-medium.env' "$SERVE"
@test "the healthcheck proves the engine serves, not merely that a process lives" {
grep -q '"/health"' "$HEALTH" || grep -q '/health' "$HEALTH"
grep -q 'status == 200' "$HEALTH"
# PORT-aware, and it must not hard-code a replica port.
grep -q 'os.environ.get("PORT"' "$HEALTH"
! grep -qE '7771|7772' "$HEALTH"
}
@test "the payload is fetched resumably and verified by size and sha256" {
grep -q 'Qwen3\.8-27B-Uncensored-Q8_0\.gguf' "$FETCH"
grep -q 'Qwen3\.8-27B-DFlash2-Q4_K_M\.gguf' "$FETCH"
grep -q -- '-fL -C -' "$FETCH"
grep -q 'verifying sizes and sha256' "$FETCH"
# The target repo is gated: the script must fail loud without a token
# rather than write a 401 body to disk and report success.
grep -q 'no HF token is available' "$FETCH"
}
@test "both cards stay balanced and keep the operator's 1.5 GiB floor" {
local ctx parallel cache_k cache_v split_hi split_lo batch ubatch
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
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")"
split_hi="$(sed -nE 's/^TENSOR_SPLIT=([0-9]+),([0-9]+)$/\1/p' "$ENV_FILE")"
split_lo="$(sed -nE 's/^TENSOR_SPLIT=([0-9]+),([0-9]+)$/\2/p' "$ENV_FILE")"
batch="$(sed -nE 's/^BATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
ubatch="$(sed -nE 's/^UBATCH_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
spec_type="$(sed -nE 's/^SPEC_TYPE=(.+)$/\1/p' "$ENV_FILE")"
[ -n "$ctx" ] && [ -n "$parallel" ] && [ -n "$cache_k" ] && [ -n "$cache_v" ] && [ -n "$spec_type" ]
[ -n "$split_hi" ] && [ -n "$split_lo" ] && [ -n "$batch" ] && [ -n "$ubatch" ]
[ "$parallel" -ge 1 ]
[ "$batch" -ge "$ubatch" ]
[ "$cache_k" = "f16" ] && [ "$cache_v" = "f16" ]
# Per card, calibrated against rocm-smi at the 35:30 / ctx 262144 / batch
# 6144 x ubatch 512 / two slots / DFlash2 point, where the two cards
# measured 6.44 / 6.90 GiB free. The fit reproduces card 1 to 0.00 GiB
# and card 0 to 0.86 GiB at that point; it is a GATE, not a meter -- the
# deployed point is re-measured on the host after every split/ctx/batch
# move (the current one is 32:33 / 327680 / DFlash2 Q4_K_M, the balanced
# split the operator asked for on 2026-09-19 -- see the TENSOR_SPLIT
# header in frank-medium.env).
#
# card 0 = its share of the weights and of the KV (qwen35 spends KV on
# its 16 full-attention layers only: 2 (K,V) x 4 heads x 256 dim
# x 2 B = 64 KiB per token at f16) plus ~1.4e6 B per ubatch unit
# of compute buffers
# card 1 = the same for its own share, plus the drafter. The drafter's
# charge depends on WHAT it is: DFlash2 is a sidecar file, so
# its weights (2.06e9 B) plus window-bounded KV (20480 B/pos x
# 2048) plus ~0.89 GiB of constant buffers; MTP's head lives in
# the target file (already charged above), so its cost is one
# nextn layer's KV (4096 B/position) plus small buffers. Its
# compute buffers are the heavier ones either way, because it
# holds the output layers and the drafter's graphs.
#
# BOTH ubatch coefficients are measured at the deployed shape, from the
# 1536 -> 512 move: 1.30 GiB on card 0 and 2.41 GiB on card 1. That
# asymmetry is why shrinking the batch moved the balance toward card 1 and
# cost card 0 a layer -- the split and the batch cannot be chosen apart.
#
# The layer it charges for is the AVERAGE (0.70 GiB: 31.46e9 of weights plus
# 16 GiB of KV over 65 layers). This stack is a hybrid, so real steps vary
# around that -- 0.43 GiB for a DeltaNet layer, 1.43 GiB for a full-attention
# one -- which makes the drift check below CONSERVATIVE: it overstates the gap
# a one-layer move creates, and so trips on moves a measurement might bless.
# That is the intent: moving the split means re-measuring, not re-arguing.
#
# The slack is the DEPLOYED gap, rounded up: at each shape the balance
# cannot be tuned finer than one layer, so the deployed split's gap IS
# what the check must accept, and a tighter slack would only reject it.
#
# What this model cannot see is CONTIGUITY -- the mechanism by which the
# DSpark-era 8192/2048 failed, a 2020 MiB pp buffer that found no room while
# every total looked fine. Total bytes are not the whole story for one big
# allocation.
local kv_pos=65536 draft_kv_pos=20480 mtp_kv_pos=4096 floor slack
local card_hi card_lo diff
local mmproj=931145984 # mmproj-Qwen3.8-27B-Uncensored-f16.gguf, on the primary device
# 1.5 GiB was the floor until the 2x196k shape: the 34:31 / 393216 /
# MTP point measured 28.91 / 30.78 GiB, leaving 2.95 / 1.08 GiB, so the
# floor is the measured 1.08 GiB rounded down. The 32:33 / 327680 /
# DFlash2 point is roomier than that (fit: 6.97 / 4.89 GiB free), so the
# floor stays at the measured 1.0 GiB -- if a large allocation fails at
# load, the next lever is a ctx cut, not a floor change.
floor=$(( 1024 * 1024 * 1024 )) # 1.0 GiB per card
# The deployed point (32:33 at ctx 327680 with DFlash2) has a fit gap of
# 2082 MiB -- the balanced split the operator asked for, not the
# tightest reachable one. The slack sits at 2150 MiB: it accepts the
# deployed split and rejects a two-layer re-skew (34:31 / 31:34 both
# move the fit gap well past it). The x16-lean 35:30 that served the
# predecessor payload (gap 0.58 GiB) no longer applies: the operator
# chose symmetry over the x1-card bottleneck.
slack=$(( 2150 * 1024 * 1024 ))
card_hi=$(( MODEL_BYTES * split_hi / (split_hi + split_lo) \
+ kv_pos * ctx * split_hi / (split_hi + split_lo) \
+ 381898377 \
+ mmproj \
+ 1363 * 1024 * ubatch ))
if [[ "$spec_type" == *dflash* || "$spec_type" == *dspark* ]]; then
card_lo=$(( MODEL_BYTES * split_lo / (split_hi + split_lo) \
+ DRAFT_BYTES \
+ kv_pos * ctx * split_lo / (split_hi + split_lo) \
+ draft_kv_pos * (ctx < 2048 ? ctx : 2048) \
+ 955046413 \
+ 2468 * 1024 * ubatch ))
else
# MTP: the head is in the target file (charged above); one nextn
# layer's KV plus buffers/graphs. The constant is CALIBRATED to the
# deployed point (2026-09-17, 34:31 / 393216): card 1 measured 30.78
# GiB, of which 4.20 GiB is MTP's total charge -- 1.61 GiB of nextn KV
# plus 2.70 GiB of buffers and graphs, far more than the 0.2 GiB
# estimate the DFlash2-era shape suggested.
card_lo=$(( MODEL_BYTES * split_lo / (split_hi + split_lo) \
+ kv_pos * ctx * split_lo / (split_hi + split_lo) \
+ mtp_kv_pos * ctx \
+ 2901231913 \
+ 2468 * 1024 * ubatch ))
fi
[ "$card_hi" -le $(( CARD_BYTES - floor )) ]
[ "$card_lo" -le $(( CARD_BYTES - floor )) ]
diff=$(( card_hi - card_lo )); [ "$diff" -ge 0 ] || diff=$(( -diff ))
[ "$diff" -le "$slack" ]
# ...and the window may not wander far past what the checkpoint was
# trained for (262144; the GGUF carries no rope scaling, so positions
# past that extrapolate on the base rope). The deployed 327680 pool is
# two 163840 slots -- entirely trained territory.
[ "$ctx" -le 393216 ]
}
@test "the host-RAM prompt cache can hold a full slot's state" {
# --cache-ram is the one cache tunable that is NOT VRAM: it caps the total
# bytes of saved prompt states in host RAM (server_prompt_cache, in
# tools/server/server-task.cpp). alloc() REFUSES any single state larger
# than the whole cap -- silently, with one WRN line -- so a budget below
# one slot's full context makes every deep conversation uncacheable and
# every turn past it re-prefill from scratch. That was the deployed state
# on 2026-09-18: 137 refusals of 11.0-11.4 GiB states against the 8192 MiB
# cap, in two days of journal.
#
# The state law at this shape, per token of sequence: 16 full-attention
# layers x 4 kv heads x 256 dim x 2 B x 2 (K,V) = 64 KiB, plus 4096 B for
# the MTP draft context's own KV. The DeltaNet state is constant-size and
# is left out of the sum -- which only makes this gate more conservative.
local ctx parallel cache_mib per_token slot_state host_mib
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
parallel="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
cache_mib="$(sed -nE 's/^CACHE_RAM=([0-9]+)$/\1/p' "$ENV_FILE")"
[ -n "$ctx" ] && [ -n "$parallel" ] && [ -n "$cache_mib" ]
per_token=$(( (16 * 4 * 256 * 2 * 2) + (4 * 256 * 2 * 2) ))
slot_state=$(( per_token * (ctx / parallel) ))
# one full slot must fit, or the cache is decorative
[ "$cache_mib" -ge $(( slot_state / 1024 / 1024 )) ]
# ...and this budget is host RAM: never more than half of dankserver's
# 62220 MiB, which the media-stack co-tenants and the page cache share.
host_mib=62220
[ "$cache_mib" -le $(( host_mib / 2 )) ]
# the arithmetic above is about nothing unless the flag goes out
grep -q -- '--cache-ram "$CACHE_RAM"' "$SERVE"
grep -q -- '--cache-prompt' "$SERVE"
}
@test "vision is enabled: the f16 projector loads with the model" {
grep -q '^NO_MMPROJ=0$' "$ENV_FILE"
grep -q '^MM_PROJ_FILE=mmproj-Qwen3\.8-27B-Uncensored-f16\.gguf$' "$ENV_FILE"
# Explicit flag, not autoload: this build starts text-only without it,
# and refuses to start without the projector file, so a missing file is
# a deploy error, not a silent text-only server.
grep -q -- '--mmproj "$MM_PROJ"' "$SERVE"
grep -q 'vision is enabled but the projector is missing' "$SERVE"
# And the download script fetches and byte-verifies it.
grep -q '^MM_PROJ_SIZE=931145984$' "$FETCH"
grep -q -- 'check "$MODEL_DIR/$MM_PROJ_FILE" "$MM_PROJ_SIZE"' "$FETCH"
}
@test "the fleet endpoint and the predecessor's sampler defaults survive" {
grep -q '^ALIAS=frank-medium$' "$ENV_FILE"
grep -q '^LISTEN_PORT=7770$' "$ENV_FILE"
grep -q -- '--alias "$ALIAS"' "$SERVE"
grep -q -- '--port "$LISTEN_PORT"' "$SERVE"
grep -q -- '--host "$BIND_ADDR"' "$SERVE"
# Prometheus scrapes this endpoint for LLM fleet metrics.
grep -q -- '--metrics' "$SERVE"
# Requests that send no sampler fields get these instead of an error.
grep -q '^TEMPERATURE=0.7$' "$ENV_FILE"
grep -q '^TOP_P=0.95$' "$ENV_FILE"
}
@test "the deploy handler installs the root unit and refuses a missing payload" {
@test "the deploy handler gates on host assets and installs the quadlet" {
grep -q 'deploy_llm_medium' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-medium' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-medium.service' "$REMOTE_DEPLOY"
grep -q 'systemctl restart frank-medium.service' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY"
grep -q 'build-llama-rdna4.sh' "$REMOTE_DEPLOY"
grep -q 'download-frank-medium-gguf.sh' "$REMOTE_DEPLOY"
grep -q '\[\[ "$missing" -eq 0 \]\] || return 1' "$REMOTE_DEPLOY"
}
@test "the deploy handler retires the vLLM quadlets and frees the port they held" {
grep -q 'retire_vllm_medium_quadlets' "$REMOTE_DEPLOY"
grep -q 'systemctl --user stop frank-medium-a.service frank-medium-b.service' "$REMOTE_DEPLOY"
grep -q 'install_quadlet frank-medium dankserver/frank-medium/quadlet frank-medium' "$REMOTE_DEPLOY"
grep -q 'wait_container_healthy frank-medium' "$REMOTE_DEPLOY"
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
# No image left to tag: the runtime is a native binary now.
# ...and no image work or quadlet install of the predecessor survives.
grep -q '\[\[ "$missing" -eq 0 \]\] || return 1' "$REMOTE_DEPLOY"
# The image is only pulled when absent, and re-tagged to the name the unit
# actually runs (the quadlet path cannot resolve registry refs at create).
grep -q 'podman image exists "$LLM_MEDIUM_IMAGE_LOCAL"' "$REMOTE_DEPLOY"
grep -q 'podman tag "$LLM_MEDIUM_IMAGE" "$LLM_MEDIUM_IMAGE_LOCAL"' "$REMOTE_DEPLOY"
# The patched libr4d is not optional (the image's own build NaNs this model).
grep -q 'patched libr4d absent' "$REMOTE_DEPLOY"
}
@test "a generated unit is never asked to enable, and the llama unit is gone" {
# podman 5.x v1 generator: generated units cannot be enabled; asking makes
# the deploy fail on a unit that is already boot-persistent via [Install].
! grep -q 'systemctl --user enable frank-medium' "$REMOTE_DEPLOY"
# ...and the root llama unit must be retired, not relinked: relinking would
# resurrect a service whose repo file no longer exists.
! grep -q 'relink_unit frank-medium' "$REMOTE_DEPLOY"
grep -q 'retire_llama_medium_unit' "$REMOTE_DEPLOY"
}
@test "the llama layer split and the replica trio are archived, not deleted" {
[ -f "$LLAMA_TRASH/frank-medium.service.llamacpp-layersplit" ]
[ -f "$LLAMA_TRASH/frank-medium.env.llamacpp-layersplit" ]
[ -f "$LLAMA_TRASH/serve-frank-medium.sh.llamacpp-layersplit" ]
[ ! -f "$REPO_ROOT/scripts/frank-medium.env" ]
[ -f "$REPLICA_TRASH/frank-medium-a.container" ]
[ -f "$REPLICA_TRASH/frank-medium-proxy.container" ]
# The replicas stay retired: the handler stops them but never installs them.
grep -q 'systemctl --user stop frank-medium-a.service frank-medium-b.service' "$REMOTE_DEPLOY"
! grep -q 'install_quadlet frank-medium-a' "$REMOTE_DEPLOY"
! grep -q 'podman tag docker.io/stilldeadcode' "$REMOTE_DEPLOY"
}
@test "the retired vLLM deployment is gone from the repo, not merely unmapped" {
[ ! -d "$REPO_ROOT/dankserver/frank-medium" ]
[ ! -f "$REPO_ROOT/tests/frank_medium_quadlet.bats" ]
# ...and the archive is present, so the two assertions above cannot pass
# because a path went missing rather than because it stayed missing.
[ -f "$REPO_ROOT/dankserver/.AGENTTRASH/20260916-vllm-radiance/frank-medium-a.container" ]
[ -f "$REPO_ROOT/dankserver/.AGENTTRASH/20260916-vllm-radiance/radiance-entrypoint.sh" ]
}
@test "the serve script refuses to start on an incomplete host" {
grep -q 'frank-medium: missing\|missing executable runtime\|missing or unreadable' "$SERVE"
grep -q 'missing or unreadable target model' "$SERVE"
grep -q 'missing or unreadable draft model' "$SERVE"
}