The 2026-09-26 same-day cutover that ended the Lucebox DS4.1 detour:
Dankserver's :7770 endpoint is now unsloth llama.cpp (the b11030-mix
prebuilt, ROCm gfx120X, bundled runtime) serving
unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 — 175.30 GiB across six sha256-
verified split parts — on both Radeon AI PRO R9700s, as a ROOT unit (the
frank-large shape: no container, no generated unit).
The deployed contract:
- Placement law: layer split -ts 1,1 with -ngl 999; the routed experts
and the 51B-param n-gram (PLE) table are COMPUTED in host RAM via
-ot exps=CPU,ngram_embeddings=CPU (the x4-pair measurement: streaming
over PCIe loses at 3.7 t/s, computing in RAM does not care).
per_layer_token_embd.weight=CPU is required by the shared MTP head's
borrow path (a GPU-resident borrow crashes the draft load).
mmap stays on; --cache-ram 16384.
- Spec chain draft-mtp,ngram-mod: the shared Q8_0 MTP head
(mtp-...-shared-Q8_0.gguf, 2,786,568,256 B) at depth 2, with the
draftless ngram-mod matcher chained behind it — the only legal way to
layer two spec implementations in one process.
- Vision explicit: --mmproj names the BF16 projector (this build never
autoloads; a server without it comes up healthy and text-only).
- Reasoning: --reasoning-format deepseek → reasoning_content; the
Qwen3.8 template's {xhigh, medium, low} reasoning_effort gate folded
by reasoningEffortMap in models.yml.
- One stream, the model's full 262,144 context; greedy server default
with Qwen-recommended sampling riding compat.extraBody.
The deployer's deploy_llm_primary is rewritten for the root unit: it
gates every artifact at its declared byte count (stat -c %s, never wc -c
— which read the whole 377 GiB lucebox file for minutes on 2026-09-26),
retires the lucebox user quadlet to dated home trash, waits out port
7770, relinks/enables/restarts the root unit and waits up to 1800 s for
/health. The quadlet-era tree under dankserver/frank-primary/ is
archived (dankserver/.AGENTTRASH/20260926-llamacpp-cutover/ on this
box; git history everywhere else) and its planner mapping replaced by
the scripts/ trio.
Observability flips back to the llamacpp namespace: a frank-primary
Prometheus job at 192.168.0.235:7770 (cross-host LAN — the netavark
published-port quirk that forces host.containers.internal for the two
Dankclaw endpoints is same-host only), the LlmEndpointDown runbook
text, and the dashboard description for three llama.cpp endpoints.
Docs: the new runbook (frank-primary-llamacpp-qwen38-flash-next-q8.md),
RETIRED banners on the lucebox and radiance runbooks and their index
entries, the AGENTS.md fleet section rewritten for the root-unit shape,
and the homepage frank-medium card renamed to frank-primary with the
Q8_0 Flash-Next spec (docs.html #models and the modality note updated:
images on frank-primary and frank-small, audio on frank-small only).
models.yml: the dankserver provider is "llamacpp (dankserver)" serving
frank-primary, contextWindow 262144, maxTokens 65536 (compaction-gap
law: 183,501 + 65,536 <= 262,144), input [text, image].
Suite: 346 green (frank_primary_unit.bats rewritten for the root unit —
16 tests pinning the placement law, the shared-head requirement, the
explicit projector, HIP env hygiene (BIND_HOST, never bare HOST), and
the deployer's byte-count gates; the quadlet-era planner and
changed-tests mappings rewritten for the trio; the observability scrape
test asserts three llamacpp jobs again).
Rollback: the lucebox image (localhost/lucebox-rocm:gfx1201) and the
378 GiB payload stay on the host; the Qwen payload stays fetched, so a
re-swap in either direction is a redeploy, not a refetch.
135 lines
5.2 KiB
Bash
Executable File
135 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Serve unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 as frank-primary on
|
|
# Dankserver's two Radeon AI PRO R9700s (gfx1201), using the unsloth
|
|
# b11030-mix prebuilt (ROCm gfx120X, bundled runtime).
|
|
#
|
|
# Shape: one stream, the model's full 262144 context, all non-overridden
|
|
# tensors on the GPUs (--n-gpu-layers 999 across both cards), with the
|
|
# routed experts and the 51B-param n-gram embedding table computed in host
|
|
# RAM via --override-tensor (the x4-pair law: streaming over PCIe loses,
|
|
# computing in RAM does not care). mmap stays ON (the PLE table must stay
|
|
# page-cache-backed; --no-mmap forces it into anonymous memory and the
|
|
# cgroup kill is silent).
|
|
#
|
|
# Spec chain: draft-mtp (the shared Q8_0 head, loaded via -md) chained with
|
|
# the draftless ngram-mod matcher. The shared head borrows the target's
|
|
# embedding tensors, which is why per_layer_token_embd.weight must be
|
|
# CPU-resident (see frank-primary.env). depth 2.
|
|
#
|
|
# Vision: the BF16 projector, loaded explicitly via --mmproj (this build
|
|
# never autoloads projectors — the frank-medium 2026-09-17 trap: a server
|
|
# started without it comes up healthy and text-only, and answers image
|
|
# requests with 500).
|
|
#
|
|
# Template: the GGUF's embedded chat template via --jinja. The Qwen3.8
|
|
# family template gates reasoning_effort to {xhigh, medium, low} — the
|
|
# harness folds its levels onto that set in models.yml
|
|
# (reasoningEffortMap). If the embedded template ever falls back to
|
|
# peg-native and bloats prompts (~73,600 tokens for a ~250-token request),
|
|
# switch to --chat-template-file scripts/qwen-sharp-v22.4.0.jinja the way
|
|
# frank-large does.
|
|
#
|
|
# Tunables come from frank-primary.env unless FRANK_PRIMARY_ENV_FILE
|
|
# overrides it; anything not set there falls through to the defaults below.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENV_FILE="${FRANK_PRIMARY_ENV_FILE:-$SCRIPT_DIR/frank-primary.env}"
|
|
if [[ -f "$ENV_FILE" ]]; then
|
|
set -a
|
|
source "$ENV_FILE"
|
|
set +a
|
|
fi
|
|
|
|
# --- defaults (used when the env file does not set them) ------------------
|
|
: "${RUNTIME_DIR:=/home/dank/GitPackages/unsloth-llama-b11030}"
|
|
: "${MODEL_ROOT:=/home/dank/models/Qwen3.8-Flash-Next-GGUF}"
|
|
: "${BIND_HOST:=0.0.0.0}"
|
|
: "${PORT:=7770}"
|
|
: "${CONTEXT_SIZE:=262144}"
|
|
: "${PARALLEL_SLOTS:=1}"
|
|
: "${SPEC_DRAFT_N_MAX:=2}"
|
|
: "${NGL:=999}"
|
|
: "${OT_OVERRIDES:=exps=CPU,ngram_embeddings=CPU,per_layer_token_embd.weight=CPU}"
|
|
: "${TENSOR_SPLIT:=1,1}"
|
|
: "${SPEC_DRAFT_DEVICE:=ROCm0}"
|
|
: "${BATCH_SIZE:=8192}"
|
|
: "${UBATCH_SIZE:=512}"
|
|
: "${CACHE_RAM:=16384}"
|
|
|
|
SERVER_BIN="$RUNTIME_DIR/llama-server"
|
|
MODEL_PARTS=(
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00001-of-00006.gguf"
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00002-of-00006.gguf"
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00003-of-00006.gguf"
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00004-of-00006.gguf"
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00005-of-00006.gguf"
|
|
"$MODEL_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-00006-of-00006.gguf"
|
|
)
|
|
DRAFTER_FILE="$MODEL_ROOT/MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf"
|
|
MMPROJ_FILE="$MODEL_ROOT/mmproj-BF16.gguf"
|
|
|
|
if [[ ! -x "$SERVER_BIN" ]]; then
|
|
echo "runtime not found at $SERVER_BIN" >&2
|
|
echo " fetch the unsloth b11030-mix gfx120X prebuilt (see the runbook)" >&2
|
|
exit 1
|
|
fi
|
|
# Every part is checked: part 1 is the -m target and parts 2-6 are followed
|
|
# by the split-file loader, so a missing tail part fails at LOAD time with
|
|
# a confusing partial-mmap error unless it is caught here.
|
|
for f in "${MODEL_PARTS[@]}" "$DRAFTER_FILE" "$MMPROJ_FILE"; do
|
|
if [[ ! -f "$f" ]]; then
|
|
echo "missing asset: $f" >&2
|
|
echo " run scripts/download-frank-primary.sh on this host" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# --- env (the unsloth build's bundled ROCm runtime) -----------------------
|
|
# Both R9700s (gfx1201, HIP devices 0 and 1); the host's Raphael iGPU is
|
|
# device 2 and must stay hidden from the enumeration.
|
|
export HIP_VISIBLE_DEVICES=0,1
|
|
export LD_LIBRARY_PATH="$RUNTIME_DIR:${LD_LIBRARY_PATH:-}"
|
|
|
|
# --- the serve command ---------------------------------------------------
|
|
ARGS=(
|
|
--host "$BIND_HOST"
|
|
--port "$PORT"
|
|
--model "${MODEL_PARTS[0]}"
|
|
--alias frank-primary
|
|
--ctx-size "$CONTEXT_SIZE"
|
|
--parallel "$PARALLEL_SLOTS"
|
|
--n-gpu-layers "$NGL"
|
|
--tensor-split "$TENSOR_SPLIT"
|
|
--jinja
|
|
--reasoning auto
|
|
--reasoning-format deepseek
|
|
--reasoning-budget -1
|
|
--flash-attn on
|
|
--batch-size "$BATCH_SIZE"
|
|
--ubatch-size "$UBATCH_SIZE"
|
|
--cache-ram "$CACHE_RAM"
|
|
# Vision: explicit, never autoloaded by this build
|
|
--mmproj "$MMPROJ_FILE"
|
|
# Spec chain: the shared MTP head + the draftless ngram matcher
|
|
--spec-type draft-mtp,ngram-mod
|
|
--model-draft "$DRAFTER_FILE"
|
|
--spec-draft-n-max "$SPEC_DRAFT_N_MAX"
|
|
--spec-draft-device "$SPEC_DRAFT_DEVICE"
|
|
# Greedy server default: deterministic e2e, and the harness overrides
|
|
# sampling per-request (models.yml compat.extraBody carries Qwen's
|
|
# recommended temp 1.0 / top_p 0.95 / top_k 20).
|
|
--temp 0
|
|
--top-p 1.0
|
|
--top-k 0
|
|
--min-p 0.0
|
|
)
|
|
|
|
# Comma-separated <pattern>=<backend> pairs -> repeated --override-tensor.
|
|
IFS=',' read -ra _ots <<< "$OT_OVERRIDES"
|
|
for ot in "${_ots[@]}"; do
|
|
ARGS+=(--override-tensor "$ot")
|
|
done
|
|
|
|
exec "$SERVER_BIN" "${ARGS[@]}"
|