frank-large: cut over to halogen container (qwen3.8-27b .hgn + DFlash2)
Runtime swap ROCmFPX llama.cpp -> peonist-ai/halogen (image digest dc0a39a0, tag 0.1.4), serving qwen3.8-27b-p1w4d-d2.hgn (33.4 GiB, the .hgn format loads in no other engine) with HALOGEN_DRAFTER=2 (DFlash2) on ONE slot of 200700 ctx: slots>1 disables the drafter (engine law). Accepted: TEXT-ONLY (vision encoder ignored; frank-small owns images) and the censored base (rollback = revert this commit; payload, fork build and build-rocmfpx.sh pin stay on disk). - serve script: podman run under the root unit, exec podman wait so Restart=always is real; gates image digest, checkpoint byte-count, flat regular-file tokenizer, GPU nodes; engine stdout to log. - deploy handler: gates the artifacts, sweeps the legacy bare llama-server + old container name by NAME (never pkill llama-server: frank-small), retires the 2026-09 quadlet, port cutover 7770. - fetch script: -C - resume, -f, byte count, sha256, --verify-only. - seam rewritten (17 tests): halogen is now the deployed runtime.
This commit is contained in:
1 parent
aa9908257d
commit
fece3ffdd0
6 files changed
+552
-560
No files matched your search
+218
-231
@@ -1,265 +1,242 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: frank-large serves kingjones777's UNCENSORED Qwen3.8-27B
|
||||
# Q6_0_ROCMFPX_AGENT (a ftype-114 ROCmFPX file) with the repo's MTP draft head
|
||||
# on the Strix Halo iGPU, via the ROCmFPX fork of llama.cpp — one session at
|
||||
# the native 262,144 window.
|
||||
# Seam: frank-large serves peonist-ai/halogen-qwen3.8-27b (a .hgn halogen
|
||||
# checkpoint) on the Strix Halo iGPU through the halogen CONTAINER
|
||||
# (ghcr.io/peonist-ai/halogen, digest-pinned), driven by the root unit's
|
||||
# serve script — one conversation of 200,700 tokens with the DFlash2
|
||||
# drafter ON. 2026-10-05 cutover from the ROCmFPX llama.cpp shape.
|
||||
#
|
||||
# This seam exists because the format is the constraint: GGML_TYPE_Q6_0_ROCMFPX
|
||||
# (tensor type 102) and GGML_TYPE_Q8_0_ROCMFPX (103) are understood by the
|
||||
# ROCmFPX fork and by nothing else on this host, so the runtime, the payload
|
||||
# and the deployer's gate have to agree
|
||||
# on paths that no other endpoint uses. On 2026-10-01 a serve-script gate and
|
||||
# its deployer twin disagreed during a cutover and the endpoint crash-looped;
|
||||
# both sides are pinned here.
|
||||
# This seam exists because the artifacts are a triangle that must agree on
|
||||
# exact strings the engine reads: the image DIGEST, the checkpoint file
|
||||
# name AND byte count inside the /models mount, and the tokenizer's
|
||||
# flat-file shape (a HF cache snapshot's symlinks dangle inside the
|
||||
# container). A serve gate and a deployer gate that disagree is the
|
||||
# 2026-10-01 crash-loop; both sides are pinned here, plus the one engine
|
||||
# law the fleet must never "optimize" away: HALOGEN_KV_SLOTS=1 is what
|
||||
# keeps speculative decoding on.
|
||||
#
|
||||
# Everything is tested from what an operator or a supervisor can observe — the
|
||||
# command line, the exit code, the deployer's error text — never from anything
|
||||
# inside llama.cpp.
|
||||
# Everything is tested from what an operator or supervisor can observe —
|
||||
# the command line podman would run, the exit code, the deployer's error
|
||||
# text — never from anything inside the engine.
|
||||
|
||||
setup() {
|
||||
load helpers
|
||||
|
||||
SERVE="$REPO_ROOT/scripts/serve-frank-large.sh"
|
||||
UNIT="$REPO_ROOT/scripts/frank-large.service"
|
||||
ENV_FILE="$REPO_ROOT/scripts/frank-large.env"
|
||||
DEPLOYER="$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
BUILD="$REPO_ROOT/scripts/build-rocmfpx.sh"
|
||||
RUNTIME_STUB="/home/dank/GitPackages/kingjones-ROCmFPX/build/bin/llama-server"
|
||||
MODEL_STUB="/home/dank/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP/Qwen3.8-27B-Uncensored-Q6_0_ROCMFPX_AGENT.gguf"
|
||||
DRAFT_STUB="/home/dank/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP/mtp-Qwen3.8-27B-Q4_0.gguf"
|
||||
MMPROJ_STUB="/home/dank/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP/mmproj-Qwen3.8-27B-BF16.gguf"
|
||||
|
||||
ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
||||
SERVE="$ROOT/scripts/serve-frank-large.sh"
|
||||
ENV_FILE="$ROOT/scripts/frank-large.env"
|
||||
UNIT="$ROOT/scripts/frank-large.service"
|
||||
DEPLOYER="$ROOT/scripts/deploy-remote.sh"
|
||||
BUILD="$ROOT/scripts/build-rocmfpx.sh"
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
SRC="$TEST_TMP/src"
|
||||
mkdir -p "$SRC"
|
||||
git -C "$SRC" init -q
|
||||
git -C "$SRC" -c user.email=t -c user.name=t commit -q --allow-empty -m x
|
||||
touch "$TEST_TMP/empty.env"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_TMP"
|
||||
}
|
||||
|
||||
# make_stub — an executable that prints its whole argv on one line, so
|
||||
# "what command line would the supervisor run" is observable output.
|
||||
make_stub() {
|
||||
cat > "$TEST_TMP/bin" <<'STUB'
|
||||
#!/bin/sh
|
||||
printf '%s\n' "$*"
|
||||
# podman_stub — a fake podman on PATH that prints the argv of `run` and
|
||||
# `wait` and answers `images` with whatever digests the test names, so
|
||||
# "what would the supervisor run" is observable output and the digest
|
||||
# gate is testable without a 3.5 GB image.
|
||||
podman_stub() {
|
||||
mkdir -p "$TEST_TMP/bin"
|
||||
cat > "$TEST_TMP/bin/podman" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
images) printf '%s\n' "${STUB_DIGESTS:-}" ;;
|
||||
rm) exit 0 ;;
|
||||
run|wait) echo "PODMAN $*"; exit "${STUB_WAIT_RC:-0}" ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$TEST_TMP/bin"
|
||||
chmod +x "$TEST_TMP/bin/podman"
|
||||
}
|
||||
|
||||
@test "the env file defines the deployment values" {
|
||||
grep -qE '^LLAMA_BIN=/' "$ENV_FILE"
|
||||
grep -qE '^MODEL_FILE=/' "$ENV_FILE"
|
||||
grep -qE '^DRAFT_FILE=/' "$ENV_FILE"
|
||||
grep -qE '^ALIAS=[a-z0-9-]+$' "$ENV_FILE"
|
||||
grep -qE '^LISTEN_PORT=[0-9]+$' "$ENV_FILE"
|
||||
grep -qE '^CTX_SIZE=[0-9]+$' "$ENV_FILE"
|
||||
# good_env — the minimum env the serve script demands, with the digest
|
||||
# stub agrees on and zero-byte artifacts passing the size gate.
|
||||
good_env() {
|
||||
: > "$TEST_TMP/empty.env"
|
||||
touch "$TEST_TMP/qwen.hgn" "$TEST_TMP/tok.gguf"
|
||||
GOOD_ENV=(
|
||||
"FRANK_LARGE_ENV_FILE=$TEST_TMP/empty.env"
|
||||
"FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen@sha256:cafe"
|
||||
"FRANK_LARGE_CONTAINER=frank-large-halogen"
|
||||
"FRANK_LARGE_LEGACY_CONTAINERS=frank-large"
|
||||
"FRANK_LARGE_DEV_KFD=$TEST_TMP/qwen.hgn"
|
||||
"FRANK_LARGE_DEV_DRI=$TEST_TMP"
|
||||
"FRANK_LARGE_LOG=$TEST_TMP/serve.log"
|
||||
"FRANK_LARGE_PUBLISH=7770:8731"
|
||||
"FRANK_LARGE_MODEL_DIR=$TEST_TMP"
|
||||
"FRANK_LARGE_CHECKPOINT=qwen.hgn"
|
||||
"FRANK_LARGE_CHECKPOINT_BYTES=0"
|
||||
"FRANK_LARGE_TOKENIZER=tok.gguf"
|
||||
"HALOGEN_MODEL_ID=frank-large"
|
||||
"HALOGEN_SLOT_CTX=200700"
|
||||
"HALOGEN_KV_SLOTS=1"
|
||||
"HALOGEN_DRAFTER=2"
|
||||
"STUB_DIGESTS=localhost/x ghcr.io/peonist-ai/halogen@sha256:cafe"
|
||||
)
|
||||
}
|
||||
|
||||
@test "the serve script fails when the runtime is missing" {
|
||||
# Only the model, the head and the projector exist; the fork was never
|
||||
# built. llama.cpp is not reached, so this fails fast and says what to
|
||||
# run instead of failing at bind time.
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/nope/llama-server" \
|
||||
"MODEL_FILE=$TEST_TMP/model.gguf" "DRAFT_FILE=$TEST_TMP/draft.gguf" \
|
||||
"MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
@test "the env file defines the halogen deployment values" {
|
||||
grep -qE '^FRANK_LARGE_IMAGE=ghcr\.io/peonist-ai/halogen@sha256:[0-9a-f]{64}$' "$ENV_FILE"
|
||||
grep -qE '^FRANK_LARGE_CONTAINER=frank-large-halogen$' "$ENV_FILE"
|
||||
grep -qE '^FRANK_LARGE_PUBLISH=7770:8731$' "$ENV_FILE"
|
||||
grep -qE '^FRANK_LARGE_CHECKPOINT=.*\.hgn$' "$ENV_FILE"
|
||||
grep -qE '^FRANK_LARGE_CHECKPOINT_BYTES=[0-9]+$' "$ENV_FILE"
|
||||
grep -qE '^HALOGEN_MODEL_ID=frank-large$' "$ENV_FILE"
|
||||
grep -qE '^HALOGEN_SLOT_CTX=[0-9]+$' "$ENV_FILE"
|
||||
}
|
||||
|
||||
@test "the drafter stays on: slots must stay 1" {
|
||||
# halogen makes batching and speculation mutually exclusive — with
|
||||
# more than one slot the drafter is DISABLED and per-stream decode
|
||||
# falls to serial speed (the sibling engine measured 41 -> 14 t/s
|
||||
# when slots went up). frank-large was a one-session endpoint before
|
||||
# this swap too (PARALLEL=1), so slots=1 preserves the contract;
|
||||
# raising it is a product decision, never a default change.
|
||||
grep -qE '^HALOGEN_KV_SLOTS=1$' "$ENV_FILE"
|
||||
grep -qE '^HALOGEN_DRAFTER=2$' "$ENV_FILE"
|
||||
# The cap/timeout pair is coupled (FLAGS.md table): a request at the
|
||||
# cap must not outlast the queue timeout, or everyone queued behind
|
||||
# it eats a 503.
|
||||
grep -qE '^HALOGEN_MAX_TOKENS_CAP=[0-9]+$' "$ENV_FILE"
|
||||
grep -qE '^HALOGEN_QUEUE_TIMEOUT=[0-9]+$' "$ENV_FILE"
|
||||
}
|
||||
|
||||
@test "the serve script refuses when there is no GPU to serve on" {
|
||||
podman_stub
|
||||
good_env
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" \
|
||||
FRANK_LARGE_DEV_KFD="$TEST_TMP/absent-node" "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"build it with scripts/build-rocmfpx.sh"* ]]
|
||||
[[ "$output" == *"no gfx1151 to serve on"* ]]
|
||||
}
|
||||
|
||||
@test "the serve script refuses to start without the model file" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/absent.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"is not readable"* ]]
|
||||
[[ "$output" == *"download-frank-large-kingjones-uncensored.sh"* ]]
|
||||
@test "the KV budget fits unified memory with margin" {
|
||||
# Pool arithmetic is the engine's documented product: slots x
|
||||
# slot_ctx x 64 KiB. Against it: 33.4 GiB of mapped weights, the
|
||||
# engine working set + auto prompt cache (~25 GiB of headroom
|
||||
# allowed), host baseline and frank-small co-tenancy. The box has
|
||||
# 121 GiB unified; the 2026-09-16 halogen era ran the same engine
|
||||
# class here on a LARGER pool with less co-tenancy slack.
|
||||
ctx="$(sed -nE 's/^HALOGEN_SLOT_CTX=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||
slots="$(sed -nE 's/^HALOGEN_KV_SLOTS=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||
pool_gib=$(( slots * ctx * 65536 / 1073741824 ))
|
||||
total_gib=$(( 34 + pool_gib + 25 + 15 ))
|
||||
[ "$total_gib" -le 121 ]
|
||||
}
|
||||
|
||||
@test "the serve script refuses to start without the MTP draft head" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/absent.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
@test "the serve script refuses when the image is not in storage" {
|
||||
podman_stub
|
||||
good_env
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" STUB_DIGESTS="" "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"MTP draft head"* ]]
|
||||
[[ "$output" == *"not in podman storage"* ]]
|
||||
[[ "$output" == *"podman pull"* ]]
|
||||
}
|
||||
|
||||
@test "the serve script refuses a missing mmproj unless vision is opted out" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/absent.gguf" "$SERVE"
|
||||
@test "the serve script refuses a missing checkpoint and names the fetch script" {
|
||||
podman_stub
|
||||
good_env
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" \
|
||||
FRANK_LARGE_CHECKPOINT=absent.hgn "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"set MMPROJ_FILE='' for text-only"* ]]
|
||||
# The documented opt-out must actually start, without a --mmproj argument.
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=" "$SERVE"
|
||||
[[ "$output" == *"checkpoint"*"missing"* ]]
|
||||
[[ "$output" == *"download-frank-large-halogen.sh"* ]]
|
||||
}
|
||||
|
||||
@test "the serve script refuses a checkpoint of the wrong byte count" {
|
||||
# A truncated curl exit-0 artifact must not boot; the model card's
|
||||
# byte count is the contract (fleet fetch law).
|
||||
podman_stub
|
||||
good_env
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" \
|
||||
FRANK_LARGE_CHECKPOINT_BYTES=42 "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"expected 42"* ]]
|
||||
}
|
||||
|
||||
@test "the serve script refuses a symlinked tokenizer" {
|
||||
# A HuggingFace cache snapshot's entries are symlinks into blobs/ and
|
||||
# dangle inside the container mount.
|
||||
podman_stub
|
||||
good_env
|
||||
ln -sf /nowhere/x "$TEST_TMP/link.gguf"
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" \
|
||||
FRANK_LARGE_TOKENIZER=link.gguf "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"missing or a symlink"* ]]
|
||||
}
|
||||
|
||||
@test "the render names the container, the port map and the engine shape" {
|
||||
podman_stub
|
||||
good_env
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" "$SERVE"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"--mmproj"* ]]
|
||||
# `podman run` output is redirected to the log by design (the unit
|
||||
# journal must not absorb engine chatter); the stub's printed argv
|
||||
# lands there, so the render assertions read the same log.
|
||||
output="$output
|
||||
$(cat "$TEST_TMP/serve.log")"
|
||||
[[ "$output" == *"--name frank-large-halogen"* ]]
|
||||
[[ "$output" == *"--publish 7770:8731"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_MODEL_ID=frank-large"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_SLOT_CTX=200700"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_KV_SLOTS=1"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_DRAFTER=2"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]]
|
||||
[[ "$output" == *"--device $TEST_TMP/qwen.hgn"* ]]
|
||||
[[ "$output" == *"ghcr.io/peonist-ai/halogen@sha256:cafe"* ]]
|
||||
# The unit's restart policy is real because the script becomes
|
||||
# `podman wait`: the container's exit code is the service's exit.
|
||||
[[ "$output" == *"PODMAN wait frank-large-halogen"* ]]
|
||||
}
|
||||
|
||||
@test "the serve script fails when LLAMA_BIN is empty" {
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env LLAMA_BIN= "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"LLAMA_BIN is empty"* ]]
|
||||
@test "the shipped env pins the fleet's window and shape" {
|
||||
# 200700 is the per-conversation hard ceiling (a longer prompt is a
|
||||
# 400 naming the limit, never a silent truncation), and the harness
|
||||
# compaction math must fit prompt + budget under it: OMP compacts at
|
||||
# 70% = 140490, and 140490 + 57344 = 197834 <= 200700.
|
||||
grep -qE '^HALOGEN_SLOT_CTX=200700$' "$ENV_FILE"
|
||||
ctx=200700
|
||||
[ $(( ctx * 70 / 100 + 57344 )) -le "$ctx" ]
|
||||
}
|
||||
|
||||
@test "the shipped env points at the ROCmFPX runtime and the AGENT payload" {
|
||||
grep -qE "^LLAMA_BIN=$RUNTIME_STUB$" "$ENV_FILE"
|
||||
grep -qE "^MODEL_FILE=$MODEL_STUB$" "$ENV_FILE"
|
||||
grep -qE "^DRAFT_FILE=$DRAFT_STUB$" "$ENV_FILE"
|
||||
grep -qE "^MMPROJ_FILE=$MMPROJ_STUB$" "$ENV_FILE"
|
||||
# HIP reads bare HOST/TEMP/GPU_MAX_HEAP_SIZE out of the environment that
|
||||
# `set -a` creates; a tunable named HOST leaks into the runtime.
|
||||
@test "no HIP-sensitive bare names leak through the env file" {
|
||||
# HIP reads bare env vars out of a `set -a` source; a tunable named
|
||||
# HOST or TEMP used to send the JIT cache to path 1.0 and segfault
|
||||
# every load (kept from the llama.cpp era — the container gets only
|
||||
# the explicit -e list, but the host env still reaches podman itself).
|
||||
! grep -qE '^(HOST|TEMP|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE"
|
||||
}
|
||||
|
||||
@test "the argv contract keeps the fleet's port and alias" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" \
|
||||
ALIAS=frank-large BIND_ADDR=127.0.0.1 LISTEN_PORT=7770 "$SERVE"
|
||||
[ "$status" -eq 0 ]
|
||||
# The caller's values must survive the env file, or a single-run operator
|
||||
# override silently does nothing.
|
||||
[[ "$output" == *"--host 127.0.0.1"* ]]
|
||||
[[ "$output" == *"--port 7770"* ]]
|
||||
[[ "$output" == *"--alias frank-large"* ]]
|
||||
# Vision is wired: the projector is the whole vision tower.
|
||||
[[ "$output" == *"--mmproj $TEST_TMP/mmproj.gguf"* ]]
|
||||
@test "the deployer gates exactly the artifacts the serve script opens" {
|
||||
# Serve and deploy must name the SAME image digest, checkpoint and
|
||||
# tokenizer: the 2026-10-01 crash-loop was exactly these twins
|
||||
# disagreeing mid-cutover.
|
||||
img="$(sed -nE 's/^FRANK_LARGE_IMAGE=(.*)$/\1/p' "$ENV_FILE")"
|
||||
grep -qF "LLM_LARGE_IMAGE=\"\${LLM_LARGE_IMAGE:-$img}\"" "$DEPLOYER"
|
||||
dir="$(sed -nE 's/^FRANK_LARGE_MODEL_DIR=(.*)$/\1/p' "$ENV_FILE")"
|
||||
ckpt="$(sed -nE 's/^FRANK_LARGE_CHECKPOINT=(.*)$/\1/p' "$ENV_FILE")"
|
||||
tok="$(sed -nE 's/^FRANK_LARGE_TOKENIZER=(.*)$/\1/p' "$ENV_FILE")"
|
||||
grep -qF "LLM_LARGE_CKPT=\"\${LLM_LARGE_CKPT:-$dir/$ckpt}\"" "$DEPLOYER"
|
||||
grep -qF "LLM_LARGE_TOKENIZER=\"\${LLM_LARGE_TOKENIZER:-$dir/$tok}\"" "$DEPLOYER"
|
||||
bytes="$(sed -nE 's/^FRANK_LARGE_CHECKPOINT_BYTES=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||
grep -qF "LLM_LARGE_CKPT_BYTES=\"\${LLM_LARGE_CKPT_BYTES:-$bytes}\"" "$DEPLOYER"
|
||||
}
|
||||
|
||||
@test "the shipped argv is one full native window on one session" {
|
||||
grep -qE '^CTX_SIZE=262144$' "$ENV_FILE"
|
||||
grep -qE '^PARALLEL=1$' "$ENV_FILE"
|
||||
# 262144 IS the model's trained window (qwen35.context_length in the GGUF),
|
||||
# so 1 slot gets the whole thing — no truncation, no yarn scaling.
|
||||
#
|
||||
# Budget, from the GGUF header: 23.15 GiB of weights + 1.57 GiB draft head
|
||||
# + 0.87 GiB projector + 16 full-attention layers x 4 kv heads x 256 dim x
|
||||
# 2 (K+V) x 2 B = 64 KiB/token of f16 KV (~16 GiB at 262,144) + ~0.14 GiB
|
||||
# of DeltaNet state + a couple GiB of compute buffers, against the iGPU's
|
||||
# ~114 GiB GTT aperture. Change CTX_SIZE and this arithmetic, not a boot
|
||||
# log, is what tells you it no longer fits.
|
||||
ctx="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
|
||||
kv_gib=$((ctx * 65536 / 1073741824))
|
||||
total_gib=$((24 + 2 + 1 + kv_gib + 3))
|
||||
[ "$total_gib" -le 114 ]
|
||||
@test "the deployer points a missing artifact at the halogen fetch script" {
|
||||
grep -qE 'download-frank-large-halogen\.sh' "$DEPLOYER"
|
||||
grep -qE 'podman pull' "$DEPLOYER"
|
||||
}
|
||||
|
||||
@test "the MTP draft contract matches the model card's measured optimum" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
[ "$status" -eq 0 ]
|
||||
# The head is the model's own nextn layer, loaded as a separate draft
|
||||
# model because this checkpoint ships it standalone (no blk.<n>.nextn.*
|
||||
# tensors in the target file).
|
||||
[[ "$output" == *"--spec-type draft-mtp"* ]]
|
||||
[[ "$output" == *"--model-draft $TEST_TMP/draft.gguf"* ]]
|
||||
# n-max 4 is the measured optimum; n-max 8 measured WORSE (acceptance
|
||||
# fell to 0.757 and the extra verification cost outran the savings), so
|
||||
# the flag is a measurement, not a wish.
|
||||
[[ "$output" == *"--spec-draft-n-max 4"* ]]
|
||||
[[ "$output" == *"--spec-draft-n-min 0"* ]]
|
||||
[[ "$output" == *"--spec-draft-p-min 0.0"* ]]
|
||||
# The draft runs on the iGPU beside the target: no device-to-device hop
|
||||
# on a link that measures single-digit GB/s.
|
||||
[[ "$output" == *"--spec-draft-device ROCm0"* ]]
|
||||
[[ "$output" == *"--spec-draft-ngl 99"* ]]
|
||||
}
|
||||
|
||||
@test "KV and placement stay unquantized with flash attention on" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"--cache-type-k f16"* ]]
|
||||
[[ "$output" == *"--cache-type-v f16"* ]]
|
||||
# ROCmFPx quantized KV has no kernel on this branch, and the endpoint has
|
||||
# the aperture to keep KV unquantized anyway.
|
||||
[[ "$output" == *"--flash-attn on"* ]]
|
||||
# --fit's placement heuristics do not know that 311 tensors of type 103
|
||||
# belong on the iGPU, so the whole stack is placed explicitly instead.
|
||||
[[ "$output" == *"--fit off"* ]]
|
||||
[[ "$output" == *"--n-gpu-layers 999"* ]]
|
||||
}
|
||||
|
||||
@test "the client contract wires thinking and template kwargs" {
|
||||
make_stub
|
||||
touch "$TEST_TMP/model.gguf" "$TEST_TMP/draft.gguf" "$TEST_TMP/mmproj.gguf"
|
||||
run env "LLAMA_BIN=$TEST_TMP/bin" "MODEL_FILE=$TEST_TMP/model.gguf" \
|
||||
"DRAFT_FILE=$TEST_TMP/draft.gguf" "MMPROJ_FILE=$TEST_TMP/mmproj.gguf" "$SERVE"
|
||||
[ "$status" -eq 0 ]
|
||||
# Without --jinja, llama.cpp silently ignores the harness's
|
||||
# chat_template_kwargs, so thinking control disappears from the API.
|
||||
[[ "$output" == *"--jinja"* ]]
|
||||
# The harness reads `reasoning_content` for this endpoint, which is what
|
||||
# the deepseek emission format produces (vLLM's `reasoning` is not it).
|
||||
[[ "$output" == *"--reasoning-format deepseek"* ]]
|
||||
# The template needs an unbounded thinking budget.
|
||||
[[ "$output" == *"--reasoning-budget -1"* ]]
|
||||
}
|
||||
|
||||
@test "the deployer gates exactly the paths the serve script opens" {
|
||||
# A gate that checks a different file than the engine opens is the
|
||||
# 2026-10-01 crash-loop: deploy passed, the unit restart-looped. Read the
|
||||
# engine's own paths out of the env file and require the deployer's
|
||||
# defaults to name the same ones.
|
||||
for pair in "LLAMA_BIN:LLM_LARGE_RUNTIME" \
|
||||
"MODEL_FILE:LLM_LARGE_MODEL" \
|
||||
"DRAFT_FILE:LLM_LARGE_DRAFT" \
|
||||
"MMPROJ_FILE:LLM_LARGE_MMPROJ"; do
|
||||
env_key="${pair%%:*}"
|
||||
dep_key="${pair##*:}"
|
||||
want="$(sed -nE "s|^${env_key}=(.*)\$|\1|p" "$ENV_FILE")"
|
||||
[ -n "$want" ]
|
||||
got="$(grep -oE "${dep_key}:-[^}]+" "$DEPLOYER" | head -1 | sed "s|^${dep_key}:-||")"
|
||||
[ "$got" = "$want" ]
|
||||
done
|
||||
}
|
||||
|
||||
@test "the deployer points a missing runtime at the fork build script" {
|
||||
grep -qE 'build it: bash scripts/build-rocmfpx\.sh' "$DEPLOYER"
|
||||
grep -qE 'fetch it: bash scripts/download-frank-large-kingjones-uncensored\.sh' "$DEPLOYER"
|
||||
}
|
||||
|
||||
@test "the stray-process sweep cannot take frank-small down with frank-large" {
|
||||
# frank-small serves a different GGUF over llama-server on the SAME host.
|
||||
# A sweep keyed on the binary name (`pkill -f llama-server`) would kill
|
||||
# the 3090 endpoint too, so it is keyed on frank-large's own payload file.
|
||||
grep -q "pkill -f 'Qwen3.8-27B-Uncensored-Q6_0_ROCMFPX_AGENT'" "$DEPLOYER"
|
||||
@test "the stray sweep cannot take frank-small or the deployer down" {
|
||||
# frank-small also listens on this host (7771, llama-server) and the
|
||||
# deployer's own command line contains "frank-large"; only the FULL
|
||||
# container name matches a stray manual halogen run and nothing else.
|
||||
grep -q "pkill -f 'frank-large-halogen'" "$DEPLOYER"
|
||||
! grep -qE "pkill -f 'llama-server'" "$DEPLOYER"
|
||||
}
|
||||
|
||||
@test "the build script refuses a source tree that is not the pinned commit" {
|
||||
# The pin is load-bearing: the older builds of this fork line segfault in
|
||||
# ggml_compute_forward_set_rows on this exact payload. A runtime that
|
||||
# differs from what was qualified must be heard about at deploy time.
|
||||
wrong="$(git -C "$SRC" rev-parse HEAD)"
|
||||
run env ROCMFPX_SRC_DIR="$SRC" ROCMFPX_COMMIT="ffff$wrong" \
|
||||
"$BUILD" --verify
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"ROCMFPX_COMMIT"*mismatch* ]]
|
||||
}
|
||||
|
||||
@test "the build script pins a real commit and the gfx1151 target" {
|
||||
grep -qE 'ROCMFPX_COMMIT=.*[0-9a-f]{40}' "$BUILD"
|
||||
grep -qE 'GPU_TARGET=.*gfx1151' "$BUILD"
|
||||
grep -qE 'DGPU_TARGETS=' "$BUILD"
|
||||
# The webui asset fetch half-failed on this box and aborted the link at
|
||||
# 98%; the build must not depend on it.
|
||||
grep -qE 'DLLAMA_BUILD_WEBUI=OFF' "$BUILD"
|
||||
! grep -qE "pkill -f 'frank-large'" "$DEPLOYER"
|
||||
}
|
||||
|
||||
@test "the deployed unit keeps the fleet's restart and boot-persistence shape" {
|
||||
@@ -267,15 +244,25 @@ STUB
|
||||
grep -qE '^WantedBy=' "$UNIT"
|
||||
grep -qE '^ExecStart=.*/serve-frank-large\.sh$' "$UNIT"
|
||||
grep -qE '^User=dank$' "$UNIT"
|
||||
# The payload is 26 GiB of mmap'd weights plus pinned staging buffers; an
|
||||
# RLIMIT smaller than the pinned set fails the load in a way that looks
|
||||
# like an engine crash.
|
||||
grep -qE '^LimitMEMLOCK=infinity$' "$UNIT"
|
||||
# The service's lifetime is the container's; stopping the service
|
||||
# must stop the container by its real name.
|
||||
grep -qE '^ExecStop=.*podman stop .* frank-large-halogen$' "$UNIT"
|
||||
}
|
||||
|
||||
@test "the unit points at no retired frank-large runtime path" {
|
||||
# The host keeps every retired payload and build on disk, so a stale path
|
||||
# in a unit still resolves — to a runtime nobody has measured in weeks.
|
||||
@test "the unit runs the halogen runtime and no retired path" {
|
||||
grep -q 'halogen' "$UNIT"
|
||||
# Every retired payload/build stays on the host, so a stale path in
|
||||
# a unit still resolves — to a runtime nobody has measured in weeks.
|
||||
! grep -qE 'GitPackages/(ROCmFPX-kairic|llama.cpp-ik_llama|llama-builds|ciru-orca)' "$UNIT"
|
||||
! grep -q 'halogen' "$UNIT"
|
||||
! grep -qE 'kingjones-ROCmFPX' "$UNIT"
|
||||
}
|
||||
|
||||
@test "the rollback door stays qualified: the ROCmFPX build pin survives" {
|
||||
# Rolling back this swap means rebuilding the fork the old payload
|
||||
# needs; the pin (real commit, gfx1151, no webui fetch) is what makes
|
||||
# that door trustworthy months later.
|
||||
grep -qE 'ROCMFPX_COMMIT=.*[0-9a-f]{40}' "$BUILD"
|
||||
grep -qE 'GPU_TARGET=.*gfx1151' "$BUILD"
|
||||
grep -qE 'DLLAMA_BUILD_WEBUI=OFF' "$BUILD"
|
||||
}
|
||||
Reference in new issue
Block a user