diff --git a/AGENTS.md b/AGENTS.md index cac2a5b..c93233b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index dec27a3..6a94350 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -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 diff --git a/scripts/download-frank-large-halogen.sh b/scripts/download-frank-large-halogen.sh index ca6ff79..21cd074 100755 --- a/scripts/download-frank-large-halogen.sh +++ b/scripts/download-frank-large-halogen.sh @@ -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" diff --git a/scripts/frank-large.env b/scripts/frank-large.env index 13ce754..6f64197 100644 --- a/scripts/frank-large.env +++ b/scripts/frank-large.env @@ -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) ------------------------ diff --git a/scripts/serve-frank-large.sh b/scripts/serve-frank-large.sh index 5e4b5fb..a341492 100755 --- a/scripts/serve-frank-large.sh +++ b/scripts/serve-frank-large.sh @@ -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 diff --git a/serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md b/serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md index 4d04867..581f237 100644 --- a/serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md +++ b/serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md @@ -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 diff --git a/tests/frank_large_unit.bats b/tests/frank_large_unit.bats index 9dea3f7..bed8873 100755 --- a/tests/frank_large_unit.bats +++ b/tests/frank_large_unit.bats @@ -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" }