#!/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 — //devices/*, //.hnpw and # //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 " /opt/halogen/npu/models.txt # podman exec frank-dragoon-halogen grep "^model " /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}" # The env file owns the staging path (it must sit inside the payload dir, which # is the container's only /models mount); an exported FRANK_DRAGOON_NPU_DIR wins # so the bats seam can stage somewhere disposable. DEST="${FRANK_DRAGOON_NPU_DIR:-}" if [ -z "$DEST" ] && [ -f "$ENV_FILE" ]; then DEST="$(sed -nE 's/^FRANK_DRAGOON_NPU_DIR=(.*)$/\1/p' "$ENV_FILE" | tail -1)" fi : "${DEST:?FRANK_DRAGOON_NPU_DIR is not set and $ENV_FILE names no FRANK_DRAGOON_NPU_DIR}" 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 -> 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)"