Files
serverAppConfigs/scripts/download-frank-dragoon-unsloth.sh
Frank 55f87fcdce llm: retire the trio service names — fleet services renamed to frank-archon/frank-dragoon/frank-zealot
The 2026-10-09 wire aliases become the service names everywhere:
systemd units, serve scripts, env files and FRANK_* variables,
deploy-plan/deploy-remote service keys, container names, Prometheus
job names + alert rules, Grafana dashboard queries, bats seams,
runbook filenames, homepage/AGENTS lore, and historical prose.
Rollback tags renamed in lockstep. Residue in the tree: zero.
Retired frank-medium/small/npu names are history and stay.

Known casualty: scripts/patches/*.patch context lines were rewritten;
those retired-era vLLM-fork patches no longer apply byte-exact.
2026-10-10 17:03:14 -07:00

116 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Download unsloth/Qwen3.8-Flash-Next-GGUF:UD-IQ4_XS (the
# frank-dragoon model) on Dankclaw.
#
# bash scripts/download-frank-dragoon-unsloth.sh
# bash scripts/download-frank-dragoon-unsloth.sh --verify-only
#
# The UD-IQ4_XS GGUF ships as three llama.cpp split parts (HF caps uploads at
# 50 GB). The parts ARE the canonical payload: the serve script points -m at
# part 1 and llama.cpp loads the 3-way split natively. Do NOT concatenate
# them into a single file to serve — part 1's header carries
# ggml.file_count=3, so a byte-correct concatenation still advertises itself
# as a split and the loader rejects the non-split filename.
# The MTP shared-Q8_0 head borrows the token embedding and the output
# projection from the main model at runtime (it is the 2.6 GB remainder of
# the 4B MTP layer). The F16 projector is fetched for a future vision flip;
# the serve script does not load it while ENABLE_VISION=0.
#
# Integrity: every part is checked against its LFS sha256 (fetched live from
# the Hub tree API, not trusted from disk). The repo is public (gated:
# false); an HF token at ~/.cache/huggingface/token is used when present to
# clear the anonymous rate limit on a 94 GB transfer.
#
# Layout after a successful run (MODEL_DIR):
# UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-0000{1,2,3}-of-00003.gguf the 3-way split
# mtp/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf 2,786,568,256 draft head
# vision/mmproj-F16.gguf 904,004,000 projector
set -euo pipefail
REPO="unsloth/Qwen3.8-Flash-Next-GGUF"
MODEL_DIR="/home/dank/models/Qwen3.8-Flash-Next-UD-IQ4_XS"
BASE="https://huggingface.co/$REPO/resolve/main"
mode="download"
[[ "${1:-}" == "--verify-only" ]] && mode="verify"
token=""
auth=()
if [[ -r "$HOME/.cache/huggingface/token" ]]; then
token="$(tr -d '[:space:]' < "$HOME/.cache/huggingface/token")"
auth=(-H "Authorization: Bearer $token")
fi
dl() { # dl <relpath>
local rel="$1" dest="$MODEL_DIR/$rel"
[[ -s "$dest" ]] && return 0
mkdir -p "$(dirname "$dest")"
echo "fetching $rel"
curl -fL -C - "${auth[@]}" --retry 5 --retry-delay 3 -o "$dest" "$BASE/$rel"
}
# relpath size sha256 — the LFS oids from the Hub tree API (2026-09-21)
FILES=(
"UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00001-of-00003.gguf 10946624 5ce89370720f8bf90890f439361282104c1aa1482d4013bb9a50923e758e71a4"
"UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00002-of-00003.gguf 49835229856 577a38a2392b40ca2193cea502e1d92f60b8cd370675d308e0ec21885d9daaa7"
"UD-IQ4_XS/Qwen3.8-Flash-Next-UD-IQ4_XS-00003-of-00003.gguf 43836407744 d4634e6d84f0ebb0940be15c90d3790bf6464e3dea3a1cddc567dc0e83ad8833"
"MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf 2786568256 5ff54097406a905cf3a724c709124ceb0e3e10235ee862298969e91c96fa96e6"
"mmproj-F16.gguf 904004000 1f7b7f0b984cf065c604360c29c8098362ed61b290db0ff12c6f360bb1a8a980"
)
RELS=()
for line in "${FILES[@]}"; do
read -r rel size _ <<<"$line"
RELS+=("$rel")
if [[ "$mode" == "download" ]]; then
dl "$rel"
fi
done
echo "verifying parts against the live Hub tree API"
python3 - "$MODEL_DIR" "$REPO" "${RELS[@]}" <<'PY'
import hashlib, json, os, sys, urllib.request
model_dir, repo = sys.argv[1], sys.argv[2]
tree = json.load(urllib.request.urlopen(
f"https://huggingface.co/api/models/{repo}/tree/main?recursive=true"))
want = {}
for f in tree:
if f["type"] == "file" and f.get("lfs"):
want[f["path"]] = (f["lfs"]["size"], f["lfs"]["oid"])
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 24), b""):
h.update(chunk)
return h.hexdigest()
ok = True
expected = set(sys.argv[3:])
for rel in sorted(expected):
if rel not in want:
print(f"NOT IN HUB TREE {rel}"); ok = False; continue
size, oid = want[rel]
path = os.path.join(model_dir, rel)
if not os.path.isfile(path):
print(f"MISSING {rel}"); ok = False; continue
actual_size = os.path.getsize(path)
if actual_size != size:
print(f"SIZE MISMATCH {rel}: {actual_size} != {size}"); ok = False; continue
digest = sha256(path)
if digest != oid:
print(f"SHA256 MISMATCH {rel}"); ok = False; continue
print(f"ok {rel}")
sys.exit(0 if ok else 1)
PY
if [[ "$mode" == "download" ]]; then
mkdir -p "$MODEL_DIR/mtp" "$MODEL_DIR/vision"
[[ -e "$MODEL_DIR/mtp/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf" ]] ||
mv "$MODEL_DIR/MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf" "$MODEL_DIR/mtp/"
[[ -e "$MODEL_DIR/vision/mmproj-F16.gguf" ]] || mv "$MODEL_DIR/mmproj-F16.gguf" "$MODEL_DIR/vision/"
fi
echo "frank-dragoon payload complete: $MODEL_DIR"