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
|
||||
`/home/dank/logs/frank_large.log` — **the boot ledger (KV pool, drafter state,
|
||||
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
|
||||
≈ 33.4 GiB, p1 weights / w4 activations / d2 KV) + `frank-large.tokenizer.gguf`
|
||||
(1,293,343 B). Cutover 2026-10-05 from the ROCmFPX llama.cpp fork
|
||||
`peonist-ai/halogen-qwen3.8-27b`: `qwen3.8-27b-p1w4d-d2.hgn` (35,865,565,184 B
|
||||
≈ 33.4 GiB, sha256 `274c3fc7…`, 1352 tensors; p1 weights / w4 activations /
|
||||
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),
|
||||
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.
|
||||
|
||||
Fetch: `scripts/download-frank-large-halogen.sh` into
|
||||
`/home/dank/models/halogen-qwen3.8-27b/` (`-f` + byte count + sha256 +
|
||||
`--verify-only`, no token on the command line). The deployer and serve script
|
||||
both refuse a **symlinked or directory tokenizer** — an HF snapshot layout
|
||||
passes `test -f` but fails the engine's open at request time.
|
||||
`/home/dank/models/halogen-qwen3.8-27b/` (`-f` + byte count + LFS sha256 +
|
||||
`--verify-only`, no token on the command line; resume goes against the
|
||||
RESOLVED CDN URL because curl drops `Range` across HF's 302). The deployer
|
||||
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`.
|
||||
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
|
||||
# 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
|
||||
# (not symlinked) file — a green deploy against a missing artifact would
|
||||
# the .hgn checkpoint at the exact byte count, the tokenizer directory
|
||||
# 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
|
||||
# same as every cutover since 2026-09-16: nothing starts until 7770 is
|
||||
# released.
|
||||
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_BYTES="${LLM_LARGE_CKPT_BYTES:-35880329102}"
|
||||
LLM_LARGE_TOKENIZER="${LLM_LARGE_TOKENIZER:-/home/dank/models/halogen-qwen3.8-27b/frank-large.tokenizer.gguf}"
|
||||
LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-35865565184}"
|
||||
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() {
|
||||
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
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$LLM_LARGE_TOKENIZER" || -L "$LLM_LARGE_TOKENIZER" ]]; then
|
||||
echo "ERROR: tokenizer missing or a symlink at $LLM_LARGE_TOKENIZER" >&2
|
||||
if [[ -L "$LLM_LARGE_TOK_DIR" || ! -d "$LLM_LARGE_TOK_DIR" ]]; then
|
||||
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
|
||||
return 1
|
||||
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
|
||||
# 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}"
|
||||
BASE="https://huggingface.co/peonist-ai/halogen-qwen3.8-27b/resolve/main"
|
||||
|
||||
FILES=(qwen3.8-27b-p1w4d-d2.hgn frank-large.tokenizer.gguf)
|
||||
# Byte sizes and sha256 (LFS oid) as published by the Hub on 2026-10-05 —
|
||||
# re-check with: curl -s "https://huggingface.co/api/models/peonist-ai/halogen-qwen3.8-27b/tree/main?recursive=true"
|
||||
SIZES=(35880329102 1293343)
|
||||
SHAS=(40874e7a5c27efcf1c83c44374db85726fa4086355b3c8aae1b363644d7e6234
|
||||
4c1ab3a19932f7d16eefc2c7fa60715c91133fe99c6dce683a3a7f11d463e7e9)
|
||||
FILES=(qwen3.8-27b-p1w4d-d2.hgn
|
||||
tokenizer/tokenizer.json
|
||||
tokenizer/vocab.json
|
||||
tokenizer/merges.txt
|
||||
tokenizer/chat_template.jinja
|
||||
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
|
||||
[ "${1:-}" = "--verify-only" ] && VERIFY_ONLY=1
|
||||
@@ -28,16 +35,26 @@ mkdir -p "$DEST"
|
||||
for i in "${!FILES[@]}"; do
|
||||
f="${FILES[$i]}"; want="${SIZES[$i]}"; sha="${SHAS[$i]}"
|
||||
path="$DEST/$f"
|
||||
mkdir -p "$(dirname "$path")"
|
||||
if [ "$VERIFY_ONLY" -eq 0 ]; then
|
||||
echo "FETCH $f -> $path"
|
||||
# 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"
|
||||
# A file already at the published size skips the fetch entirely:
|
||||
# resuming it would ask the CDN for a range past EOF (416 -> curl
|
||||
# error under set -e), and re-pulling 33.4 GiB to prove a point is
|
||||
# not what --verify-only is for. sha256 below still decides.
|
||||
if [ -f "$path" ] && [ "$(stat -c %s "$path")" = "$want" ]; then
|
||||
echo "HAVE $f ($want bytes) — skipping fetch"
|
||||
else
|
||||
echo "FETCH $f -> $path"
|
||||
# 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
|
||||
[ -f "$path" ] || { echo "MISSING $path (run without --verify-only)" >&2; exit 1; }
|
||||
got="$(stat -c %s "$path")"
|
||||
@@ -45,9 +62,13 @@ for i in "${!FILES[@]}"; do
|
||||
echo "OK $f ($got bytes)"
|
||||
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
|
||||
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" = "$sha" ] || { echo "SHA256 $f: $got != $sha" >&2; exit 1; }
|
||||
echo "OK $f sha256"
|
||||
|
||||
+17
-7
@@ -4,8 +4,9 @@
|
||||
# RUNTIME (2026-10-05, Frank): halogen — the closed-source C++/HIP engine
|
||||
# (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
|
||||
# NVFP4-derivative quant of the BASE Qwen/Qwen3.8-27B, as a rootless
|
||||
# container driven by scripts/serve-frank-large.sh. This CUTS OVER the
|
||||
# NVFP4-derivative quant of the BASE Qwen/Qwen3.8-27B (35,865,565,184 B,
|
||||
# 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
|
||||
# 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
|
||||
@@ -78,11 +79,20 @@ FRANK_LARGE_PUBLISH=7770:8731
|
||||
|
||||
FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-qwen3.8-27b
|
||||
FRANK_LARGE_CHECKPOINT=qwen3.8-27b-p1w4d-d2.hgn
|
||||
FRANK_LARGE_CHECKPOINT_BYTES=35880329102
|
||||
FRANK_LARGE_CHECKPOINT_SHA256=40874e7a5c27efcf1c83c44374db85726fa4086355b3c8aae1b363644d7e6234
|
||||
FRANK_LARGE_TOKENIZER=frank-large.tokenizer.gguf
|
||||
FRANK_LARGE_TOKENIZER_BYTES=1293343
|
||||
FRANK_LARGE_TOKENIZER_SHA256=4c1ab3a19932f7d16eefc2c7fa60715c91133fe99c6dce683a3a7f11d463e7e9
|
||||
FRANK_LARGE_CHECKPOINT_BYTES=35865565184
|
||||
FRANK_LARGE_CHECKPOINT_SHA256=274c3fc767fb57faf025dd03c76376b1fc1b32448aef45aaf02d1898fa962089
|
||||
# The tokenizer is a FLAT DIRECTORY, $FRANK_LARGE_MODEL_DIR/tokenizer, mounted
|
||||
# whole at /tokenizer — the model card ships `tokenizer/` and the engine's
|
||||
# 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
|
||||
# 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 —
|
||||
# serve gate and deploy gate disagreeing = green deploy, red unit):
|
||||
# image digest present; checkpoint present at the exact byte count;
|
||||
# tokenizer present, readable, and a FLAT REGULAR FILE — the engine repo
|
||||
# warns that a HF cache snapshot's symlinks into blobs/ dangle inside a
|
||||
# container; GPU device nodes present.
|
||||
# tokenizer directory present with every named file a FLAT REGULAR FILE —
|
||||
# the model card ships `tokenizer/` as a flat directory and the engine
|
||||
# defaults HALOGEN_TOKENIZER to /tokenizer, so a HF cache snapshot's
|
||||
# symlinks into blobs/ dangle inside the mount; GPU device nodes present.
|
||||
#
|
||||
# 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
|
||||
@@ -44,7 +44,7 @@ set +a
|
||||
: "${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_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_SLOT_CTX:?HALOGEN_SLOT_CTX must be set}"
|
||||
: "${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
|
||||
fi
|
||||
|
||||
# The tokenizer must be a regular file in a FLAT directory: pointing the
|
||||
# mount at a HuggingFace cache snapshot hands the container symlinks into
|
||||
# a sibling blobs/ that dangle inside the mount.
|
||||
TOK="$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_TOKENIZER"
|
||||
if [ ! -f "$TOK" ] || [ -L "$TOK" ]; then
|
||||
echo "frank-large: tokenizer $TOK missing or a symlink (HF cache snapshot?);" >&2
|
||||
# The payload ships the tokenizer as a FLAT DIRECTORY (model card:
|
||||
# "tokenizer/ flat tokenizer directory"). A HuggingFace cache snapshot
|
||||
# symlinks each file into a sibling blobs/, and those symlinks dangle
|
||||
# inside the container mount — so the directory must be real and every
|
||||
# named file must be a regular file, not a link.
|
||||
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
|
||||
exit 1
|
||||
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
|
||||
# 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 \
|
||||
-e HALOGEN_MODEL_ID="$HALOGEN_MODEL_ID" \
|
||||
-e HALOGEN_CHECKPOINT="/models/$FRANK_LARGE_CHECKPOINT" \
|
||||
-e HALOGEN_TOKENIZER="/tokenizer/$FRANK_LARGE_TOKENIZER" \
|
||||
-e HALOGEN_SLOT_CTX="$HALOGEN_SLOT_CTX" \
|
||||
-e HALOGEN_KV_SLOTS="${HALOGEN_KV_SLOTS:-1}" \
|
||||
-e HALOGEN_DRAFTER="$HALOGEN_DRAFTER" \
|
||||
@@ -122,7 +130,7 @@ podman run -d --name "$FRANK_LARGE_CONTAINER" \
|
||||
-e HALOGEN_QUEUE_TIMEOUT="${HALOGEN_QUEUE_TIMEOUT:-7200}" \
|
||||
${HALOGEN_CACHE_MB:+-e HALOGEN_CACHE_MB="$HALOGEN_CACHE_MB"} \
|
||||
-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" \
|
||||
> "$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
|
||||
pinned digest, launched by `scripts/serve-frank-large.sh` under a root
|
||||
systemd unit as a podman container — serving
|
||||
**`peonist-ai/halogen-qwen3.8-27b`** (`qwen3.8-27b-p1w4d-d2.hgn`, 35,880,329,102 B
|
||||
≈ 33.4 GiB) with the **DFlash2** drafter at **one slot x 200,700 ctx**.
|
||||
**`peonist-ai/halogen-qwen3.8-27b`** (`qwen3.8-27b-p1w4d-d2.hgn`,
|
||||
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]]),
|
||||
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
|
||||
was the Flash-Next w4b era, different image, retired.
|
||||
- **Payload:** `scripts/download-frank-large-halogen.sh` (fetch law: `curl -f`,
|
||||
exact byte count, LFS/hub sha256, `--verify-only`, token never in argv) into
|
||||
`/home/dank/models/halogen-qwen3.8-27b/`. The tokenizer bundle
|
||||
(`frank-large.tokenizer.gguf`) must be a FLAT regular file — the deployer
|
||||
and serve script both refuse a symlink or directory: an HF symlinked/snapshot
|
||||
layout passes `test -f` but fails the engine's open at request time.
|
||||
exact byte count, LFS sha256 where the Hub publishes one, `--verify-only`,
|
||||
token never in argv) into `/home/dank/models/halogen-qwen3.8-27b/`. The
|
||||
tokenizer is a FLAT DIRECTORY (`tokenizer/`, 5 files — tokenizer.json,
|
||||
vocab.json, merges.txt, chat_template.jinja, tokenizer_config.json),
|
||||
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
|
||||
`/etc/systemd/system/`), `ExecStart` = serve script, `ExecStop` = `podman
|
||||
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.
|
||||
good_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=(
|
||||
"FRANK_LARGE_ENV_FILE=$TEST_TMP/empty.env"
|
||||
"FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen@sha256:cafe"
|
||||
@@ -68,7 +69,7 @@ good_env() {
|
||||
"FRANK_LARGE_MODEL_DIR=$TEST_TMP"
|
||||
"FRANK_LARGE_CHECKPOINT=qwen.hgn"
|
||||
"FRANK_LARGE_CHECKPOINT_BYTES=0"
|
||||
"FRANK_LARGE_TOKENIZER=tok.gguf"
|
||||
"FRANK_LARGE_TOKENIZER_FILES=tok.json"
|
||||
"HALOGEN_MODEL_ID=frank-large"
|
||||
"HALOGEN_SLOT_CTX=200700"
|
||||
"HALOGEN_KV_SLOTS=1"
|
||||
@@ -83,6 +84,8 @@ good_env() {
|
||||
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 '^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_SLOT_CTX=[0-9]+$' "$ENV_FILE"
|
||||
}
|
||||
@@ -156,14 +159,15 @@ good_env() {
|
||||
[[ "$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
|
||||
# dangle inside the container mount.
|
||||
# dangle inside the container mount — even when the directory itself
|
||||
# is real.
|
||||
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"
|
||||
rm "$TEST_TMP/tokenizer/tok.json"
|
||||
ln -sf /nowhere/x "$TEST_TMP/tokenizer/tok.json"
|
||||
run env PATH="$TEST_TMP/bin:$PATH" "${GOOD_ENV[@]}" "$SERVE"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"missing or a symlink"* ]]
|
||||
}
|
||||
@@ -186,6 +190,7 @@ $(cat "$TEST_TMP/serve.log")"
|
||||
[[ "$output" == *"-e HALOGEN_DRAFTER=2"* ]]
|
||||
[[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]]
|
||||
[[ "$output" == *"--device $TEST_TMP/qwen.hgn"* ]]
|
||||
[[ "$output" == *"$TEST_TMP/tokenizer:/tokenizer:ro"* ]]
|
||||
[[ "$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.
|
||||
@@ -218,9 +223,9 @@ $(cat "$TEST_TMP/serve.log")"
|
||||
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"
|
||||
tokf="$(sed -nE 's/^FRANK_LARGE_TOKENIZER_FILES="(.*)"$/\1/p' "$ENV_FILE")"
|
||||
grep -qF "LLM_LARGE_TOK_DIR=\"\${LLM_LARGE_TOK_DIR:-$dir/tokenizer}\"" "$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")"
|
||||
grep -qF "LLM_LARGE_CKPT_BYTES=\"\${LLM_LARGE_CKPT_BYTES:-$bytes}\"" "$DEPLOYER"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user