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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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) ------------------------
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user