halogen-flash-server already ships an NPU path on the pin we run: for each
model named in HALOGEN_NPU_MODELS it loads the vendor's precompiled AIE
programs (XDNA2 executes only AIE-compiled graphs) and answers them on
/v1/embeddings inside the SAME container behind port 7770. That gives the
fleet an in-house embedding endpoint without touching the iGPU trunk.
The lane is opt-in and one line wide: HALOGEN_NPU_MODELS blank is the
pre-lane endpoint byte for byte (no device, no mounts, no engine env).
The three host preconditions the vendor names are gated in the serve script
and mirrored in the deployer (the 2026-10-01 twin law), each refused by name:
- the staged model files. /models is read-only and HALOGEN_DOWNLOAD stays
unset so the container opens no outbound connection at boot, so
scripts/download-frank-dragoon-npu.sh stages them from the pinned
revision with the catalog's own byte counts and sha256s (the engine
re-checks both at boot);
- the distro XRT's libxrt_coreutil/libxrt_core/libxrt_driver_xdna, each
mounted twice — where the image looks for XRT and at its own path —
because those libraries find each other where they were installed and
an /opt/xilinx/xrt of symlinks dangles inside the container;
- the GPU fabric clock HELD. On Strix Halo the iGPU and the NPU co-running
hang the machine (and corrupt NPU results) while FCLK changes speed, and
halogen refuses to start in that state; it cannot read host sysfs from
inside, so the hold is proven host-side. dankclaw/host/ carries the
vendor's halogen-fabric-clock{,.service} verbatim at tag v0.16.2 (sha256
a3a4f7f4e0142e71… / 5f9f1cc4c85a1c02…); the deployer installs, enables
and gates it only while the lane is named, and releases the clock plus
drops the boot hook when it is not.
After health, the deployer's lane gate is a real embedding through
/v1/embeddings — a 200 on /health proves nothing about the NPU programs.
Planner: dankclaw/host/halogen-fabric-clock* re-dispatches frank-dragoon
(the deployer installs it); dankclaw/host/grub and the fetch script plan
nothing.
Seam: 11 new tests — the off arm really renders nothing lane-specific, the
on arm renders the device/mounts/env, every missing precondition refuses by
name, the deployer twins and the host unit shape are pinned.
Co-Authored-By: omp <omp@dankclaw>
111 lines
5.8 KiB
Bash
Executable File
111 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# download-frank-dragoon-npu.sh — stage the NPU half of frank-dragoon: the
|
|
# vendor-precompiled AIE programs halogen-flash-server runs on the XDNA2 NPU
|
|
# when HALOGEN_NPU_MODELS names them. These are NOT model weights you can
|
|
# quantize yourself: XDNA2 executes only programs compiled by AMD's AIE
|
|
# compiler, so the only source is peonist-ai's per-model NPU repo, and the
|
|
# engine checks every file's size and sha256 against its own catalog at boot
|
|
# (/opt/halogen/npu/models.txt inside the image) and refuses to load a model
|
|
# whose files do not match.
|
|
#
|
|
# The manifest below IS that catalog block (dumped from the pinned 0.16.2
|
|
# image on 2026-10-19), so the files land in the layout the entrypoint
|
|
# expects — <dir>/<model-id>/devices/*, <dir>/<model-id>/<model-id>.hnpw and
|
|
# <dir>/<model-id>/tokenizer/tokenizer.json — and the endpoint can keep
|
|
# /models read-only and open no outbound connection at boot (HALOGEN_DOWNLOAD
|
|
# stays unset; the vendor's own README: with it unset the container makes no
|
|
# outbound connections at all).
|
|
#
|
|
# The fetch law, same as every sibling script: curl -f (an access error must
|
|
# not become a "successful" 153-byte artifact), the exact byte count and the
|
|
# sha256 from the manifest, --verify-only to re-check a staged set.
|
|
#
|
|
# Adding a second NPU model: read its block out of the image's catalog and add
|
|
# a case below —
|
|
# podman exec frank-dragoon-halogen grep "^file <model-id> " /opt/halogen/npu/models.txt
|
|
# podman exec frank-dragoon-halogen grep "^model <model-id> " /opt/halogen/npu/models.txt
|
|
# and add the model id to HALOGEN_NPU_MODELS in frank-dragoon.env.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}"
|
|
DEST="${FRANK_DRAGOON_NPU_DIR:-$HOME/models/halogen-npu}"
|
|
HF="https://huggingface.co"
|
|
|
|
MODELS="${HALOGEN_NPU_MODELS:-}"
|
|
if [ -z "$MODELS" ] && [ -f "$ENV_FILE" ]; then
|
|
MODELS="$(sed -nE 's/^HALOGEN_NPU_MODELS=(.*)$/\1/p' "$ENV_FILE" | tail -1)"
|
|
fi
|
|
|
|
VERIFY_ONLY=0
|
|
case "${1:-}" in
|
|
--verify-only) VERIFY_ONLY=1 ;;
|
|
"") ;;
|
|
*) echo "usage: ${0##*/} [--verify-only] (models come from HALOGEN_NPU_MODELS)" >&2; exit 2 ;;
|
|
esac
|
|
[ -n "$MODELS" ] || { echo "no NPU model named — set HALOGEN_NPU_MODELS or run with it in the environment" >&2; exit 1; }
|
|
|
|
# manifest <model-id> -> repo revision + one "path bytes sha256" per file.
|
|
manifest() {
|
|
case "$1" in
|
|
qwen3-embedding-0.6b)
|
|
NPU_REPO="peonist-ai/halogen-npu-qwen3-embedding-0.6b"
|
|
NPU_REV="4960fd9d4423dd15373ad21c83db48dbdb3b2798"
|
|
FILES=(
|
|
"devices/NOTICE.md 3248 0f267b3d2523ddf14997ee58660a492a9eaf10ccf6b08bf19dfdc85b9a478b91"
|
|
"devices/consts.hnpb 5086496 ae430e41545a0b1012938cda373b0c450364d3be631111cca3a8c43679f93336"
|
|
"devices/devices.hnpm 24047 4e9bca9d95d084fb89ce5b99f579a00666c8ca1efae2613c53e3a4878cc9f174"
|
|
"devices/u0.elf 4931968 b38f165886cb037b7d3951d9cb878473eaba58a432acd45416ff85fd88aa4e46"
|
|
"devices/u1.elf 5456000 b1811feee851b8af833b37721da92099d89772110815bbbe2c3fa1c276f0ff42"
|
|
"devices/u2.elf 6505552 406ea9a95b470628a7382e376f4a72cdf4d7aa4c59b4ddf0a4bdd139d63d6ff2"
|
|
"devices/u3.elf 6817216 bc205d51b523d770151a0c61968f7c70bf624412827580bb6fe0aaf4650dc2ee"
|
|
"devices/u4.elf 7444032 a5a91338a2603acdcd5c11df532033fda2d3f5479ea44ae87f98fafbdff9a62d"
|
|
"devices/u5.elf 7432416 6c2bf41835569e0eba25ded7c05c7264ae69c6700bdeb4791057d884f2a4c126"
|
|
"devices/u6.elf 4941232 919d0038ca088eafddf195f3466ff38db27d3635be1cffa5d4d4b5140902a68e"
|
|
"devices/u7.elf 5488304 d904cf8afae9428dd167960549e18cce5441745f18c62f8c50975dffc077d3c2"
|
|
"devices/u8.elf 4941232 db3aef69b17825854ee0dd2df475125a3f5c872f95c686793d6af1ee83596fa7"
|
|
"devices/u9.elf 5488304 d182727e551084db8fb9600441d8aa373c7f09a3d267ae0a5e921ef92326db1c"
|
|
"qwen3-embedding-0.6b.hnpw 813032248 b9f9901429df7b46ec17153aa3899f738adbea8e4356080da0c6e6586fb750d1"
|
|
"tokenizer/tokenizer.json 11423705 def76fb086971c7867b829c23a26261e38d9d74e02139253b38aeb9df8b4b50a"
|
|
) ;;
|
|
*)
|
|
echo "unknown NPU model '$1' — add its catalog block to manifest() in this script" >&2
|
|
return 1 ;;
|
|
esac
|
|
}
|
|
|
|
for MODEL in ${MODELS//,/ }; do
|
|
manifest "$MODEL" || exit 1
|
|
echo "== $MODEL ($NPU_REPO@$NPU_REV) =="
|
|
for entry in "${FILES[@]}"; do
|
|
read -r f want sha <<<"$entry"
|
|
path="$DEST/$MODEL/$f"
|
|
mkdir -p "$(dirname "$path")"
|
|
if [ "$VERIFY_ONLY" -eq 0 ]; then
|
|
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, so resolve the
|
|
# effective URL first and resume against the CDN (the sibling
|
|
# 62 GiB fetch hit this on 2026-10-18).
|
|
url=$(curl -sfIL -o /dev/null -w '%{url_effective}' "$HF/$NPU_REPO/resolve/$NPU_REV/$f") \
|
|
|| { echo "cannot resolve $NPU_REPO/$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; }
|
|
if [ -L "$path" ]; then
|
|
echo "SYMLINK $path — the serve gate refuses symlinks; replace with a real file" >&2
|
|
exit 1
|
|
fi
|
|
got="$(stat -c %s "$path")"
|
|
[ "$got" = "$want" ] || { echo "SIZE $path: $got != $want" >&2; exit 1; }
|
|
got="$(sha256sum "$path" | cut -d' ' -f1)"
|
|
[ "$got" = "$sha" ] || { echo "SHA256 $path: $got != $sha" >&2; exit 1; }
|
|
echo "OK $f ($want bytes, sha256)"
|
|
done
|
|
done
|
|
echo "DOWNLOAD_DONE $DEST ($MODELS)"
|