frank-large: correct the halogen payload contract to the Hub's truth

The first constants were wrong in three ways, found while resuming the
checkpoint (the file was COMPLETE; the manifest was not):
- checkpoint is 35,865,565,184 B / LFS sha256 274c3fc7... (not
  35,880,329,102 / 40874e7a...), 1352 tensors per the card;
- the tokenizer is a FLAT tokenizer/ DIRECTORY of 5 files mounted whole
  at /tokenizer (model-card docker-run parity; the engine's
  HALOGEN_TOKENIZER default is the directory) — there is no
  frank-large.tokenizer.gguf;
- plain git blobs (vocab/merges/jinja/config) carry no LFS oid, so the
  fetch script size-checks them and sha-verifies only the two LFS
  objects (checkpoint, tokenizer.json).

Gates flipped together (serve + deployer twins): tokenizer dir must be a
real directory and every named file a regular file, no symlinks (HF
snapshot links dangle in the mount). Fetch made idempotent: a file at
the published size skips the transfer (resuming it is a CDN 416 under
set -e). Seam rewritten to the same contract, 17/17.
This commit is contained in:
Frank
2026-10-05 16:45:12 -07:00
parent 87695c5126
commit bdbe814fac
7 changed files with 126 additions and 66 deletions
+9 -7
View File
@@ -315,9 +315,9 @@ stop -t 120 frank-large-halogen`, the script `exec podman wait` so the restart
policy is real) on port 7770 from the Strix Halo iGPU. Engine stdout goes to policy is real) on port 7770 from the Strix Halo iGPU. Engine stdout goes to
`/home/dank/logs/frank_large.log` — **the boot ledger (KV pool, drafter state, `/home/dank/logs/frank_large.log` — **the boot ledger (KV pool, drafter state,
any clamp) lives there, not in the journal**. Serving any clamp) lives there, not in the journal**. Serving
`peonist-ai/halogen-qwen3.8-27b`: `qwen3.8-27b-p1w4d-d2.hgn` (35,880,329,102 B `peonist-ai/halogen-qwen3.8-27b`: `qwen3.8-27b-p1w4d-d2.hgn` (35,865,565,184 B
≈ 33.4 GiB, p1 weights / w4 activations / d2 KV) + `frank-large.tokenizer.gguf` ≈ 33.4 GiB, sha256 `274c3fc7…`, 1352 tensors; p1 weights / w4 activations /
(1,293,343 B). Cutover 2026-10-05 from the ROCmFPX llama.cpp fork d2 KV) + the flat `tokenizer/` directory (5 files). Cutover 2026-10-05 from the ROCmFPX llama.cpp fork
([[frank-large-rocmfpx-q6agent-uncensored-mtp]], measured 27.6–28.1 t/s prose), ([[frank-large-rocmfpx-q6agent-uncensored-mtp]], measured 27.6–28.1 t/s prose),
chosen for the publisher's 31.7 t/s DFlash2 mean. chosen for the publisher's 31.7 t/s DFlash2 mean.
@@ -347,10 +347,12 @@ Qwen3.8 template rejects `reasoning_effort` outside `xhigh|medium|low`, so the
`models.yml` fold stays mandatory. `models.yml` fold stays mandatory.
Fetch: `scripts/download-frank-large-halogen.sh` into Fetch: `scripts/download-frank-large-halogen.sh` into
`/home/dank/models/halogen-qwen3.8-27b/` (`-f` + byte count + sha256 + `/home/dank/models/halogen-qwen3.8-27b/` (`-f` + byte count + LFS sha256 +
`--verify-only`, no token on the command line). The deployer and serve script `--verify-only`, no token on the command line; resume goes against the
both refuse a **symlinked or directory tokenizer** — an HF snapshot layout RESOLVED CDN URL because curl drops `Range` across HF's 302). The deployer
passes `test -f` but fails the engine's open at request time. and serve script both refuse a missing tokenizer file or **any symlink** —
an HF cache snapshot symlinks each entry into `blobs/` and those links
dangle inside the container mount.
Runbook: `serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md`. Runbook: `serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md`.
Rollback: revert the swap commit and redeploy — the ROCmFPX payload Rollback: revert the swap commit and redeploy — the ROCmFPX payload
+15 -6
View File
@@ -1102,15 +1102,17 @@ deploy_anubis() {
# #
# The gates mirror serve-frank-large.sh's predicates EXACTLY (the # The gates mirror serve-frank-large.sh's predicates EXACTLY (the
# 2026-10-01 crash-loop lesson): image digest in dank's podman storage, # 2026-10-01 crash-loop lesson): image digest in dank's podman storage,
# the .hgn checkpoint at the exact byte count, the tokenizer a regular # the .hgn checkpoint at the exact byte count, the tokenizer directory
# (not symlinked) file — a green deploy against a missing artifact would # holding every named file as a regular (not symlinked) file — a green
# deploy against a missing artifact would
# leave a restart-looping unit and a dark endpoint. The port gate is the # leave a restart-looping unit and a dark endpoint. The port gate is the
# same as every cutover since 2026-09-16: nothing starts until 7770 is # same as every cutover since 2026-09-16: nothing starts until 7770 is
# released. # released.
LLM_LARGE_IMAGE="${LLM_LARGE_IMAGE:-ghcr.io/peonist-ai/halogen@sha256:dc0a39a0016d6cfc58a197978febaafdf8d28403f724ded6111d98b5fb7ac0ea}" LLM_LARGE_IMAGE="${LLM_LARGE_IMAGE:-ghcr.io/peonist-ai/halogen@sha256:dc0a39a0016d6cfc58a197978febaafdf8d28403f724ded6111d98b5fb7ac0ea}"
LLM_LARGE_CKPT="${LLM_LARGE_CKPT:-/home/dank/models/halogen-qwen3.8-27b/qwen3.8-27b-p1w4d-d2.hgn}" LLM_LARGE_CKPT="${LLM_LARGE_CKPT:-/home/dank/models/halogen-qwen3.8-27b/qwen3.8-27b-p1w4d-d2.hgn}"
LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-35880329102}" LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-35865565184}"
LLM_LARGE_TOKENIZER="${LLM_LARGE_TOKENIZER:-/home/dank/models/halogen-qwen3.8-27b/frank-large.tokenizer.gguf}" LLM_LARGE_TOK_DIR="${LLM_LARGE_TOK_DIR:-/home/dank/models/halogen-qwen3.8-27b/tokenizer}"
LLM_LARGE_TOK_FILES="${LLM_LARGE_TOK_FILES:-tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json}"
retire_halogen_large_quadlet() { retire_halogen_large_quadlet() {
local trash="$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet" local trash="$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet"
@@ -1162,11 +1164,18 @@ deploy_llm_large() {
echo "frank-large: re-check: bash scripts/download-frank-large-halogen.sh --verify-only" >&2 echo "frank-large: re-check: bash scripts/download-frank-large-halogen.sh --verify-only" >&2
return 1 return 1
fi fi
if [[ ! -f "$LLM_LARGE_TOKENIZER" || -L "$LLM_LARGE_TOKENIZER" ]]; then if [[ -L "$LLM_LARGE_TOK_DIR" || ! -d "$LLM_LARGE_TOK_DIR" ]]; then
echo "ERROR: tokenizer missing or a symlink at $LLM_LARGE_TOKENIZER" >&2 echo "ERROR: tokenizer dir missing or a symlink at $LLM_LARGE_TOK_DIR" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2 echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2
return 1 return 1
fi fi
for tf in $LLM_LARGE_TOK_FILES; do
if [[ ! -f "$LLM_LARGE_TOK_DIR/$tf" || -L "$LLM_LARGE_TOK_DIR/$tf" ]]; then
echo "ERROR: tokenizer file missing or a symlink: $LLM_LARGE_TOK_DIR/$tf" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2
return 1
fi
done
retire_halogen_large_quadlet retire_halogen_large_quadlet
# The predecessor on a re-deploy is THIS unit: stop it (ExecStop stops # The predecessor on a re-deploy is THIS unit: stop it (ExecStop stops
+37 -16
View File
@@ -13,12 +13,19 @@ set -euo pipefail
DEST="${FRANK_LARGE_MODEL_DIR:-$HOME/models/halogen-qwen3.8-27b}" DEST="${FRANK_LARGE_MODEL_DIR:-$HOME/models/halogen-qwen3.8-27b}"
BASE="https://huggingface.co/peonist-ai/halogen-qwen3.8-27b/resolve/main" BASE="https://huggingface.co/peonist-ai/halogen-qwen3.8-27b/resolve/main"
FILES=(qwen3.8-27b-p1w4d-d2.hgn frank-large.tokenizer.gguf) FILES=(qwen3.8-27b-p1w4d-d2.hgn
# Byte sizes and sha256 (LFS oid) as published by the Hub on 2026-10-05 — tokenizer/tokenizer.json
# re-check with: curl -s "https://huggingface.co/api/models/peonist-ai/halogen-qwen3.8-27b/tree/main?recursive=true" tokenizer/vocab.json
SIZES=(35880329102 1293343) tokenizer/merges.txt
SHAS=(40874e7a5c27efcf1c83c44374db85726fa4086355b3c8aae1b363644d7e6234 tokenizer/chat_template.jinja
4c1ab3a19932f7d16eefc2c7fa60715c91133fe99c6dce683a3a7f11d463e7e9) tokenizer/tokenizer_config.json)
# Byte sizes as published by the Hub on 2026-10-05, plus sha256 (the LFS
# oid) where the Hub stores an LFS object — plain git blobs carry no oid
# and are size-checked only. Re-check with:
# curl -s "https://huggingface.co/api/models/peonist-ai/halogen-qwen3.8-27b/tree/main?recursive=true"
SIZES=(35865565184 12809320 6722759 3353259 8952 17928)
SHAS=(274c3fc767fb57faf025dd03c76376b1fc1b32448aef45aaf02d1898fa962089
0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3 - - - -)
VERIFY_ONLY=0 VERIFY_ONLY=0
[ "${1:-}" = "--verify-only" ] && VERIFY_ONLY=1 [ "${1:-}" = "--verify-only" ] && VERIFY_ONLY=1
@@ -28,16 +35,26 @@ mkdir -p "$DEST"
for i in "${!FILES[@]}"; do for i in "${!FILES[@]}"; do
f="${FILES[$i]}"; want="${SIZES[$i]}"; sha="${SHAS[$i]}" f="${FILES[$i]}"; want="${SIZES[$i]}"; sha="${SHAS[$i]}"
path="$DEST/$f" path="$DEST/$f"
mkdir -p "$(dirname "$path")"
if [ "$VERIFY_ONLY" -eq 0 ]; then if [ "$VERIFY_ONLY" -eq 0 ]; then
echo "FETCH $f -> $path" # A file already at the published size skips the fetch entirely:
# HF answers /resolve/ with a 302 to the Xet CDN and curl DROPS the # resuming it would ask the CDN for a range past EOF (416 -> curl
# Range header across redirects — a naive -C - then "resumes" by # error under set -e), and re-pulling 33.4 GiB to prove a point is
# pulling the ~1 KB redirect body (measured 2026-10-05: file frozen # not what --verify-only is for. sha256 below still decides.
# 14 MiB short, curl exit 0). Resolve the effective URL first, then if [ -f "$path" ] && [ "$(stat -c %s "$path")" = "$want" ]; then
# resume against the CDN, which honors Range. echo "HAVE $f ($want bytes) — skipping fetch"
url=$(curl -sfIL -o /dev/null -w '%{url_effective}' "$BASE/$f") \ else
|| { echo "cannot resolve $BASE/$f" >&2; exit 1; } echo "FETCH $f -> $path"
curl -fL --retry 5 -C - -o "$path" "$url" # HF answers /resolve/ with a 302 to the Xet CDN and curl DROPS
# the Range header across redirects — a naive -C - then
# "resumes" by pulling the ~1 KB redirect body (measured
# 2026-10-05: file frozen 14 MiB short, curl exit 0). Resolve
# the effective URL first, then resume against the CDN, which
# honors Range.
url=$(curl -sfIL -o /dev/null -w '%{url_effective}' "$BASE/$f") \
|| { echo "cannot resolve $BASE/$f" >&2; exit 1; }
curl -fL --retry 5 -C - -o "$path" "$url"
fi
fi fi
[ -f "$path" ] || { echo "MISSING $path (run without --verify-only)" >&2; exit 1; } [ -f "$path" ] || { echo "MISSING $path (run without --verify-only)" >&2; exit 1; }
got="$(stat -c %s "$path")" got="$(stat -c %s "$path")"
@@ -45,9 +62,13 @@ for i in "${!FILES[@]}"; do
echo "OK $f ($got bytes)" echo "OK $f ($got bytes)"
done done
echo "byte counts match; verifying sha256 (one pass over 33.4 GiB)..." echo "byte counts match; verifying sha256 where the Hub publishes an LFS oid..."
for i in "${!FILES[@]}"; do for i in "${!FILES[@]}"; do
f="${FILES[$i]}"; sha="${SHAS[$i]}" f="${FILES[$i]}"; sha="${SHAS[$i]}"
if [ "$sha" = "-" ]; then
echo "OK $f (plain git blob, size-checked)"
continue
fi
got="$(sha256sum "$DEST/$f" | cut -d' ' -f1)" got="$(sha256sum "$DEST/$f" | cut -d' ' -f1)"
[ "$got" = "$sha" ] || { echo "SHA256 $f: $got != $sha" >&2; exit 1; } [ "$got" = "$sha" ] || { echo "SHA256 $f: $got != $sha" >&2; exit 1; }
echo "OK $f sha256" echo "OK $f sha256"
+17 -7
View File
@@ -4,8 +4,9 @@
# RUNTIME (2026-10-05, Frank): halogen — the closed-source C++/HIP engine # RUNTIME (2026-10-05, Frank): halogen — the closed-source C++/HIP engine
# (github.com/peonist-ai/halogen-server, image ghcr.io/peonist-ai/halogen, # (github.com/peonist-ai/halogen-server, image ghcr.io/peonist-ai/halogen,
# digest-pinned below) serving peonist-ai/halogen-qwen3.8-27b, a ~6.32 bpw # digest-pinned below) serving peonist-ai/halogen-qwen3.8-27b, a ~6.32 bpw
# NVFP4-derivative quant of the BASE Qwen/Qwen3.8-27B, as a rootless # NVFP4-derivative quant of the BASE Qwen/Qwen3.8-27B (35,865,565,184 B,
# container driven by scripts/serve-frank-large.sh. This CUTS OVER the # 1352 tensors), as a podman container under a root unit, driven by
# scripts/serve-frank-large.sh. This CUTS OVER the
# 2026-10-02..10-03 ROCmFPX llama.cpp shape (uncensored # 2026-10-02..10-03 ROCmFPX llama.cpp shape (uncensored
# Q6_0_ROCMFPX_AGENT + MTP, 27.6-28.1 t/s): rollback = revert the swap # Q6_0_ROCMFPX_AGENT + MTP, 27.6-28.1 t/s): rollback = revert the swap
# commit's trio; the ROCmFPX payload, draft head and build all stay on # commit's trio; the ROCmFPX payload, draft head and build all stay on
@@ -78,11 +79,20 @@ FRANK_LARGE_PUBLISH=7770:8731
FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-qwen3.8-27b FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-qwen3.8-27b
FRANK_LARGE_CHECKPOINT=qwen3.8-27b-p1w4d-d2.hgn FRANK_LARGE_CHECKPOINT=qwen3.8-27b-p1w4d-d2.hgn
FRANK_LARGE_CHECKPOINT_BYTES=35880329102 FRANK_LARGE_CHECKPOINT_BYTES=35865565184
FRANK_LARGE_CHECKPOINT_SHA256=40874e7a5c27efcf1c83c44374db85726fa4086355b3c8aae1b363644d7e6234 FRANK_LARGE_CHECKPOINT_SHA256=274c3fc767fb57faf025dd03c76376b1fc1b32448aef45aaf02d1898fa962089
FRANK_LARGE_TOKENIZER=frank-large.tokenizer.gguf # The tokenizer is a FLAT DIRECTORY, $FRANK_LARGE_MODEL_DIR/tokenizer, mounted
FRANK_LARGE_TOKENIZER_BYTES=1293343 # whole at /tokenizer — the model card ships `tokenizer/` and the engine's
FRANK_LARGE_TOKENIZER_SHA256=4c1ab3a19932f7d16eefc2c7fa60715c91133fe99c6dce683a3a7f11d463e7e9 # HALOGEN_TOKENIZER default is that directory (the card's own docker run sets
# no tokenizer flag). The Hub stores it as 5 files (sizes from the Hub API,
# 2026-10-05): tokenizer.json 12809320 (LFS sha256
# 0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3),
# vocab.json 6722759, merges.txt 3353259, chat_template.jinja 8952,
# tokenizer_config.json 17928 (plain git blobs — size-checked, no LFS oid).
# The serve gate and the deployer refuse a missing file or ANY symlink: an
# HF cache snapshot symlinks each file into blobs/, and those links dangle
# inside the container mount.
FRANK_LARGE_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json"
# --- engine flags (docs/FLAGS.md of the engine repo; anything unset stays # --- engine flags (docs/FLAGS.md of the engine repo; anything unset stays
# at the image default, which is what was qualified) ------------------------ # at the image default, which is what was qualified) ------------------------
+21 -13
View File
@@ -16,10 +16,10 @@
# #
# Gates mirror the deployer EXACTLY (the 2026-10-01 crash-loop lesson — # Gates mirror the deployer EXACTLY (the 2026-10-01 crash-loop lesson —
# serve gate and deploy gate disagreeing = green deploy, red unit): # serve gate and deploy gate disagreeing = green deploy, red unit):
# image digest present; checkpoint present at the exact byte count; # tokenizer directory present with every named file a FLAT REGULAR FILE —
# tokenizer present, readable, and a FLAT REGULAR FILE — the engine repo # the model card ships `tokenizer/` as a flat directory and the engine
# warns that a HF cache snapshot's symlinks into blobs/ dangle inside a # defaults HALOGEN_TOKENIZER to /tokenizer, so a HF cache snapshot's
# container; GPU device nodes present. # symlinks into blobs/ dangle inside the mount; GPU device nodes present.
# #
# Why containers instead of the bare binary (the old frank-large shape): # Why containers instead of the bare binary (the old frank-large shape):
# the payload is .hgn and only halogen reads it; halogen ships as a # the payload is .hgn and only halogen reads it; halogen ships as a
@@ -44,7 +44,7 @@ set +a
: "${FRANK_LARGE_MODEL_DIR:?FRANK_LARGE_MODEL_DIR must name the payload dir}" : "${FRANK_LARGE_MODEL_DIR:?FRANK_LARGE_MODEL_DIR must name the payload dir}"
: "${FRANK_LARGE_CHECKPOINT:?FRANK_LARGE_CHECKPOINT must name the .hgn inside it}" : "${FRANK_LARGE_CHECKPOINT:?FRANK_LARGE_CHECKPOINT must name the .hgn inside it}"
: "${FRANK_LARGE_CHECKPOINT_BYTES:?FRANK_LARGE_CHECKPOINT_BYTES must pin the size}" : "${FRANK_LARGE_CHECKPOINT_BYTES:?FRANK_LARGE_CHECKPOINT_BYTES must pin the size}"
: "${FRANK_LARGE_TOKENIZER:?FRANK_LARGE_TOKENIZER must name the tokenizer file}" : "${FRANK_LARGE_TOKENIZER_FILES:?FRANK_LARGE_TOKENIZER_FILES must list the tokenizer dir contents}"
: "${HALOGEN_MODEL_ID:?HALOGEN_MODEL_ID must be the served alias (frank-large)}" : "${HALOGEN_MODEL_ID:?HALOGEN_MODEL_ID must be the served alias (frank-large)}"
: "${HALOGEN_SLOT_CTX:?HALOGEN_SLOT_CTX must be set}" : "${HALOGEN_SLOT_CTX:?HALOGEN_SLOT_CTX must be set}"
: "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set (1 keeps the drafter on)}" : "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set (1 keeps the drafter on)}"
@@ -76,15 +76,24 @@ if [ "$CKPT_SIZE" != "$FRANK_LARGE_CHECKPOINT_BYTES" ]; then
exit 1 exit 1
fi fi
# The tokenizer must be a regular file in a FLAT directory: pointing the # The payload ships the tokenizer as a FLAT DIRECTORY (model card:
# mount at a HuggingFace cache snapshot hands the container symlinks into # "tokenizer/ flat tokenizer directory"). A HuggingFace cache snapshot
# a sibling blobs/ that dangle inside the mount. # symlinks each file into a sibling blobs/, and those symlinks dangle
TOK="$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_TOKENIZER" # inside the container mount — so the directory must be real and every
if [ ! -f "$TOK" ] || [ -L "$TOK" ]; then # named file must be a regular file, not a link.
echo "frank-large: tokenizer $TOK missing or a symlink (HF cache snapshot?);" >&2 TOK_DIR="$FRANK_LARGE_MODEL_DIR/tokenizer"
if [ -L "$TOK_DIR" ] || [ ! -d "$TOK_DIR" ]; then
echo "frank-large: tokenizer dir $TOK_DIR missing or a symlink (HF cache snapshot?);" >&2
echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2 echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2
exit 1 exit 1
fi fi
for TF in $FRANK_LARGE_TOKENIZER_FILES; do
if [ ! -f "$TOK_DIR/$TF" ] || [ -L "$TOK_DIR/$TF" ]; then
echo "frank-large: tokenizer file $TOK_DIR/$TF missing or a symlink;" >&2
echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2
exit 1
fi
done
# GPU access is mandatory but the node paths are variables so the seam can # GPU access is mandatory but the node paths are variables so the seam can
# drive the gate on a box without an AMD GPU. # drive the gate on a box without an AMD GPU.
@@ -114,7 +123,6 @@ podman run -d --name "$FRANK_LARGE_CONTAINER" \
--security-opt seccomp=unconfined \ --security-opt seccomp=unconfined \
-e HALOGEN_MODEL_ID="$HALOGEN_MODEL_ID" \ -e HALOGEN_MODEL_ID="$HALOGEN_MODEL_ID" \
-e HALOGEN_CHECKPOINT="/models/$FRANK_LARGE_CHECKPOINT" \ -e HALOGEN_CHECKPOINT="/models/$FRANK_LARGE_CHECKPOINT" \
-e HALOGEN_TOKENIZER="/tokenizer/$FRANK_LARGE_TOKENIZER" \
-e HALOGEN_SLOT_CTX="$HALOGEN_SLOT_CTX" \ -e HALOGEN_SLOT_CTX="$HALOGEN_SLOT_CTX" \
-e HALOGEN_KV_SLOTS="${HALOGEN_KV_SLOTS:-1}" \ -e HALOGEN_KV_SLOTS="${HALOGEN_KV_SLOTS:-1}" \
-e HALOGEN_DRAFTER="$HALOGEN_DRAFTER" \ -e HALOGEN_DRAFTER="$HALOGEN_DRAFTER" \
@@ -122,7 +130,7 @@ podman run -d --name "$FRANK_LARGE_CONTAINER" \
-e HALOGEN_QUEUE_TIMEOUT="${HALOGEN_QUEUE_TIMEOUT:-7200}" \ -e HALOGEN_QUEUE_TIMEOUT="${HALOGEN_QUEUE_TIMEOUT:-7200}" \
${HALOGEN_CACHE_MB:+-e HALOGEN_CACHE_MB="$HALOGEN_CACHE_MB"} \ ${HALOGEN_CACHE_MB:+-e HALOGEN_CACHE_MB="$HALOGEN_CACHE_MB"} \
-v "$FRANK_LARGE_MODEL_DIR:/models:ro" \ -v "$FRANK_LARGE_MODEL_DIR:/models:ro" \
-v "$FRANK_LARGE_MODEL_DIR:/tokenizer:ro" \ -v "$FRANK_LARGE_MODEL_DIR/tokenizer:/tokenizer:ro" \
"$FRANK_LARGE_IMAGE" \ "$FRANK_LARGE_IMAGE" \
> "$LOG" 2>&1 > "$LOG" 2>&1
@@ -4,8 +4,9 @@ frank-large is the Strix Halo iGPU endpoint on Dankclaw (`:7770`). Since
2026-10-05 it runs the **halogen** engine — `ghcr.io/peonist-ai/halogen` at a 2026-10-05 it runs the **halogen** engine — `ghcr.io/peonist-ai/halogen` at a
pinned digest, launched by `scripts/serve-frank-large.sh` under a root pinned digest, launched by `scripts/serve-frank-large.sh` under a root
systemd unit as a podman container — serving systemd unit as a podman container — serving
**`peonist-ai/halogen-qwen3.8-27b`** (`qwen3.8-27b-p1w4d-d2.hgn`, 35,880,329,102 B **`peonist-ai/halogen-qwen3.8-27b`** (`qwen3.8-27b-p1w4d-d2.hgn`,
≈ 33.4 GiB) with the **DFlash2** drafter at **one slot x 200,700 ctx**. 35,865,565,184 B ≈ 33.4 GiB, sha256 `274c3fc7…`, 1352 tensors) with the
**DFlash2** drafter at **one slot x 200,700 ctx**.
It replaced the ROCmFPX llama.cpp fork endpoint ([[frank-large-rocmfpx-q6agent-uncensored-mtp]]), It replaced the ROCmFPX llama.cpp fork endpoint ([[frank-large-rocmfpx-q6agent-uncensored-mtp]]),
which remains the rollback target with its payload on disk. which remains the rollback target with its payload on disk.
@@ -76,11 +77,15 @@ bearing numbers:
storage before dispatch. NOT the 2026-09 `halogen-flash-server` repo — that storage before dispatch. NOT the 2026-09 `halogen-flash-server` repo — that
was the Flash-Next w4b era, different image, retired. was the Flash-Next w4b era, different image, retired.
- **Payload:** `scripts/download-frank-large-halogen.sh` (fetch law: `curl -f`, - **Payload:** `scripts/download-frank-large-halogen.sh` (fetch law: `curl -f`,
exact byte count, LFS/hub sha256, `--verify-only`, token never in argv) into exact byte count, LFS sha256 where the Hub publishes one, `--verify-only`,
`/home/dank/models/halogen-qwen3.8-27b/`. The tokenizer bundle token never in argv) into `/home/dank/models/halogen-qwen3.8-27b/`. The
(`frank-large.tokenizer.gguf`) must be a FLAT regular file — the deployer tokenizer is a FLAT DIRECTORY (`tokenizer/`, 5 files — tokenizer.json,
and serve script both refuse a symlink or directory: an HF symlinked/snapshot vocab.json, merges.txt, chat_template.jinja, tokenizer_config.json),
layout passes `test -f` but fails the engine's open at request time. mounted whole at `/tokenizer`; the engine's `HALOGEN_TOKENIZER` default is
that directory, so the container gets no tokenizer flag (model-card
docker-run parity). The deployer and serve script refuse a missing file or
ANY symlink: an HF cache snapshot symlinks each entry into `blobs/` and
those links dangle inside the container mount.
- **Unit:** `scripts/frank-large.service` (root, `User=dank`, relinked into - **Unit:** `scripts/frank-large.service` (root, `User=dank`, relinked into
`/etc/systemd/system/`), `ExecStart` = serve script, `ExecStop` = `podman `/etc/systemd/system/`), `ExecStart` = serve script, `ExecStop` = `podman
stop -t 120 frank-large-halogen`, script `exec podman wait` so stop -t 120 frank-large-halogen`, script `exec podman wait` so
+15 -10
View File
@@ -55,7 +55,8 @@ STUB
# stub agrees on and zero-byte artifacts passing the size gate. # stub agrees on and zero-byte artifacts passing the size gate.
good_env() { good_env() {
: > "$TEST_TMP/empty.env" : > "$TEST_TMP/empty.env"
touch "$TEST_TMP/qwen.hgn" "$TEST_TMP/tok.gguf" touch "$TEST_TMP/qwen.hgn"
mkdir -p "$TEST_TMP/tokenizer" && touch "$TEST_TMP/tokenizer/tok.json"
GOOD_ENV=( GOOD_ENV=(
"FRANK_LARGE_ENV_FILE=$TEST_TMP/empty.env" "FRANK_LARGE_ENV_FILE=$TEST_TMP/empty.env"
"FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen@sha256:cafe" "FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen@sha256:cafe"
@@ -68,7 +69,7 @@ good_env() {
"FRANK_LARGE_MODEL_DIR=$TEST_TMP" "FRANK_LARGE_MODEL_DIR=$TEST_TMP"
"FRANK_LARGE_CHECKPOINT=qwen.hgn" "FRANK_LARGE_CHECKPOINT=qwen.hgn"
"FRANK_LARGE_CHECKPOINT_BYTES=0" "FRANK_LARGE_CHECKPOINT_BYTES=0"
"FRANK_LARGE_TOKENIZER=tok.gguf" "FRANK_LARGE_TOKENIZER_FILES=tok.json"
"HALOGEN_MODEL_ID=frank-large" "HALOGEN_MODEL_ID=frank-large"
"HALOGEN_SLOT_CTX=200700" "HALOGEN_SLOT_CTX=200700"
"HALOGEN_KV_SLOTS=1" "HALOGEN_KV_SLOTS=1"
@@ -83,6 +84,8 @@ good_env() {
grep -qE '^FRANK_LARGE_PUBLISH=7770:8731$' "$ENV_FILE" grep -qE '^FRANK_LARGE_PUBLISH=7770:8731$' "$ENV_FILE"
grep -qE '^FRANK_LARGE_CHECKPOINT=.*\.hgn$' "$ENV_FILE" grep -qE '^FRANK_LARGE_CHECKPOINT=.*\.hgn$' "$ENV_FILE"
grep -qE '^FRANK_LARGE_CHECKPOINT_BYTES=[0-9]+$' "$ENV_FILE" grep -qE '^FRANK_LARGE_CHECKPOINT_BYTES=[0-9]+$' "$ENV_FILE"
grep -qE '^FRANK_LARGE_CHECKPOINT_SHA256=[0-9a-f]{64}$' "$ENV_FILE"
grep -qE '^FRANK_LARGE_TOKENIZER_FILES="tokenizer\.json( [a-z_.]+)+"$' "$ENV_FILE"
grep -qE '^HALOGEN_MODEL_ID=frank-large$' "$ENV_FILE" grep -qE '^HALOGEN_MODEL_ID=frank-large$' "$ENV_FILE"
grep -qE '^HALOGEN_SLOT_CTX=[0-9]+$' "$ENV_FILE" grep -qE '^HALOGEN_SLOT_CTX=[0-9]+$' "$ENV_FILE"
} }
@@ -156,14 +159,15 @@ good_env() {
[[ "$output" == *"expected 42"* ]] [[ "$output" == *"expected 42"* ]]
} }
@test "the serve script refuses a symlinked tokenizer" { @test "the serve script refuses a symlinked tokenizer file" {
# A HuggingFace cache snapshot's entries are symlinks into blobs/ and # A HuggingFace cache snapshot's entries are symlinks into blobs/ and
# dangle inside the container mount. # dangle inside the container mount — even when the directory itself
# is real.
podman_stub podman_stub
good_env good_env
ln -sf /nowhere/x "$TEST_TMP/link.gguf" rm "$TEST_TMP/tokenizer/tok.json"
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" \ ln -sf /nowhere/x "$TEST_TMP/tokenizer/tok.json"
FRANK_LARGE_TOKENIZER=link.gguf "$SERVE" run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" "$SERVE"
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
[[ "$output" == *"missing or a symlink"* ]] [[ "$output" == *"missing or a symlink"* ]]
} }
@@ -186,6 +190,7 @@ $(cat "$TEST_TMP/serve.log")"
[[ "$output" == *"-e HALOGEN_DRAFTER=2"* ]] [[ "$output" == *"-e HALOGEN_DRAFTER=2"* ]]
[[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]] [[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]]
[[ "$output" == *"--device $TEST_TMP/qwen.hgn"* ]] [[ "$output" == *"--device $TEST_TMP/qwen.hgn"* ]]
[[ "$output" == *"$TEST_TMP/tokenizer:/tokenizer:ro"* ]]
[[ "$output" == *"ghcr.io/peonist-ai/halogen@sha256:cafe"* ]] [[ "$output" == *"ghcr.io/peonist-ai/halogen@sha256:cafe"* ]]
# The unit's restart policy is real because the script becomes # The unit's restart policy is real because the script becomes
# `podman wait`: the container's exit code is the service's exit. # `podman wait`: the container's exit code is the service's exit.
@@ -218,9 +223,9 @@ $(cat "$TEST_TMP/serve.log")"
grep -qF "LLM_LARGE_IMAGE=\"\${LLM_LARGE_IMAGE:-$img}\"" "$DEPLOYER" grep -qF "LLM_LARGE_IMAGE=\"\${LLM_LARGE_IMAGE:-$img}\"" "$DEPLOYER"
dir="$(sed -nE 's/^FRANK_LARGE_MODEL_DIR=(.*)$/\1/p' "$ENV_FILE")" dir="$(sed -nE 's/^FRANK_LARGE_MODEL_DIR=(.*)$/\1/p' "$ENV_FILE")"
ckpt="$(sed -nE 's/^FRANK_LARGE_CHECKPOINT=(.*)$/\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")" tokf="$(sed -nE 's/^FRANK_LARGE_TOKENIZER_FILES="(.*)"$/\1/p' "$ENV_FILE")"
grep -qF "LLM_LARGE_CKPT=\"\${LLM_LARGE_CKPT:-$dir/$ckpt}\"" "$DEPLOYER" grep -qF "LLM_LARGE_TOK_DIR=\"\${LLM_LARGE_TOK_DIR:-$dir/tokenizer}\"" "$DEPLOYER"
grep -qF "LLM_LARGE_TOKENIZER=\"\${LLM_LARGE_TOKENIZER:-$dir/$tok}\"" "$DEPLOYER" grep -qF "LLM_LARGE_TOK_FILES=\"\${LLM_LARGE_TOK_FILES:-$tokf}\"" "$DEPLOYER"
bytes="$(sed -nE 's/^FRANK_LARGE_CHECKPOINT_BYTES=([0-9]+)$/\1/p' "$ENV_FILE")" 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" grep -qF "LLM_LARGE_CKPT_BYTES=\"\${LLM_LARGE_CKPT_BYTES:-$bytes}\"" "$DEPLOYER"
} }