frank-primary: swap Dankserver to llama.cpp Qwen3.8-Flash-Next Q8_0

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.
This commit is contained in:
Frank
2026-09-26 19:45:28 -07:00
parent d89813363e
commit 8500600d09
25 changed files with 856 additions and 544 deletions
+75 -81
View File
@@ -9,7 +9,7 @@ services run from `/home/dank/serverAppConfigs` on each host (the 2026-08-22
merge absorbed the former `vllmScripts` repo, history preserved).
- **Dankserver** (192.168.0.235) — media/library stack (RomM, Copyparty,
Calibre-Web, Jellyfin, Plex) plus the `frank-medium` LLM service.
Calibre-Web, Jellyfin, Plex) plus the `frank-primary` LLM service.
- **Dankclaw** (192.168.0.143) — AI/search/dev/observability stack (SearXNG
via Gluetun, ComfyUI, OpenWebUI, Hermes, Gitea, Grafana, Prometheus, Loki,
Alloy, Pi-hole) plus TWO LLM services: `frank-large` (stock llama.cpp on
@@ -24,7 +24,7 @@ merge absorbed the former `vllmScripts` repo, history preserved).
| Target | Service | Runtime | Model | Port |
|--------|---------|---------|-------|------|
| Dankserver | `frank-medium` | vllm-radiance 0.9.3 (rootless Quadlet, image `localhost/radiance-vllm:0.9.3`, gfx1201) | just1moremodel Qwen3.8-27B-Uncensored-MXFP4-awq (MXFP4/quark W4A8, 19.37 GiB) with the z-lab DFlash2 external drafter (depth 7), 2x Radeon AI PRO R9700 in ONE tensor-parallel process (TP=2 over the R4D one-shot allreduce — no RCCL collective; both cards on CPU root ports at Gen5 x8 since the 2026-09-20 ProArt B850-Creator swap), 262144 ctx over up to 8 conversations, UNQUANTIZED bf16 KV (`KV_DTYPE=auto`, cut 2026-09-21 from the FP8/fp8-KV shape to test whether fp8 KV had been degrading W4A8 output quality; auto-sized 486,464-token pool = 1.86x the window), prefix caching, vision | 7770 |
| Dankserver | `frank-primary` | unsloth llama.cpp b11030-mix prebuilt (ROCm gfx120X, bundled runtime, root systemd unit) | unsloth Qwen3.8-Flash-Next-GGUF Q_8 (175.30 GiB, 6 split parts) with the shared Q_8 MTP head (depth 2) chained behind `ngram-mod`, BF16 vision (mmproj), layer split `-ts 1,1` across both R9700s with the routed experts + 51B-param n-gram table computed in host RAM (`-ot exps=CPU,ngram_embeddings=CPU,per_layer_token_embd.weight=CPU` — the shared-head borrow path needs the last), 262144 ctx single stream (1 slot), mmap on, `--cache-ram 16384` | 7770 |
| Dankclaw | `frank-large` | unsloth llama.cpp b11030 prebuilt (ROCm gfx1151, root systemd unit) | unsloth Qwen3.8-27B-GGUF:UD-Q8_K_XL (27B dense, 31.5 GiB) with the z-lab DFlash2 Q4_K_M drafter (depth 4) on the gfx1151 iGPU (Strix Halo LPDDR, 114 GiB GTT), 262144 ctx single stream (1 slot), q8_0 KV, text only | 7770 |
| Dankclaw | `frank-small` | mainline llama.cpp master 37b53fd (root systemd unit) | HauhauCS Gemma4-12B-QAT-Uncensored-Balanced Q4_K_M (BF16 mmproj + MTP assistant sidecar, `draft-mtp,ngram-mod`) on the RTX 3090, 1048576 pool / 4 slots of 262,144, q8_0 KV, the fleet's fastest vision endpoint | 7771 |
@@ -61,16 +61,15 @@ All edits flow through git. Never edit files on a remote machine directly.
./scripts/deploy.sh dankclaw
./scripts/deploy.sh dankserver
./scripts/deploy.sh dankclaw --service searxng
./scripts/deploy.sh dankserver --service frank-medium
./scripts/deploy.sh dankserver --service frank-primary
./scripts/deploy.sh dankclaw --dry-run
```
The planner maps changed paths to services; the remote runner restarts only
the selected service and records its revision outside the checkout.
Documentation-only changes are a no-op. LLM services are selected only by
their own files — `frank-large`/`frank-small` by their unit/env/serve trio in
`scripts/`, `frank-medium` by its Quadlet unit, entrypoint and healthcheck under
`dankserver/frank-medium/` — so
their own files — every endpoint (`frank-large`/`frank-small`/`frank-primary`)
by its unit/env/serve trio in `scripts/` — so
unrelated
container changes cannot interrupt inference. Set `FRANK_SUDO_PW` for headless
runs on a host without passwordless sudo; the value never enters the repo.
@@ -232,9 +231,8 @@ merely wedged kernel usually still answers ARP.
`sudo systemctl stop frank-large && sudo systemctl mask frank-large`
- Dankclaw `frank-small` (root unit, llama.cpp on the 3090):
`sudo systemctl stop frank-small && sudo systemctl mask frank-small`
- Dankserver `frank-medium` (**rootless Quadlet since 2026-09-20** — the
system unit is retired, so `sudo systemctl` sees nothing):
`systemctl --user stop frank-medium && systemctl --user mask frank-medium`
- Dankserver `frank-primary` (root unit, llama.cpp on the 2x R9700):
`sudo systemctl stop frank-primary && sudo systemctl mask frank-primary`
2. Bench.
3. Unmask and start; verify `curl http://192.168.0.<host>:7770/health`.
@@ -247,9 +245,8 @@ deletes the unit file from `/etc/systemd/system/`, not just the symlink.
The harness reaches the fleet through `.omp/agent/models.yml` in the
operator's home — `/home/blank/.omp/agent/models.yml` on the dev box, which is
where the harness runs; there is no copy on the servers. Each provider is one
HOST:PORT; each model `id` is the served alias: `frank-medium` →
HOST:PORT; each model `id` is the served alias: `frank-primary` →
`192.168.0.235:7770`, `frank-large` → `192.168.0.143:7770`, `frank-small` →
`192.168.0.143:7771`. Per-role routing lives beside it in `config.yml` under
`modelRoles`.
Resolution rules: the default model resolves as a `defaultProvider` +
@@ -262,16 +259,19 @@ When renaming a service, retiring an endpoint, or moving a model: update the
provider block, model `id`, `contextWindow`, `maxTokens`, `input` in
`models.yml`, then fix every `modelRoles` entry that named the old endpoint —
a role pointing at a provider the file does not define fails at call time. As
of 2026-09-20 both `frank-small` and `frank-medium` accept images
(`input: [text, image]`) — frank-medium's checkpoint serves vision through the
Gemma4 merge path with no separate mmproj, verified on held-out synthetic
images; `frank-large` remains text-only because its stock llama.cpp deployment loads no
of 2026-09-26 both `frank-small` and `frank-primary` accept images
(`input: [text, image]`) — frank-primary loads the BF16 mmproj explicitly
(this build never autoloads projectors); `frank-large` remains text-only
because its stock llama.cpp deployment loads no
vision projector (the mmproj is not loaded). Note also that
the reasoning channel differs by runtime: llama.cpp endpoints emit
`reasoning_content`, vLLM emits `reasoning`.
`reasoning_content`, vLLM emits `reasoning` — academic since the
2026-09-26 swap, as every endpoint in the fleet is llama.cpp now.
There is **no audio**
input anywhere in the fleet: that capability left with the FLM NPU endpoint on
2026-09-11.
input anywhere in the fleet except frank-small: that capability left with the
FLM NPU endpoint on 2026-09-11 and returned on 2026-09-17 with the Gemma4
projector's audio encoder (frank-small transcribes WAV verbatim even though
`audio` is omitted from its `input` list — OMP's schema rejects it).
### frank-large — stock llama.cpp Qwen3.8-27B UD-Q8_K_XL (Dankclaw, ACTIVE)
@@ -341,73 +341,66 @@ ubatch 2048 OOMs the draft's pp buffer, and why draft depth 4 + poll
Runbook: `serverAppConfigs-docs/LLM/frank-small-gemma4-12b.md`.
Rollback: [[frank-small-qwen38-gsq-rco]].
### frank-medium — radiance TP=2, DFlash2 drafter (Dankserver, ACTIVE)
### frank-primary — llama.cpp Qwen3.8-Flash-Next Q8_0 (Dankserver, ACTIVE)
vllm-radiance **0.9.3** (`localhost/radiance-vllm:0.9.3`, rootless **Quadlet**
`dankserver/frank-medium/quadlet/frank-medium.container`) serving
`just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq` (MXFP4/quark W4A8, 19.37
GiB) with the **z-lab DFlash2 external drafter** (`num_speculative_tokens: 7`,
live mean accept 4.24/7) in **one tensor-parallel process across both
R9700s** (`--tensor-parallel-size 2`). 262,144 ctx, `--max-num-seqs 8`, chunk
8192, **unquantized bf16 KV** (`KV_DTYPE=auto`, cut 2026-09-21 from the
FP8/fp8-KV shape to test whether fp8 KV had been degrading W4A8 output
quality), prefix caching, GPU fill 0.98, **no KV-memory pin** — the pool
auto-sized to 486,464 tokens (1.86× the full window). DFlash2 was A/B tested
against MTP self-speculation on this checkpoint and wins on decode (100.0 vs
85.9 t/s) — see the runbook. Tools via `qwen3_coder`, reasoning via `qwen3`
(the field is `reasoning`, not llama.cpp's `reasoning_content`), vision through
the Gemma4 merge path, chat template Qwen-Sharp v22.4.0 (froggeric,
repo-tracked at `dankserver/frank-medium/`, terse system prompt by default).
unsloth llama.cpp **b11030-mix prebuilt** (ROCm gfx120X, commit `6ba30d05b`,
bundled ROCm runtime at `/home/dank/GitPackages/unsloth-llama-b11030/`, **root
systemd unit** `scripts/frank-primary.service`, `User=dank`) serving
`unsloth/Qwen3.8-Flash-Next-GGUF` at **Q_8** — 175.30 GiB across six split
parts under `/home/dank/models/Qwen3.8-Flash-Next-GGUF/Q8_0/`, fetched by
`scripts/download-frank-primary.sh` (per-file sha256 + byte-count verified) —
on **both** Radeon AI PRO R9700s. Replaced the Lucebox DS4.1 quadlet (hours
old) on 2026-09-26, which had replaced radiance frank-medium the same day.
**The entrypoint is quantization-aware.** `radiance-entrypoint.sh` reads
`quantization_config.quant_method` from the checkpoint's `config.json` and
serves the plain FP8 path (no MXFP4 patches, `--quantization fp8`,
`ROCM_AITER_UNIFIED_ATTN`, the DFlash2 drafter) or the MXFP4 patch-and-serve
path (the DFlash2 external drafter). A checkpoint swap is therefore four quadlet
env vars (`SNAP`, `SPEC_METHOD`, `SPEC`, `KV_MEM`) — the deploy handler reads
the serving shape from the quadlet and gates on the right assets. The MXFP4
checkpoint (`just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq` + the z-lab
DFlash2-FP8 drafter) remains on disk as the rollback target. Full swap
process + toolchain: [[frank-medium-model-swap]].
**Placement: compute the experts in RAM, never stream them.** Layer split
`-ts 1,1` with `-ngl 999`; the routed experts and the 51B-param n-gram
(PLE) table are overridden to CPU (`-ot
exps=CPU,ngram_embeddings=CPU,per_layer_token_embd.weight=CPU`) and computed
from the mmap page cache — the x4-pair law (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 with `invalid vector subscript`). Dense weights,
attention, the BF16 projector and the MTP head sit on the cards (~29 GiB);
mmap must stay on (`--no-mmap` forces the table into anonymous memory →
silent cgroup kill) and the PLE tensor must never carry a GPU target
(23 → 13.4 t/s when tried). `--cache-ram 16384`; KV is the hybrid
DeltaNet/QSA pattern, a fraction of the dense-model cost.
**TP=2 runs because the board changed.** The predecessor board (TUF GAMING
B650-E) put card #2 behind chipset bridges advertising `AtomicOpsCap: Routing-`
and no collective could be dispatched there. The ProArt B850-Creator WiFi NEO
routes both cards to CPU GPP root ports (`00:01.1 → 03:00.0`,
`00:01.3 → 07:00.0`, Gen5 x8, every hop `Routing+`, both endpoints `ReqEn+`).
The collective in use is vLLM's **R4D one-shot allreduce**, which never touches
RCCL (TheRock RCCL has no gfx1201 device code; the Ubuntu RCCL 2.27 build does,
and that is what `scripts/build-llama-rdna4.sh GGML_HIP_RCCL=ON` links).
**Spec chain `draft-mtp,ngram-mod`, depth 2.** The shared Q_8 head
(`MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf`, 2,786,568,256 B — unsloth's
recommended pairing) loads via `--model-draft` and borrows the target's
embedding tensors; `ngram-mod` chains behind it (draftless, so legal).
The dankclaw UD-IQ4_XS deployment of the same model family ran this head
at depth 2 with 0.83–0.92 acceptance. ngram-mod prints no startup banner —
identify it behaviourally (`draft_n` > 0 where text repeats).
**Measured, not assumed** (`scripts/bench-conversation.py`, 10 real prompts,
max_tokens 512, greedy, salted per arm, service stopped between arms):
radiance TP=2 95.8 t/s decode geomean / 0.21 s TTFT / 259.4 t/s burst-4, vs the
llama.cpp layer split's 45.7 / 0.72 / 71.7 and llama.cpp `--split-mode tensor`'s
30.5 / 0.71 / 54.3. Honest caveat: the arms also differ in quantization (MXFP4
vs Q8_0), so part of the margin is the quant, not the parallelism. Re-measured
against production after deploy: 92.4 t/s, 0.22 s, 248.9.
**Vision explicit, reasoning deepseek-format.** `--mmproj` names the BF16
projector (this build never autoloads one — a server without the flag comes
up healthy and text-only and answers image requests 500). `--jinja` with the
GGUF's embedded template; `--reasoning-format deepseek` fills
`reasoning_content`. The Qwen3.8 template gates `reasoning_effort` to
`{xhigh, medium, low}` — folded by `reasoningEffortMap` in `models.yml`.
One stream, full 262,144 ctx (`PARALLEL_SLOTS=1`); greedy server default,
Qwen-recommended sampling rides `compat.extraBody` per request. `--flash-attn
on`, batch 8192 / ubatch 512 (the measured ROCm optimum class), port 7770,
alias `frank-primary`.
Files: `dankserver/frank-medium/quadlet/frank-medium.container`,
`dankserver/frank-medium/radiance-entrypoint.sh` (quantization-aware),
`dankserver/frank-medium/frank-medium-healthcheck.py`. Swap toolchain:
`scripts/hf-manifest.py` (generate a `<filename> <size> <sha256>` manifest
from the live repo tree), `scripts/hf-fetch.sh` (resumable, byte + sha256
verified, idempotent fetch), `scripts/swap-frank-medium.sh` (generate
manifest → download → detect quantization → edit the quadlet → deploy). The
llama.cpp layer-split trio (`scripts/frank-medium.service`, `.env`,
`serve-frank-medium.sh`) is archived in `scripts/.AGENTTRASH/
20260920-llama-layersplit/`; `deploy-remote.sh`'s `retire_llama_medium_unit`
moved the installed unit symlink there rather than `systemctl disable`, which
would delete it. The deploy handler reads `SNAP`/`SPEC_METHOD` from the
quadlet, gates on the image tag + the checkpoint's `config.json` (drafter only
for `dflash`, `libr4d` only for `mxfp4`/`quark`), then waits for :7770 and
gates on `/health`.
Files: `scripts/frank-primary.service` (root unit, relinked to
`/etc/systemd/system/`), `scripts/serve-frank-primary.sh` (validates binary
+ all 6 parts + drafter + mmproj, then `exec`s llama-server),
`scripts/frank-primary.env` (tunables; the bind address is `BIND_HOST` —
never a bare `HOST`, HIP reads it). The deployer's `deploy_llm_primary`
gates every artifact at its declared byte count (`stat -c %s`, never `wc -c`),
retires the lucebox quadlet to `~/.AGENTTRASH/20260926-llamacpp-cutover/`,
waits out port 7770, relinks/enables/restarts the root unit and waits up to
1800 s for `/health` (cold start page-ins ~175 GiB).
Runbook: `serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md` (serving
details); swap process + toolchain: `serverAppConfigs-docs/LLM/
frank-medium-model-swap.md`.
Rollback: the MXFP4 checkpoint on disk (`SPEC_METHOD=dflash`, `SPEC=7`);
before that, [[frank-medium-llamacpp-dflash2]] (SUPERSEDED 2026-09-20).
Runbook: `serverAppConfigs-docs/LLM/frank-primary-llamacpp-qwen38-flash-next-q8.md`.
Rollback: the lucebox DS4.1 material on host (image
`localhost/lucebox-rocm:gfx1201` + the 377 GiB payload + the archived
quadlet files in `dankserver/.AGENTTRASH/20260926-llamacpp-cutover/`) — see
[[frank-primary-lucebox-ds4]]; the Qwen payload stays fetched, so a
forward re-swap is a redeploy.
## Key Lessons Learned
Durable operational lessons from the fleet's history. Full detail in the
@@ -756,8 +749,7 @@ Active:
|---------|---------|
| frank-large (stock llama.cpp Qwen3.8-27B UD-Q8_K_XL, Dankclaw) | `serverAppConfigs-docs/LLM/frank-large-unsloth-qwen38-27b-q8xl.md` |
| frank-small (llama.cpp Gemma4-12B QAT, Dankclaw) | `serverAppConfigs-docs/LLM/frank-small-gemma4-12b.md` |
| frank-medium (radiance TP=2 + MTP, Dankserver) | `serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md` |
| frank-medium model swap (toolchain + process) | `serverAppConfigs-docs/LLM/frank-medium-model-swap.md` |
| frank-primary (llama.cpp Qwen3.8-Flash-Next Q8_0 + shared MTP, Dankserver) | `serverAppConfigs-docs/LLM/frank-primary-llamacpp-qwen38-flash-next-q8.md` |
| Dankclaw kernel posture (`amd_iommu=off`) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
| Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` |
@@ -767,6 +759,8 @@ Retired (rollback targets and history):
|---------|------|--------|---------|
| kingjones STRIX Qwen3.8-Flash-Next ROCmFP4 | Dankclaw | 2026-08-30 → 09-11 | `kingjones-strix-rocmfp4.md` |
| ik_llama.cpp Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M (`frank-large`) | Dankclaw | 2026-09-15 → 09-16 | `frank-large-ik-uncensored.md` |
| vllm-radiance TP=2 MXFP4 + DFlash2 (`frank-medium`) | Dankserver | 2026-09-20 → 09-26 | `frank-medium-radiance-mxfp4.md` |
| Lucebox DS4.1 DeepSeek-V4.1-Flash ROCmFP23 (`frank-primary`) | Dankserver | 2026-09-26 (same-day swap) | `frank-primary-lucebox-ds4.md` |
| halogen-flash-server Qwen3.8-Flash-Next w4b (`frank-large`) | Dankclaw | 2026-09-16 → 09-19 | `halogen-flash-server.md` |
| CIRU v4.4.1 Orca Qwen3.8-Flash (`frank-large`) | Dankclaw | 2026-09-19 → 09-21 | `frank-large-ciru-orca.md` |
| unsloth llama.cpp b11030 Qwen3.8-Flash-Next UD-IQ4_XS (`frank-large`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-large-unsloth-qwen38-flash-next.md` |
@@ -1,90 +0,0 @@
#!/usr/bin/env bash
# frank-primary entrypoint: exec the native luce_server with the DeepSeek V4
# launch shape for this host (2x R9700 gfx1201, 246 GiB RAM).
#
# The image's own entrypoint (server/scripts/entrypoint.sh) only forwards a
# fixed Qwen/DFlash flag set and cannot express the DS4 flags, so this unit
# bypasses it (PodmanArgs=--entrypoint) and execs the binary directly. The
# C++ server reads LUCE_* environment variables straight from the process
# environment, so no env plumbing is needed here.
#
# SOURCE BRANCH. The `deepseek41` architecture exists only on the
# `feat/ds41-engine` branch of Luce-Org/lucebox: main carries `deepseek4`
# alone and rejects this checkpoint with "unsupported model architecture
# 'deepseek41'" (server/src/common/backend_plan.cpp). The image is built from
# that branch — see serverAppConfigs-docs/LLM/frank-primary-lucebox-ds41.md.
#
# LAUNCH SHAPE (server/docs/DS41.md — the V4.1 page, not the V4 DS4.md one):
# --ds4-prefill exact THE ONLY prefill this placement can run. Any
# approximate mode (dense, sparse) makes
# DeepSeek4Backend::requires_monolithic_model() true,
# and a monolithic load asks ggml for the whole 187 GiB
# expert set as ONE device-0 buffer — measured
# 2026-09-26: "allocating 191932.25 MiB on device 0:
# cudaMalloc failed: out of memory", then
# "monolithic HIP load required for dense prefill".
# DS41.md's dense-prefill section measures that mode
# where the experts fit (in-process owners); on a
# 32 GiB target it cannot. exact feeds the prompt token
# by token, which is what the hybrid tier streams for.
# --chunk 1024 DS41.md's cold-expert chunk. The runtime caps the
# chunk to what fits after headroom regardless.
# --max-ctx 131072 the V4.1 default window. The MLA compressed caches
# are F16 by design and stay small; --cache-type-k/v
# are ignored by the DeepSeek4 backend, so they are
# not passed.
# --prefix-cache-slots 0 --prefill-cache-slots 0
# the DS41.md bring-up recipe (the server default is
# 32 prefix slots). Lift them once capacity under load
# has been measured, not before.
#
# EXPERT PLACEMENT. 377 GiB of weights against 64 GiB of VRAM: only the dense
# weights, the compressed caches and a hot expert stack fit on hip:0, and the
# remaining routed experts are streamed out of the mmapped page cache/NVMe by
# the hybrid expert tier. LUCE_EXPERT_STREAM_DEVICE=1 moves the streamed-
# expert slot cache onto the second R9700 — it defaults to the target device,
# where it would compete with the hot stack. Both the owner stack and the
# slot cache then auto-size to their device's free memory minus the 1.5 GiB
# discrete headroom.
#
# No DSpark draft is published for DeepSeek-V4.1-Flash, so this is plain
# autoregressive decode (no --draft, no LUCE_DS4_SPEC).
set -euo pipefail
MODEL="${FRANK_PRIMARY_MODEL:-/models/DeepSeek-V4.1-Flash/DeepSeek-V4.1-Flash-ROCMFP23.gguf}"
BIN="${FRANK_PRIMARY_BIN:-/opt/lucebox-hub/server/build/luce_server}"
# The second R9700 holds the streamed-expert slot cache and computes them.
export LUCE_EXPERT_STREAM_DEVICE="${LUCE_EXPERT_STREAM_DEVICE:-1}"
# The hot expert stack must be sized EXPLICITLY. Left alone the runtime
# budgets gpu_free - kv - warm - safety (22.35 GiB here), which leaves only
# its 0.50 GiB safety margin free while the fit check then demands the 1.50
# GiB discrete headroom — its own auto-sizing can never pass on a 32 GiB
# target: "device 0 memory: 30.97 of 31.86 GiB used, 0.89 GiB free (headroom
# 1.50 GiB): does not fit". 18000 MB is the value DS41.md measured its R9700
# rows at; it puts ~1500 experts (37/layer) resident and leaves device 0
# ~5 GiB free, so the fit check passes with room for transient buffers.
export LUCE_EXPERT_BUDGET_MB="${LUCE_EXPERT_BUDGET_MB:-18000}"
# rocBLAS/BLAS dispatch and queue depth, as the fleet's other ROCm servers.
export ROCBLAS_USE_HIPBLASLT="${ROCBLAS_USE_HIPBLASLT:-0}"
export GPU_MAX_HW_QUEUES="${GPU_MAX_HW_QUEUES:-8}"
if [[ ! -f "$MODEL" ]]; then
echo "frank-primary: model missing at $MODEL" >&2
exit 1
fi
if [[ ! -x "$BIN" ]]; then
echo "frank-primary: binary missing at $BIN (image build failed?)" >&2
exit 1
fi
exec "$BIN" "$MODEL" \
--host 0.0.0.0 \
--port 7770 \
--model-name frank-primary \
--target-device hip:0 \
--max-ctx 131072 \
--chunk 1024 \
--ds4-prefill exact \
--prefix-cache-slots 0 \
--prefill-cache-slots 0
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
"""frank-primary health probe: 200 on /health only once the engine serves.
File-based (not an inline HealthCmd) because the podman 5.x v1 quadlet
generator splits HealthCmd on spaces into an argv array.
"""
import sys
import urllib.request
try:
with urllib.request.urlopen("http://127.0.0.1:7770/health", timeout=10) as r:
sys.exit(0 if r.status == 200 else 1)
except Exception:
sys.exit(1)
@@ -1,40 +0,0 @@
diff --git a/Dockerfile.rocm b/Dockerfile.rocm
index 4ea7a1d7..0e22355b 100644
--- a/Dockerfile.rocm
+++ b/Dockerfile.rocm
@@ -59,18 +59,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
git-lfs \
- hipblas-dev \
- hipblaslt-dev \
- hipcub-dev \
libcurl4-openssl-dev \
ninja-build \
pkg-config \
python3 \
- rocblas-dev \
- rocprim-dev \
- rocwmma-dev \
&& rm -rf /var/lib/apt/lists/*
+# Ubuntu jammy ships cmake 3.22, which predates the block command that
+# ROCm 7.14 hipblaslt-config.cmake requires (CMake >= 3.25). Install the
+# official 3.31 binary; /usr/local/bin precedes /usr/bin on PATH.
+RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-linux-x86_64.tar.gz | tar -xz -C /usr/local --strip-components=1
+
WORKDIR /src
# COPY ordering mirrors the CUDA Dockerfile: C++ build inputs first so the
@@ -152,11 +151,8 @@ ENV PATH=/opt/rocm/bin:${PATH}
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
- hipblas \
- hipblaslt \
libgomp1 \
pciutils \
- rocblas \
&& rm -rf /var/lib/apt/lists/*
# uv (pinned) manages Python 3.12 + resolves the luce + pflash
@@ -1,90 +0,0 @@
# Quadlet unit: frank-primary (rootless, boot-persistent).
#
# Install path on dankserver: ~/.config/containers/systemd/frank-primary.container
# Unit name: frank-primary.service (container name: frank-primary)
#
# The dankserver LLM endpoint (:7770, alias frank-primary): the Lucebox
# runtime (github.com/Luce-Org/lucebox, local image localhost/lucebox-rocm:
# gfx1201 built from Dockerfile.rocm with LUCE_HIP_ARCHES=gfx1201 and
# ROCM_VERSION=7.14.1-full to match the host driver's ROCm 7.x line)
# serving DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB GGUF, 40-layer MoE,
# 384 routed experts + 1 shared per layer, top-6 routing, Engram tables at
# layers 1 and 14) on the two Radeon AI PRO R9700s (gfx1201). The model
# cannot be resident: dense weights, the compressed MLA caches and a hot
# expert stack sit on hip:0, the streamed-expert slot cache on hip:1, and
# every other routed expert is pulled out of the mmapped page cache (246 GiB
# RAM holds most of the 179 GiB expert set; the rest reads from NVMe). Lazy
# mmap page-in is the engine default — no flag.
#
# The image's entrypoint only forwards a fixed Qwen/DFlash flag set and
# cannot express the DS4 flags, so the unit bypasses it (--entrypoint) and
# runs the repo's frank-primary-entrypoint.sh, which execs the native
# luce_server with the DS4 launch shape (see that script's header).
#
# SHAPE. Host network, :7770, served model name frank-primary, OpenAI /v1
# (Chat Completions with tool metadata, Responses, Anthropic Messages,
# Open WebUI metadata) — the fleet contract is unchanged from
# frank-medium.
#
# DEVICES. /dev/kfd + /dev/dri through the amdgpu/KFD stack. Both R9700s are
# visible (indices 0 and 1; the Raphael iGPU enumerates as index 2 and stays
# out). They are NOT driven as a symmetric pair: the in-process expert-
# parallel path (LUCE_DS4_MOE_TP_INPROC) is qualified for gfx1151+gfx1201
# pairs, not gfx1201+gfx1201. Instead hip:0 owns the dense weights and the
# hot expert stack while LUCE_EXPERT_STREAM_DEVICE=1 (entrypoint) gives hip:1
# the streamed-expert slot cache, so each card carries one tier and neither
# waits on the other.
#
# MOUNTS. /models (ro — the GGUF is mmapped, nothing writes), and the
# entrypoint + healthcheck scripts straight out of this checkout
# (deploy.sh git-pulls the checkout before dispatching).
#
# IMAGE. localhost/-aliased tag: this host's podman quadlet path cannot
# resolve registry-prefixed refs from local storage at create time (same
# as frank-medium). Pull=never keeps boots off the registry.
#
# CUTOVER. This unit REPLACES frank-medium on :7770. The deploy handler
# (deploy_llm_primary in scripts/deploy-remote.sh) stops and masks the
# radiance unit before starting this one; the radiance unit is retired to
# the home trash after frank-primary is verified (trash policy, never rm).
#
# DO NOT name an environment variable TEMP, HOST or GPU_MAX_HEAP_SIZE: the
# HIP runtime reads bare TEMP for its JIT cache path (2026-08-13 incident).
[Unit]
Description=frank-primary — Lucebox DeepSeek-V4.1-Flash-ROCMFP23 on R9700 (gfx1201), experts in RAM
After=network-online.target
[Container]
ContainerName=frank-primary
Image=localhost/lucebox-rocm:gfx1201
# Host network: luce_server binds 0.0.0.0:7770 directly; no PublishPort.
AddDevice=/dev/kfd
AddDevice=/dev/dri
GroupAdd=keep-groups
PodmanArgs=--privileged --ipc=host --security-opt seccomp=unconfined --cap-add SYS_PTRACE --network=host --entrypoint /usr/local/bin/frank-primary-entrypoint.sh
# Both R9700s: hip:0 target, hip:1 the streamed-expert slot cache. Index 2,
# the Raphael iGPU, stays out.
Environment=HIP_VISIBLE_DEVICES=0,1
Environment=ROCR_VISIBLE_DEVICES=0,1
# The 377 GiB GGUF, mmapped; pages pull from disk/page cache on demand.
Volume=/home/dank/models:/models:ro
# Entrypoint + healthcheck from this checkout, mounted read-only.
Volume=/home/dank/serverAppConfigs/dankserver/frank-primary/frank-primary-entrypoint.sh:/usr/local/bin/frank-primary-entrypoint.sh:ro
Volume=/home/dank/serverAppConfigs/dankserver/frank-primary/frank-primary-healthcheck.py:/usr/local/bin/frank-primary-healthcheck.py:ro
# 200 only once the engine serves; first start page-ins the dense weights
# and builds the graphs, which takes a while. HealthCmd is split on spaces
# into an argv array by the podman 5.x v1 generator, hence the file probe.
HealthCmd=python3 /usr/local/bin/frank-primary-healthcheck.py
HealthInterval=60s
HealthTimeout=30s
HealthRetries=3
HealthStartPeriod=1800s
Pull=never
[Service]
TimeoutStartSec=1800
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
+12 -7
View File
@@ -371,13 +371,11 @@ def services_for_path(path: str, target: str) -> set[str]:
if name == "node-exporter":
_add(services, "node-exporter")
return services
if len(parts) >= 2 and parts[1] == "frank-primary":
# frank-primary is the Lucebox DeepSeek V4 Quadlet (2026-09-26
# cutover, replacing frank-medium on :7770): its entrypoint and
# healthcheck live under this directory and the container mounts
# them straight out of this checkout, so any file here changes
# deployed state.
_add(services, "frank-primary")
# frank-primary's quadlet-era directory left the tree on 2026-09-26
# (the Lucebox DS4.1 unit was retired for the llama.cpp root unit —
# the trio in scripts/ now maps it). Nothing lives under
# dankserver/frank-primary/ except dated trash, and trash plans
# nothing (the retire rule above the service blocks).
return services
if parts[0] == "frankbook-pro":
@@ -421,6 +419,13 @@ def services_for_path(path: str, target: str) -> set[str]:
# what the live frank-large endpoint renders, so it plans
# frank-large.
_add(services, "frank-large")
if name in {"frank-primary.env", "frank-primary.service", "serve-frank-primary.sh"}:
# llama.cpp frank-primary (2026-09-26, replacing the Lucebox
# DS4.1 quadlet) keeps its unit, env and serve files here the
# same way: a root unit on the unsloth b11030-mix prebuilt
# serving Qwen3.8-Flash-Next Q8_0. The download script fetches
# the payload, which is host-side state, so it selects nothing.
_add(services, "frank-primary")
# Deployment/test/benchmark scripts and other repo tooling do not
# restart a runtime service.
return services
+87 -41
View File
@@ -1178,52 +1178,98 @@ deploy_llm_small() {
wait_for_health frank-small http://127.0.0.1:7771/health 900
}
# deploy_llm_primary — frank-primary is the Lucebox DeepSeek V4 Quadlet
# (the 2026-09-26 cutover that replaces frank-medium on :7770). The model
# is a 377 GiB GGUF and the image is a local build, so the gates are:
# image present, model file present at its declared byte count (a partial
# download must never be served).
# deploy_llm_primary — frank-primary is unsloth llama.cpp (the b11030-mix
# prebuilt, ROCm gfx120X, bundled runtime) serving Qwen3.8-Flash-Next at
# Q8_0 on the two R9700s, as a ROOT unit — the same shape as frank-large on
# Dankclaw: a host binary, no container image, no generated unit.
# relink_unit points /etc/systemd/system/frank-primary.service at the repo
# file and boot persistence is that unit's [Install] section.
#
# The 2026-09-26 swap that replaced the Lucebox DS4.1 Quadlet on this port.
# The payload is six Q8_0 split parts plus the shared MTP head and the
# projector, so the gate walks every artifact at its declared byte count (a
# partial download must never be served — and stat -c %s, never wc -c, which
# read the whole 377 GiB lucebox file for minutes on 2026-09-26).
LLM_PRIMARY_RUNTIME=/home/dank/GitPackages/unsloth-llama-b11030/llama-server
LLM_PRIMARY_ROOT=/home/dank/models/Qwen3.8-Flash-Next-GGUF
LLM_PRIMARY_PART_SIZES=(
10946624
682434912
54400261312
49446841216
49668930400
34015618784
)
LLM_PRIMARY_DRAFT="$LLM_PRIMARY_ROOT/MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf"
LLM_PRIMARY_DRAFT_SIZE=2786568256
LLM_PRIMARY_MMPROJ="$LLM_PRIMARY_ROOT/mmproj-BF16.gguf"
LLM_PRIMARY_MMPROJ_SIZE=907542944
# The lucebox quadlet held this port until the 2026-09-26 cutover. Its
# generated user unit is host state, not repo state: stop it and move the
# installed .container into dated home trash (the same retire pattern as
# retire_halogen_large_quadlet). The image and the 377 GiB payload stay on
# disk as the rollback target.
retire_lucebox_primary_quadlet() {
local trash="$HOME/.AGENTTRASH/20260926-llamacpp-cutover/quadlet"
systemctl --user stop frank-primary.service 2>/dev/null || true
if [[ -e "$quadlet_unit_dir/frank-primary.container" ]]; then
mkdir -p "$trash"
mv -- "$quadlet_unit_dir/frank-primary.container" "$trash/frank-primary.container"
echo "retired quadlet frank-primary.container -> $trash"
fi
systemctl --user daemon-reload
}
deploy_llm_primary() {
local missing=0
local model="/home/dank/models/DeepSeek-V4.1-Flash/DeepSeek-V4.1-Flash-ROCMFP23.gguf"
local want_size=405349859328
if ! podman image exists localhost/lucebox-rocm:gfx1201; then
echo "ERROR: lucebox image absent (localhost/lucebox-rocm:gfx1201)" >&2
echo "frank-primary: podman build --build-arg LUCE_HIP_ARCHES=gfx1201 --build-arg ROCM_VERSION=7.14.1-full -t localhost/lucebox-rocm:gfx1201 -f Dockerfile.rocm . (see runbook)" >&2
# Refuse rather than restart-loop. A unit whose runtime or payload is
# missing never becomes healthy, and a green deploy would leave the
# endpoint dark with nothing in the deploy log to say why.
local missing=0 i part want_size got_size
if [[ ! -x "$LLM_PRIMARY_RUNTIME" ]]; then
echo "ERROR: runtime absent at $LLM_PRIMARY_RUNTIME" >&2
echo "frank-primary: fetch the unsloth b11030-mix gfx120X prebuilt (see the runbook)" >&2
missing=1
fi
# stat -c %s, never `wc -c < FILE`. wc falls back to reading the whole
# stream whenever its fstat shortcut does not apply, and this payload is
# 377 GiB: on 2026-09-26 a deploy sat in wc for minutes (30 s of CPU,
# NVMe-bound) before it could even reach the cutover.
got_size="$(stat -c %s "$model" 2>/dev/null || echo 0)"
if [[ "$got_size" != "$want_size" ]]; then
echo "ERROR: model absent or incomplete at $model (have $got_size, want $want_size)" >&2
missing=1
fi
[[ "$missing" -eq 0 ]] || return 1
# Cutover COMPLETED 2026-09-26: frank-primary took :7770 and the radiance
# unit is retired. The host-side unit, its mask and the repo files were all
# moved to trash by the parent session, so there is nothing left to stop or
# mask here — masking a name that no longer exists would only resurrect it
# in the unit search path. The port gate stays, because 7770 is a
# host-network bind: nothing new can be created until the holder released it.
# 7770 is a host-network bind: nothing new can be created until the old
# holder released it (the same gate as the 2026-09-16 cutover).
sleep 2
wait_port_free 7770 || return 1
install_quadlet frank-primary dankserver/frank-primary/quadlet frank-primary
# No enable: generated units cannot be enabled on this host (v1 quadlet
# generator); boot persistence is the unit's [Install] WantedBy=default.target.
systemctl --user start frank-primary.service
# First start page-ins the dense weights and builds the graphs;
# HealthStartPeriod in the unit is 1800s to match.
wait_container_healthy frank-primary 1800 || {
podman logs --tail 40 frank-primary 2>&1 || true
for i in 1 2 3 4 5 6; do
part="$LLM_PRIMARY_ROOT/Q8_0/Qwen3.8-Flash-Next-Q8_0-0000$i-of-00006.gguf"
want_size="${LLM_PRIMARY_PART_SIZES[$((i-1))]}"
got_size="$(stat -c %s "$part" 2>/dev/null || echo 0)"
if [[ "$got_size" != "$want_size" ]]; then
echo "ERROR: part $i absent or incomplete at $part (have $got_size, want $want_size)" >&2
missing=1
fi
done
for part in "$LLM_PRIMARY_DRAFT:$LLM_PRIMARY_DRAFT_SIZE" \
"$LLM_PRIMARY_MMPROJ:$LLM_PRIMARY_MMPROJ_SIZE"; do
local file="${part%%:*}" want="${part##*:}"
got_size="$(stat -c %s "$file" 2>/dev/null || echo 0)"
if [[ "$got_size" != "$want" ]]; then
echo "ERROR: artifact absent or incomplete at $file (have $got_size, want $want)" >&2
missing=1
fi
done
[[ "$missing" -eq 0 ]] || {
echo "frank-primary: run scripts/download-frank-primary.sh on this host" >&2
return 1
}
retire_lucebox_primary_quadlet
# On a re-deploy the predecessor is THIS unit: stop it before waiting on
# the port, or the wait deadlocks on the server it just stopped serving.
run_privileged systemctl stop frank-primary.service 2>/dev/null || true
# Nothing starts until the port is released: the lucebox cutover on
# 2026-09-26 and the frank-large cutover on 2026-09-16 both failed
# exactly here when the next engine bound 7770 a second too early.
wait_port_free 7770 || return 1
relink_unit frank-primary
run_privileged systemctl daemon-reload
run_privileged systemctl enable frank-primary.service
run_privileged systemctl restart frank-primary.service
# Cold start page-ins 175 GiB of expert pages and builds the graphs;
# 1800s parity with the lucebox HealthStartPeriod this port has always
# needed.
wait_for_health frank-primary http://127.0.0.1:7770/health 1800
}
wait_for_health() {
+78
View File
@@ -0,0 +1,78 @@
# frank-primary — unsloth llama.cpp (b11030-mix prebuilt, ROCm gfx120X)
# serving unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 on the two Radeon AI PRO
# R9700s (gfx1201), with the shared Q8_0 MTP drafter head, the BF16 vision
# projector, and the draft-mtp + ngram-mod spec chain.
#
# These are the tunables read by scripts/serve-frank-primary.sh. The flag
# list is owned by the serve script; this file only sets fleet values.
#
# DO NOT name a new tunable TEMP, HOST or GPU_MAX_HEAP_SIZE: the HIP
# runtime reads those bare (the 2026-08-13 TEMP incident). The bind address
# is BIND_HOST, not HOST — frank-large's env predates that lesson and gets
# away with it; do not copy the shape.
# The payload (scripts/download-frank-primary.sh): the serve script refuses
# to start without all six Q8_0 parts, the MTP head and the projector.
MODEL_ROOT=/home/dank/models/Qwen3.8-Flash-Next-GGUF
# The runtime: the unsloth b11030-mix prebuilt for gfx120X (the same
# b11030-mix build frank-large runs on gfx1151 — it bundles its own ROCm
# runtime, no host SDK gate).
RUNTIME_DIR=/home/dank/GitPackages/unsloth-llama-b11030
# Fleet endpoint: the port and alias the lucebox DS4.1 quadlet held before
# the 2026-09-26 swap, so no client, probe or models.yml entry changes
# address.
BIND_HOST=0.0.0.0
PORT=7770
# One conversation, the model's full native 262144 window.
CONTEXT_SIZE=262144
PARALLEL_SLOTS=1
# Spec chain: draft-mtp (the shared Q8_0 head) at depth 2, with the
# draftless ngram-mod matcher chained behind it. Depth 2 is the measured
# optimum class for this pair at long contexts (the retired Dankclaw
# Flash-Next deployment ran exactly this head at depth 2 with 0.83-0.92
# acceptance); each extra verified position re-batches the CPU expert reads,
# so depth buys less than bandwidth here.
SPEC_DRAFT_N_MAX=2
# Device placement. -ngl 999 puts every layer's non-overridden tensors on
# the GPUs; the big overrides then move the bandwidth-huge tensors to host
# RAM (computed there — the x4-pair measurement: streaming over PCIe loses,
# computing in RAM does not). OT_OVERRIDES is comma-separated
# <tensor-pattern>=<backend> pairs passed as repeated --override-tensor
# flags:
# * exps=CPU — the routed experts. ~100 GiB at Q8, read once per token;
# the CPU backend computes them from page cache.
# * the n-gram embedding block (the 51B-param PLE table) — sparse row
# lookups, tiny PCIe traffic from CPU RAM; NEVER pin with a GPU target
# (the 23 -> 13.4 t/s regression).
# * per_layer_token_embd.weight=CPU — REQUIRED by the shared MTP head's
# borrow path: GPU-resident, it crashes the draft load with
# "invalid vector subscript" (llama.cpp PR #28243).
# The exact tensor pattern for the n-gram block was discovered at load time
# on 2026-09-26 (see the runbook); this list is the deployed set.
NGL=999
OT_OVERRIDES=exps=CPU,ngram_embeddings=CPU,per_layer_token_embd.weight=CPU
TENSOR_SPLIT=1,1
# The drafter lives on the first R9700 (the card the load log shows holding
# the shared embedding borrow path).
SPEC_DRAFT_DEVICE=ROCm0
# Batch geometry: logical batch 8192 with ubatch 512 — the measured optimum
# class on this fleet's ROCm stack (a bigger logical batch is free; compute
# buffers reserve at max graph size). ubatch 512 balances prefill
# throughput against the asymmetric compute-buffer cost on the card that
# holds the output layers and the drafter.
BATCH_SIZE=8192
UBATCH_SIZE=512
# Prompt-cache budget: 16 GiB. The KV itself is small (the hybrid
# DeltaNet/QSA pattern keeps a 262144 window a fraction of the full-ctx
# x all-layers number), so the prompt cache is the RAM-side spend that
# actually moves prefill on repeat traffic. Host RAM budget: ~175 GiB of
# expert pages + this cache + the server's own arenas inside 246 GiB.
CACHE_RAM=16384
+21
View File
@@ -0,0 +1,21 @@
[Unit]
Description=unsloth llama.cpp (b11030-mix, ROCm gfx120X) Qwen3.8-Flash-Next Q8_0, shared-Q8_0 MTP + ngram-mod spec chain, BF16 vision, 262144 ctx / 1 slot, root systemd unit (dankserver, frank-primary, 2x Radeon AI PRO R9700)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dank
Group=dank
# The model is mmap'd (175 GiB of expert pages + the 51B-param n-gram
# table live in page cache); an RLIMIT_MEMLOCK smaller than what the
# runtime pins would fail the load mid-start. Parity with the other LLM
# units on this fleet.
LimitMEMLOCK=infinity
Environment="FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env"
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
+134
View File
@@ -0,0 +1,134 @@
#!/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[@]}"
+8 -8
View File
@@ -187,16 +187,16 @@ for path in "${CHANGED[@]}"; do
# as additions, with "no focused-test mapping".
.AGENTTRASH/*|*/.AGENTTRASH/*)
;;
# frank-primary is the lucebox DS4.1 Quadlet (2026-09-26 cutover on
# :7770): its entrypoint and healthcheck live under
# dankserver/frank-primary/ and the container mounts them straight out
# of the checkout, so any file here changes deployed state. The
# radiance quadlet that held this endpoint before that date is archive
# material in dankserver/.AGENTTRASH/ and maps to nothing.
dankserver/frank-primary/*)
# frank-primary is llama.cpp (the unsloth b11030-mix prebuilt) on the
# two R9700s as a ROOT unit — the 2026-09-26 swap that replaced the
# Lucebox DS4.1 quadlet on :7770. Its env, unit and serve script all
# live in scripts/, which is why this case has to precede the generic
# scripts/*.service and scripts/* fallbacks below.
scripts/frank-primary.*|scripts/serve-frank-primary.sh)
add_test tests/frank_primary_unit.bats
add_test tests/llm_units.bats
add_test tests/deployment_scoping.bats
add_test tests/docs_completeness.bats
add_test tests/scripts_syntax.bats
;;
# frank-large's chat template lives in scripts/ (moved there on
# 2026-09-26 when its old home was retired): a template edit changes
@@ -0,0 +1,135 @@
---
type: configuration
title: frank-primary llama.cpp Qwen3.8-Flash-Next Q8_0 Runbook
description: ACTIVE frank-primary runbook — unsloth llama.cpp (the b11030-mix prebuilt, ROCm gfx120X, bundled runtime, root systemd unit) serving unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 (175.3 GiB across six split parts) on Dankserver's two Radeon AI PRO R9700s (gfx1201), with the shared Q8_0 MTP drafter (depth 2) chained behind ngram-mod, BF16 vision, and the routed experts + 51B-param n-gram embedding table computed in host RAM. Port 7770, one stream, full 262,144 ctx. Cutover 2026-09-26, replacing the same-day Lucebox DS4.1 deployment.
tags:
- frank-primary
- dankserver
- llamacpp
- unsloth
- qwen38-flash-next
- q8_0
- mtp
- ngram-mod
- spec-decode
- vision
- rocm
- gfx1201
- r9700
timestamp: 2026-09-26
---
# frank-primary llama.cpp Qwen3.8-Flash-Next Q8_0 Runbook
The ACTIVE dankserver LLM endpoint (:7770, alias **frank-primary**):
unsloth llama.cpp serving `unsloth/Qwen3.8-Flash-Next-GGUF` at **Q_8**
(the 6-part `Q8_0/` main, 175.30 GiB) on **both** Radeon AI PRO R9700s,
with the **shared Q_8 MTP drafter** (`draft-mtp`, depth 2) layered with
**`ngram-mod`**, and **vision** through `mmproj-BF16.gguf`. It replaces the
Lucebox DS4.1 quadlet that held the port for hours earlier the same day
([[frank-primary-lucebox-ds4]]), which itself replaced
[[frank-medium-radiance-mxfp4]].
## Why this shape
- **One runtime for the fleet.** All three endpoints are llama.cpp now —
one metric namespace (`llamacpp:*`), one reasoning contract
(`reasoning_content`), one tool contract (`--jinja`). The two-day
experiment with a single-vendor engine (lucebox) ended because Qwen
Flash-Next at Q8 on llama.cpp delivers better quality than the DS4.1
fp2/fp3 stream, plus vision, plus the full 262k window, at comparable
speed.
- **The placement law, not a preference:** the routed experts and the
51B-param n-gram (PLE) embedding table are **computed in host RAM**
(`-ot exps=CPU,ngram_embeddings=CPU`) and never streamed over PCIe. The
x4-pair measurement on the same model shape: streaming loses (3.7 t/s),
computing in RAM does not care (20+ t/s). The dense weights, attention,
projector and MTP head sit on the two cards (~29 GiB); the ~100 GiB of
expert pages live in mmap page cache — `--no-mmap` is forbidden (it
forces the table into anonymous memory; the cgroup kill is silent) and
pinning the PLE table with a GPU target cost 23 → 13.4 t/s when tried.
- **Two cards, layer split** (`-ts 1,1`): both R9700s are on CPU root
ports with full PCIe atomics (the ProArt B850-Creator swap,
2026-09-20), but this deployment uses llama.cpp's layer split —
`-ot` tensor overrides do not compose with tensor-parallel mode, and
`exps=CPU` is the override the placement law needs. The 27B-dense
equivalent on this host measured 1991 t/s prefill / 18.8 t/s decode in
this shape; Flash-Next Q8 with CPU experts lands in the same class
(measured numbers below).
- **Spec chain: `draft-mtp,ngram-mod`.** MTP is a property of the FILE:
the shared-Q8_0 head (`mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf`,
2,786,568,256 B) is loaded via `--model-draft` and borrows the target's
embedding tensors — which is why `per_layer_token_embd.weight` must be
CPU-resident in the override set (a GPU-resident borrow crashes the
draft load with `invalid vector subscript`). `ngram-mod` is draftless,
so it chains legally behind the drafter. Depth 2: each longer
verification round re-batches the CPU expert reads, so depth buys less
than bandwidth here.
- **Vision, explicitly.** This build never autoloads projectors — without
`--mmproj` the server comes up healthy and text-only and answers image
requests with 500 (the frank-medium 2026-09-17 trap). The BF16 projector
(907,542,944 B) is named in the serve script.
## Deployment
|Piece|Value|
|---|---|
|Unit|`scripts/frank-primary.service` — root systemd unit, `User=dank`, `Restart=on-failure`, `LimitMEMLOCK=infinity`, relinked to `/etc/systemd/system/`|
|Serve script|`scripts/serve-frank-primary.sh` — validates binary + all 6 parts + drafter + mmproj before exec|
|Env tunables|`scripts/frank-primary.env` — paths and numbers only; bind is `BIND_HOST` (never a bare `HOST`: HIP reads it)|
|Runtime|`/home/dank/GitPackages/unsloth-llama-b11030/llama-server` — unsloth b11030-mix prebuilt (commit 6ba30d05b), ROCm gfx120X, bundled runtime via `LD_LIBRARY_PATH`|
|Payload|`/home/dank/models/Qwen3.8-Flash-Next-GGUF/` — `Q8_0/` (6 parts), `MTP/mtp-...-shared-Q8_0.gguf`, `mmproj-BF16.gguf`; fetched by `scripts/download-frank-primary.sh` (sha256 + byte-count verified)|
|Port / alias|7770 / `frank-primary` — unchanged from the lucebox and radiance eras; no client moves|
|Context|262,144 × 1 slot (`PARALLEL_SLOTS=1`, one stream, the model's full native window)|
|Sampling|Greedy server default (deterministic e2e); Qwen-recommended sampling rides `models.yml` `compat.extraBody` per request|
|Template|The GGUF's embedded template via `--jinja`; reasoning → `reasoning_content` (`--reasoning-format deepseek`)|
|Observability|`frank-primary` Prometheus job at `192.168.0.235:7770` — cross-host LAN scrape (the netavark published-port quirk that forces `host.containers.internal` is same-host only); exports `llamacpp:*`|
### Cutover mechanics
The deployer (`deploy_llm_primary` in `scripts/deploy-remote.sh`) gates on
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
(`~/.AGENTTRASH/20260926-llamacpp-cutover/`), waits for port 7770 to be
truly free, relinks + enables + restarts the root unit, and waits up to
1800 s for `/health` (cold start page-ins ~175 GiB and builds the graphs).
## Verification (2026-09-26 deploy)
- `/v1/models` reports `frank-primary`.
- Chat completion returns `content` + `reasoning_content`.
- Tool call returns well-formed `tool_calls` (`--jinja`).
- Real image request answered from the projector (not a health check —
health cannot see vision).
- Spec chain live: `draft acceptance` lines in the unit journal; ngram-mod
has no startup banner (identify behaviourally: `draft_n` > 0 where text
repeats).
- `reasoning_effort: high` → 200 (the template's `{xhigh, medium, low}`
gate is folded by `reasoningEffortMap` in `models.yml`).
- VRAM/RAM headroom: `rocm-smi` per card + `free -g` after the first long
completion.
_Measured numbers land here after the deploy battery._
## Rollback
The lucebox DS4.1 material is fully retained on the host: image
`localhost/lucebox-rocm:gfx1201`, the 378 GiB payload, and the quadlet-era
repo files in `dankserver/.AGENTTRASH/20260926-llamacpp-cutover/` (see
[[frank-primary-lucebox-ds4]]). Restore = move the `.container` back,
uninstall the root unit (`systemctl disable --now frank-primary` — the
link, not the file; never `systemctl disable` a unit whose file must
survive), `daemon-reload`, start the user quadlet. The Qwen payload
(~179 GiB) stays fetched, so a forward re-swap is a redeploy, not a
refetch.
## Fleet context
- [[frank-large-unsloth-qwen38-27b-q8xl]] — Dankclaw :7770, the same
b11030-mix binary family on gfx1151.
- [[frank-small-gemma4-12b]] — Dankclaw :7771, mainline llama.cpp on the
3090; the only audio-capable endpoint.
- The harness contract lives in `/home/blank/.omp/agent/models.yml`
(`llamacpp (dankserver)` provider, `contextWindow 262144`,
`maxTokens 65536`, `input: [text, image]`).
@@ -1,7 +1,7 @@
---
type: configuration
title: frank-primary Lucebox DeepSeek V4 Runbook
description: ACTIVE frank-primary runbook — the Lucebox runtime (github.com/Luce-Org/lucebox, local image localhost/lucebox-rocm:gfx1201) serving DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB GGUF, 43-layer MoE, 256 routed experts, top-6) on Dankserver's R9700 with the routed experts computed on CPU from the mmapped page cache. Replaces frank-medium (vllm-radiance TP=2) on :7770, cutover 2026-09-26.
description: RETIRED 2026-09-26 frank-primary runbook (active for hours on 2026-09-26) — the Lucebox runtime (github.com/Luce-Org/lucebox, local image localhost/lucebox-rocm:gfx1201) serving DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB GGUF, 43-layer MoE, 256 routed experts, top-6) on Dankserver's R9700 with the routed experts computed on CPU from the mmapped page cache. Replaced the same day by [[frank-primary-llamacpp-qwen38-flash-next-q8]] (llama.cpp Qwen3.8-Flash-Next Q8_0); payload and image retained on the host as the rollback target.
tags:
- frank-primary
- dankserver
@@ -18,14 +18,22 @@ timestamp: 2026-09-26
# frank-primary Lucebox DeepSeek V4 Runbook
The ACTIVE dankserver LLM endpoint (:7770, alias **frank-primary**): the
> [!warning] RETIRED 2026-09-26 (same day it went live)
> Replaced by [[frank-primary-llamacpp-qwen38-flash-next-q8]] — unsloth
> llama.cpp serving Qwen3.8-Flash-Next Q8_0 on both R9700s. This runtime
> served for hours; the rollback material (image `localhost/lucebox-rocm:
> gfx1201` and the 378 GiB payload) is retained on the host, and the
> quadlet-era files live in `dankserver/.AGENTTRASH/20260926-llamacpp-cutover/`.
The dankserver LLM endpoint (:7770, alias **frank-primary**) as it stood on
2026-09-26: the
[Lucebox](https://github.com/Luce-Org/lucebox) runtime serving
`Lucebox/DeepSeek-V4.1-Flash-ROCMFP23-GGUF` (377.2 GiB, 405,349,859,328
bytes, LFS oid
`2daea6ca1715bc987aae201e63445d3f7abdc2337c05f5f569f5e50237ce8be2`) on a
single Radeon AI PRO R9700 (gfx1201). It replaces
[[frank-medium-radiance-mxfp4]] (vllm-radiance TP=2), retired on
2026-09-26.
single Radeon AI PRO R9700 (gfx1201). It replaced
[[frank-medium-radiance-mxfp4]] (vllm-radiance TP=2), retired earlier the
same day.
## Why this shape
+40 -27
View File
@@ -1,14 +1,14 @@
# LLM Fleet — Knowledge Base
Documentation for the LLM inference fleet on the two hosts (2026-09-21):
**vllm-radiance on Dankserver** serving frank-medium as ONE tensor-parallel
process across both Radeon AI PRO R9700 cards (just1moremodel
Qwen3.8-27B-Uncensored-MXFP4 + the z-lab DFlash2 drafter, bf16 KV, TP=2 over the
R4D one-shot allreduce), and TWO endpoints on Dankclaw — stock llama.cpp
(unsloth b11030, ROCm gfx1151; frank-large, unsloth Qwen3.8-27B UD-Q8_K_XL +
the z-lab DFlash2 Q4_K_M drafter on the gfx1151 iGPU, port 7770) and mainline
llama.cpp (frank-small, HauhauCS Gemma4-12B QAT Q4_K_M with MTP and vision on
the RTX 3090, port 7771).
Documentation for the LLM inference fleet on the two hosts (2026-09-26):
**llama.cpp on all three endpoints** — Dankserver runs unsloth llama.cpp
(the b11030-mix prebuilt, ROCm gfx120X) serving frank-primary
(unsloth Qwen3.8-Flash-Next Q8_0 + the shared Q8_0 MTP head + ngram-mod +
vision) across both Radeon AI PRO R9700s on :7770, and Dankclaw runs two
endpoints — stock llama.cpp (unsloth b11030, ROCm gfx1151; frank-large,
unsloth Qwen3.8-27B UD-Q8_K_XL + the z-lab DFlash2 Q4_K_M drafter on the
gfx1151 iGPU, port 7770) and mainline llama.cpp (frank-small, HauhauCS
Gemma4-12B QAT Q4_K_M with MTP and vision on the RTX 3090, port 7771).
> 2026-08-22: the former `vllmScripts` repository is merged into the
> serverAppConfigs repository. This knowledge base moved from the old repo's
@@ -22,11 +22,10 @@ graph LR
subgraph Dankserver[AMD Dankserver - Ubuntu 26.04]
A[2x Radeon AI PRO R9700
gfx1201, 32 GiB each]
B2[vllm-radiance 0.9.3, TP=2
frank-medium, port 7770]
C[Qwen3.8-27B MXFP4 + DFlash2 drafter
ONE process on both cards, 262144 ctx, up to 8 seqs]
A --> B2 --> C
B2[unsloth llama.cpp b11030-mix, ROCm gfx120X
frank-primary, port 7770]
C[Qwen3.8-Flash-Next Q8_0 + shared-Q8_0 MTP head
layer split on both cards, 262144 ctx / 1 slot, ngram-mod, vision]
end
subgraph Dankclaw[AMD Strix Halo - Ubuntu 26.04]
@@ -53,20 +52,34 @@ frank-small, port 7771]
| Server | GPU | VRAM / Memory | OS |
| --------------------------------------- | ----------------------------- | -------------------- | ------------ |
| [[Dankserver System Specs\|Dankserver]] | 2x AMD Radeon AI PRO R9700 | 2x 32 GB VRAM, 60 GB RAM | Ubuntu 26.04 |
| [[Dankserver System Specs\|Dankserver]] | 2x AMD Radeon AI PRO R9700 | 2x 32 GB VRAM, 246 GB RAM | Ubuntu 26.04 |
| [[Dankclaw System Specs\|Dankclaw]] | AMD Radeon 8060S (Strix Halo) | gfx1151 unified memory | Ubuntu 26.04 |
## Active Deployment
- [[frank-medium-radiance-mxfp4]] — the ACTIVE frank-medium runbook:
vllm-radiance (stilldeadcode/vllm-radiance:0.9.3) serving
just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq (MXFP4/quark W4A8) with the
z-lab DFlash2-FP8 drafter as ONE tensor-parallel process (TP=2) behind
:7770, now on unquantized bf16 KV (`KV_DTYPE=auto`, cut 2026-09-21 to test
whether the endpoint's historical fp8 KV had been degrading W4A8 quality).
The FP8 checkpoint (orcarouter/Qwen3.8-27B-Uncensored-FP8, the 09-20→09-21
shape) is the on-disk rollback. The three-arm battery that chose TP=2, the
auto-sized KV pool, and verified capabilities (tools, reasoning)
- [[frank-primary-llamacpp-qwen38-flash-next-q8]] — the ACTIVE
frank-primary runbook (2026-09-26, root systemd unit, port 7770):
unsloth llama.cpp (b11030-mix prebuilt, ROCm gfx120X, bundled runtime)
serving unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0 across both R9700s —
routed experts and the 51B-param n-gram table computed in host RAM
(`-ot exps=CPU,ngram_embeddings=CPU`), the shared Q8_0 MTP head
(depth 2) chained behind ngram-mod, BF16 vision, one full 262,144-token
conversation, `reasoning_content`, the placement law, the cutover and
rollback
- [[frank-primary-lucebox-ds4]] — RETIRED 2026-09-26 (same-day swap,
rollback target; active for hours): the Lucebox DS4.1 engine
(localhost/lucebox-rocm:gfx1201) serving
Lucebox/DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB) with streamed experts —
the fleet's only non-llama.cpp runtime detour
- [[frank-medium-radiance-mxfp4]] — RETIRED 2026-09-26 (replaced by
[[frank-primary-lucebox-ds4]], then
[[frank-primary-llamacpp-qwen38-flash-next-q8]]): vllm-radiance 0.9.3
serving just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq (MXFP4/quark
W4A8) with the z-lab DFlash2-FP8 drafter as ONE tensor-parallel process
(TP=2) behind :7770, on unquantized bf16 KV (`KV_DTYPE=auto`, cut
2026-09-21). The FP8 checkpoint is the on-disk rollback. The three-arm
battery that chose TP=2, the auto-sized KV pool, and the verified
capability battery (tools, reasoning)
- [[frank-medium-model-swap]] — how to swap the checkpoint behind frank-medium:
the streamlining toolchain (hf-manifest.py, hf-fetch.sh,
swap-frank-medium.sh), the quantization-aware entrypoint contract, the
@@ -181,9 +194,9 @@ frank-small, port 7771]
on Dankclaw (superseded 2026-08-24 by the kingjones ROCmFP4 model)
- [[kairic-edge]] — the retired Kairic Edge IU4 experiment
(deployed and reverted 2026-08-22)
- [[Dankserver System Specs]] — the 2x R9700 box (frank-medium)
- [[Dankclaw System Specs]] — the Strix Halo gfx1151 box (frank-large on the
Kairic Edge runtime since 2026-09-21, frank-small on the RTX 3090)
- [[Dankserver System Specs]] — the 2x R9700 box (frank-primary)
- [[Dankclaw System Specs]] — the Strix Halo gfx1151 box (frank-large on
the unsloth b11030 runtime since 2026-09-21, frank-small on the RTX 3090)
- [[frank-npu]] — RETIRED 2026-09-11: the FLM NPU sight+sound endpoint
on Dankclaw (Gemma4 E4B vision+audio pack, XDNA2, port 7771), kept as
the retirement record — the IOMMU/SVA err=-19 root cause and where
+12 -6
View File
@@ -124,12 +124,18 @@ The LLM inference fleet on the two hosts. Full index:
- [[deployment]] — unified deploy and update process for both servers
- [[frank-redo-2026-08-17]] — the 2026-08-17 topology change
- [[frank-primary-lucebox-ds4]] — the ACTIVE frank-primary runbook:
the Lucebox runtime serving DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB
GGUF, 43-layer MoE, 256 routed experts top-6) on Dankserver's R9700
with the routed experts computed on CPU from the mmapped page cache —
replaces frank-medium on :7770 (cutover 2026-09-26)
- [[frank-medium-radiance-mxfp4]] — the ACTIVE frank-medium runbook:
- [[frank-primary-llamacpp-qwen38-flash-next-q8]] — the ACTIVE
frank-primary runbook: unsloth llama.cpp (b11030-mix prebuilt, ROCm
gfx120X, root unit) serving unsloth Qwen3.8-Flash-Next at Q8_0 (175.3
GiB) on Dankserver's both R9700s — the shared Q8_0 MTP head chained
behind ngram-mod, BF16 vision, the experts and the 51B n-gram table
computed in host RAM, the placement law, the cutover, and the rollback
- [[frank-primary-lucebox-ds4]] — RETIRED 2026-09-26 (same-day swap;
rollback target): the Lucebox runtime serving
DeepSeek-V4.1-Flash-ROCMFP23 (377.2 GiB GGUF, 43-layer MoE, 256 routed
experts top-6) on Dankserver's R9700 with the routed experts computed
on CPU from the mmapped page cache — held :7770 for hours
- [[frank-medium-radiance-mxfp4]] — RETIRED 2026-09-26:
vllm-radiance as ONE tensor-parallel process (TP=2 over the R4D one-shot
allreduce) serving just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq (MXFP4
W4A8, DFlash2 drafter, unquantized bf16 KV since 2026-09-21) on Dankserver's
@@ -1,7 +1,7 @@
{
"uid": "llm-fleet",
"title": "LLM fleet \u2014 capacity and reuse",
"description": "The signals that explain the LLM endpoints under load. One metric namespace: llamacpp:* for frank-small (Dankclaw, RTX 3090) and frank-large (Dankclaw, gfx1151 iGPU). frank-primary (Dankserver, the Lucebox DS4.1 engine since the 2026-09-26 cutover) exports no metrics and has no panels here. The endpoint picker selects between them. See serverAppConfigs-docs/Infrastructure/Observability.md for the namespace split and the addressability rules.",
"description": "The signals that explain the LLM endpoints under load. One metric namespace: llamacpp:* across all three endpoints — frank-small (Dankclaw, RTX 3090), frank-large (Dankclaw, gfx1151 iGPU) and frank-primary (Dankserver, 2x R9700, llama.cpp again since the 2026-09-26 swap back from Lucebox DS4.1). The endpoint picker selects between them. See serverAppConfigs-docs/Infrastructure/Observability.md for the namespace split and the addressability rules.",
"tags": [
"llm",
"capacity"
@@ -96,7 +96,7 @@
},
{
"expr": "job:llm_spec_accept_length:rate5m{job=~\"$job\"}",
"legendFormat": "{{job}} mean accepted length (vLLM)",
"legendFormat": "{{job}} mean accepted length",
"refId": "B"
},
{
@@ -110,7 +110,7 @@
"id": 3,
"type": "timeseries",
"title": "Requests waiting (queue depth)",
"description": "llama.cpp: anything above zero means the engine is serialising (with --parallel 1 a second concurrent request always shows up here). vLLM: the scheduler's own waiting/running gauges against MAXSEQS=8.",
"description": "llama.cpp: anything above zero means the engine is serialising (with --parallel 1 a second concurrent request always shows up here). All three endpoints are llama.cpp since the 2026-09-26 frank-primary swap.",
"gridPos": {
"h": 8,
"w": 12,
@@ -138,7 +138,7 @@
"id": 4,
"type": "timeseries",
"title": "Decode and prefill throughput",
"description": "llama.cpp: llamacpp:predicted_tokens_seconds is decode, llamacpp:prompt_tokens_seconds is prompt. vLLM: the token counters, rate'd. The series carry the job label of their scrape, so the endpoint picker selects between the runtimes.",
"description": "llamacpp:predicted_tokens_seconds is decode, llamacpp:prompt_tokens_seconds is prompt. The series carry the job label of their scrape, so the endpoint picker selects between the endpoints.",
"gridPos": {
"h": 8,
"w": 12,
@@ -166,7 +166,7 @@
"id": 5,
"type": "timeseries",
"title": "Decode steps and batch size",
"description": "llama.cpp-only signal \u2014 vLLM does not publish per-step counters. Steps per second and the busy-slot count per step (the effective batch).",
"description": "Per-step counters: steps per second and the busy-slot count per step (the effective batch). With frank-primary's --parallel 1 the slot count stays at 0 or 1 and says nothing about the card; with frank-small's 4 slots it tracks the agent-fleet burst shape.",
"gridPos": {
"h": 8,
"w": 12,
+7 -7
View File
@@ -36,8 +36,8 @@
Pick a model in the chat's model selector — the three models the fleet runs are
described <a href="#models">below</a>. Open WebUI keeps your chat history; the raw
model endpoints do not.</p>
<p>There is no audio input anywhere on the fleet. Text in, and — on
<code>frank-small</code> only — images too.</p>
<p>Images go to <code>frank-primary</code> or <code>frank-small</code>
(both see them). Audio (WAV) goes to <code>frank-small</code> only.</p>
</section>
<section id="models">
@@ -54,12 +54,12 @@
<li><strong>Not for:</strong> images. It cannot see them.</li>
</ul>
<h2>frank-medium — the primary model</h2>
<h2>frank-primary — the primary model</h2>
<ul>
<li><strong>Model:</strong> Qwen3.8-27B-Uncensored (MXFP4/W4A8, 19.4 GiB).</li>
<li><strong>Capacity:</strong> 262k-token context shared by up to eight conversations at once; sees images.</li>
<li><strong>Model:</strong> Qwen3.8-Flash-Next (Q8_0, 175.3 GiB) with MTP + n-gram speculative drafting on both R9700s.</li>
<li><strong>Capacity:</strong> 262k-token context, exactly one conversation at a time; sees images.</li>
<li><strong>Inputs:</strong> text, image.</li>
<li><strong>Best for:</strong> software engineering, system administration, and instruction tasks. Beats Claude Sonnet 5 per Artificial Analysis.</li>
<li><strong>Best for:</strong> software engineering, system administration, and instruction tasks.</li>
</ul>
<h2>frank-small — the fast utility model</h2>
@@ -72,7 +72,7 @@
<p class="foot-note">Which one to pick? Small utility work, RAG, search, and
screenshots: <code>frank-small</code>. Software engineering, sysadmin, and
instruction tasks: <code>frank-medium</code>. Cybersecurity and debugging:
instruction tasks: <code>frank-primary</code>. Cybersecurity and debugging:
<code>frank-large</code> (with Frank's permission).</p>
</section>
+2 -2
View File
@@ -281,12 +281,12 @@
"showLink": false
},
{
"name": "frank-medium",
"name": "frank-primary",
"category": "user",
"section": "AI",
"details": {
"role": "Primary model",
"spec": "Qwen3.8-27B-Uncensored (MXFP4/W4A8, 19.4 GiB) — 262k-token context across up to eight conversations; sees images. Beats Claude Sonnet 5 per Artificial Analysis.",
"spec": "Qwen3.8-Flash-Next (Q8_0, 175.3 GiB) — full 262k-token context, one conversation; sees images. Spec-drafted (MTP + n-gram) on both R9700s.",
"inputs": ["text", "image"],
"use": "Software engineering, system administration, and instruction tasks."
},
+24 -13
View File
@@ -79,35 +79,46 @@ scrape_configs:
# this container, not the guarded vhosts: these are machine clients and
# Prometheus cannot solve an Anubis challenge.
#
# One metric namespace: llamacpp:* — frank-large and frank-small, both
# llama.cpp with --metrics and both verified 2026-09-20 to export
# One metric namespace: llamacpp:* — frank-large, frank-small and
# frank-primary, all llama.cpp with /metrics (the family verified from
# /metrics on 2026-09-16 and 2026-09-20) and all exporting
# requests_deferred (the queue depth, the only form llama.cpp shows the
# capacity wall in), spec_decode_num_drafts_total, tokens_predicted_total,
# and the prompt_tokens_cached_total / prompt_tokens_total reuse pair.
#
# frank-large is scraped over host.containers.internal, not its own LAN address:
# this Prometheus is a bridge-networked container (Network=dankclaw_default),
# and on this netavark host a container can only reach the host's LAN IP at a
# PUBLISHED port — a host-process bind like llama-server's 0.0.0.0:7770 is
# refused at 192.168.0.143:7770 while it answers at 169.254.1.2:7770. That is
# frank-large and frank-small are scraped over host.containers.internal,
# not their own LAN addresses: this Prometheus is a bridge-networked
# container (Network=dankclaw_default), and on this netavark host a
# container can only reach the host's LAN IP at a PUBLISHED port — a
# host-process bind like llama-server's 0.0.0.0:7770 is refused at
# 192.168.0.143:7770 while it answers at 169.254.1.2:7770. That is
# why frank-small was red here for days (up{job="frank-small"} == 0,
# "dial tcp 192.168.0.143:7771") even though the endpoint was healthy and
# exporting: the target address was unreachable by construction, not the
# service down. Same host, same rule, so both local endpoints use the gateway.
# (2026-09-26) frank-medium is replaced by frank-primary (lucebox DS4.1),
# which exposes no metrics endpoint (/metrics -> 404), so there is no job
# for it here. The endpoint's liveness is covered by the homepage probe;
# the documented remedy is a blackbox exporter, which is not deployed.
# service down. Same host, same rule, so both local endpoints use the
# gateway.
#
# (2026-09-26) frank-primary is llama.cpp again (the unsloth b11030-mix
# prebuilt serving Qwen3.8-Flash-Next Q8_0) on Dankserver, a DIFFERENT
# machine: the netavark published-port quirk above is same-host only, so
# plain LAN routing reaches 192.168.0.235:7770 and the job returns.
- job_name: "frank-small"
metrics_path: "/metrics"
static_configs:
- targets: ["host.containers.internal:7771"]
labels:
service: frank-small
- job_name: "frank-large"
metrics_path: "/metrics"
static_configs:
- targets: ["host.containers.internal:7770"]
labels:
service: frank-large
- job_name: "frank-primary"
metrics_path: "/metrics"
static_configs:
- targets: ["192.168.0.235:7770"]
labels:
service: frank-primary
+8 -10
View File
@@ -1,11 +1,10 @@
# Alerting and recording rules for the LLM fleet.
#
# One metric namespace: llamacpp:* — frank-small and frank-large, both
# llama.cpp with --metrics (the family verified from /metrics on 2026-09-16
# and 2026-09-20). frank-primary (Dankserver, the Lucebox DS4.1 engine since
# the 2026-09-26 cutover) answers 404 on /metrics, so nothing here selects
# it; its liveness is covered by the homepage probe, and the documented
# remedy is a blackbox exporter (not deployed).
# One metric namespace: llamacpp:* — frank-small, frank-large and
# frank-primary, all llama.cpp with /metrics (the family verified from
# /metrics on 2026-09-16 and 2026-09-20). frank-primary is scraped across
# the LAN at 192.168.0.235:7770 (the netavark published-port quirk that
# forces the gateway for the Dankclaw endpoints is same-host only).
#
# Every expression is keyed to a series that EXISTS. Nothing matches on
# absence: when an endpoint's runtime leaves the fleet, its selectors are
@@ -111,7 +110,6 @@ groups:
description: >-
Prometheus cannot reach the LLM endpoint. frank-small and
frank-large are root units on Dankclaw: `systemctl is-active
frank-small` / `frank-large`. Both scraped endpoints export the
llamacpp:* family. frank-primary (Dankserver) is not scraped —
its runtime exports no metrics; check the homepage probe or
`systemctl --user is-active frank-primary` on Dankserver.
frank-small` / `frank-large`. frank-primary is a root unit on
Dankserver: `systemctl is-active frank-primary`. All three
scraped endpoints export the llamacpp:* family.
+15 -8
View File
@@ -21,21 +21,28 @@ load helpers
[[ "$output" != *"tests/homepage_pages.bats"* ]]
}
@test "changed-test wrapper maps the frank-primary dir to its focused tests" {
@test "changed-test wrapper maps the frank-primary trio to its focused tests" {
# The root-unit shape since the 2026-09-26 swap: the trio lives in
# scripts/ and must select the unit seam before the generic
# scripts/*.service fallbacks can swallow it.
run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths \
dankserver/frank-primary/quadlet/frank-primary.container
scripts/frank-primary.env \
scripts/frank-primary.service \
scripts/serve-frank-primary.sh
[ "$status" -eq 0 ]
[[ "$output" == *"tests/frank_primary_unit.bats"* ]]
[[ "$output" == *"tests/llm_units.bats"* ]]
[[ "$output" == *"tests/deployment_scoping.bats"* ]]
[[ "$output" == *"tests/docs_completeness.bats"* ]]
[[ "$output" != *"tests/homepage_pages.bats"* ]]
# The retired frank-medium tree must no longer select any LLM seam: the
# mappings were removed with the service, and a stale case would keep
# firing tests for an archived runtime. Both archive shapes — the radiance
# Quadlet era and the llama.cpp layersplit trio before it — map to nothing.
# The retired trees must no longer select any LLM seam: the mappings
# were removed with the services, and a stale case would keep firing
# tests for archived runtimes. All three archive shapes — the radiance
# Quadlet era, the llama.cpp layersplit trio, and the lucebox quadlet
# dir — map to nothing.
run bash "$REPO_ROOT/scripts/test-changed.sh" --list --paths \
dankserver/.AGENTTRASH/20260926-frank-medium-radiance/frank-medium.container \
scripts/.AGENTTRASH/20260920-llama-layersplit/serve-frank-medium.sh.llamacpp-layersplit
scripts/.AGENTTRASH/20260920-llama-layersplit/serve-frank-medium.sh.llamacpp-layersplit \
dankserver/.AGENTTRASH/20260926-llamacpp-cutover/frank-primary.container
[ "$status" -eq 0 ]
[[ "$output" != *"tests/frank_medium_unit.bats"* ]]
[[ "$output" != *"tests/frank_primary_unit.bats"* ]]
+14 -4
View File
@@ -63,13 +63,23 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
[ -z "$output" ]
}
@test "deployment planner maps the frank-primary container dir to frank-primary only" {
@test "deployment planner maps the frank-primary trio to frank-primary only" {
# The root-unit shape since the 2026-09-26 swap: unit, env and serve
# script live in scripts/ and select exactly frank-primary. The
# download script fetches host-side state and must select nothing, and
# the lucebox quadlet-era tree in dated trash plans nothing either.
run python3 "$PLANNER" --target dankserver --paths \
dankserver/frank-primary/quadlet/frank-primary.container \
dankserver/frank-primary/frank-primary-entrypoint.sh \
dankserver/frank-primary/frank-primary-healthcheck.py
scripts/frank-primary.env \
scripts/frank-primary.service \
scripts/serve-frank-primary.sh
[ "$status" -eq 0 ]
[ "$output" = "frank-primary" ]
run python3 "$PLANNER" --target dankserver --paths \
scripts/download-frank-primary.sh \
dankserver/.AGENTTRASH/20260926-llamacpp-cutover/frank-primary.container
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "the small endpoint is Dankclaw's again, and Dankserver's copy stays retired" {
+143 -69
View File
@@ -1,94 +1,168 @@
#!/usr/bin/env bats
# Seam: frank-primary — the Dankserver LLM endpoint on :7770.
#
# This file asserts the LUCEBOX shape (the 2026-09-26 cutover that replaced
# frank-medium): a rootless Quadlet running the lucebox DS4.1 engine on the
# two R9700s, with the model mmapped out of /home/dank/models and the
# entrypoint/healthcheck mounted straight out of this checkout.
# This file asserts the llama.cpp ROOT-UNIT shape (the 2026-09-26 swap that
# replaced the Lucebox DS4.1 Quadlet): the unsloth b11030-mix prebuilt on
# the two R9700s (gfx1201) serving unsloth/Qwen3.8-Flash-Next-GGUF at Q8_0
# with the shared Q8_0 MTP head, the BF16 projector, and the
# draft-mtp + ngram-mod chain. The seam pins what the flags alone do not
# say: the CPU-override placement law (experts and the 51B n-gram table
# are COMPUTED in host RAM, never streamed), mmap staying on, the
# shared-head borrow requirement (per_layer_token_embd on CPU), the
# explicit projector (never autoloaded by this build), HIP env hygiene,
# and the deployer's byte-count gates over a six-part payload.
load helpers
UNIT="$REPO_ROOT/dankserver/frank-primary/quadlet/frank-primary.container"
EP="$REPO_ROOT/dankserver/frank-primary/frank-primary-entrypoint.sh"
HC="$REPO_ROOT/dankserver/frank-primary/frank-primary-healthcheck.py"
UNIT="$REPO_ROOT/scripts/frank-primary.service"
SERVE="$REPO_ROOT/scripts/serve-frank-primary.sh"
ENV_FILE="$REPO_ROOT/scripts/frank-primary.env"
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
@test "the frank-primary quadlet file and its mounted scripts exist" {
@test "frank-primary unit, serve script and env exist in scripts/" {
[ -f "$UNIT" ]
[ -x "$EP" ]
[ -f "$HC" ]
# The healthcheck is python; a syntax error would leave the container
# permanently unhealthy with the failure hidden inside HealthCmd.
python3 -m py_compile "$HC"
[ -x "$SERVE" ]
[ -f "$ENV_FILE" ]
}
@test "frank-primary is boot-persistent and self-healing" {
# Rootless Podman does not auto-start bare containers and restart:
# unless-stopped does not survive shutdown's explicit stop. The unit must
# carry both the boot symlink and a restart policy (2026-08-26 incident:
# a compose-only Caddy never came back after a reboot).
grep -q '^WantedBy=default.target' "$UNIT"
grep -qE '^Restart=(always|on-failure)' "$UNIT"
@test "frank-primary is boot-persistent, self-healing and unprivileged" {
# Root unit parity with frank-large/frank-small: enable is the [Install]
# section, the restart policy survives a crash, and the mmap'd payload
# needs an unbounded memlock.
grep -q '^WantedBy=multi-user.target$' "$UNIT"
grep -q '^Restart=on-failure$' "$UNIT"
grep -q '^RestartSec=' "$UNIT"
grep -q '^User=dank$' "$UNIT"
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
}
@test "frank-primary binds the host network and needs no published port" {
# luce_server binds 0.0.0.0:7770 directly, so the container takes the
# host netns. A PublishPort would be dead config.
grep -q -- '--network=host' "$UNIT"
! grep -q '^PublishPort=' "$UNIT"
@test "frank-primary runs the repo's own serve script with the repo's env" {
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh$' "$UNIT"
grep -q 'FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env' "$UNIT"
}
@test "frank-primary reaches both R9700s and hides the Raphael iGPU" {
# /dev/kfd + /dev/dri via AddDevice; HIP_VISIBLE_DEVICES=0,1 keeps the
# host's Raphael iGPU (index 2) out of the enumeration so the engine's
# device picks cannot land on it.
grep -q '^AddDevice=/dev/kfd' "$UNIT"
grep -q '^AddDevice=/dev/dri' "$UNIT"
grep -q '^Environment=HIP_VISIBLE_DEVICES=0,1$' "$UNIT"
@test "frank-primary drafts with the shared MTP head chained behind ngram-mod" {
# The chain is draft-mtp (context-backed) + ngram-mod (draftless): the
# only legal way to layer two spec implementations in one process. The
# shared Q8_0 head borrows the target's embedding tensors — which is
# why per_layer_token_embd.weight must be CPU-resident (a GPU-resident
# borrow crashes the draft load with "invalid vector subscript").
grep -q -- '--spec-type draft-mtp,ngram-mod' "$SERVE"
grep -q '^SPEC_DRAFT_N_MAX=2$' "$ENV_FILE"
grep -q -- '--spec-draft-n-max "$SPEC_DRAFT_N_MAX"' "$SERVE"
grep -q 'DRAFTER_FILE="$MODEL_ROOT/MTP/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf"' "$SERVE"
grep -q -- '--spec-draft-device "$SPEC_DRAFT_DEVICE"' "$SERVE"
}
@test "frank-primary never names a bare HIP-reserved env var" {
# The HIP runtime reads bare TEMP for its JIT cache path (the 2026-08-13
# incident: TEMP=1.0 sent the JIT cache to path "1.0" and segfaulted every
# load). HOST and GPU_MAX_HEAP_SIZE are the same class. The unit's
# Environment= lines must not define them; the entrypoint exports
# GPU_MAX_HW_QUEUES (explicitly not GPU_MAX_HEAP_SIZE).
! grep -qE '^Environment=(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$UNIT"
! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$EP"
@test "the bandwidth-huge tensors are computed in host RAM, not streamed" {
# The x4-pair law: streaming experts over PCIe loses (3.7 tok/s) while
# computing them in RAM does not care (20+ tok/s on the same model
# shape). exps=CPU plus the n-gram embedding table on CPU; the PLE
# table must NEVER carry a GPU target (the 23 -> 13.4 t/s regression).
grep -q '^OT_OVERRIDES=exps=CPU,ngram_embeddings=CPU,per_layer_token_embd.weight=CPU$' "$ENV_FILE"
grep -q -- '--override-tensor "$ot"' "$SERVE"
# ...and mmap must stay on: --no-mmap forces the 51B-param table into
# anonymous memory and the cgroup kill is silent.
! grep -q -- '--no-mmap' "$SERVE"
! grep -q '^MMPROJ_ON_GPU' "$ENV_FILE"
}
@test "the unit mounts the entrypoint and healthcheck from the checkout" {
# deploy.sh git-pulls this checkout before dispatching, so a Volume
# reference is how an edit here reaches the container without an image
# rebuild. Absolute host paths, read-only.
grep -q 'Volume=/home/dank/serverAppConfigs/dankserver/frank-primary/frank-primary-entrypoint.sh:/usr/local/bin/frank-primary-entrypoint.sh:ro' "$UNIT"
grep -q 'Volume=/home/dank/serverAppConfigs/dankserver/frank-primary/frank-primary-healthcheck.py:/usr/local/bin/frank-primary-healthcheck.py:ro' "$UNIT"
@test "the projector is explicit, never autoloaded" {
# This build never autoloads mmproj from the model directory: without
# the flag the server comes up healthy and text-only, answering image
# requests with 500 (the frank-medium 2026-09-17 trap).
grep -q -- '--mmproj "$MMPROJ_FILE"' "$SERVE"
grep -q 'MMPROJ_FILE="$MODEL_ROOT/mmproj-BF16.gguf"' "$SERVE"
}
@test "first-start patience is configured on both sides of the health check" {
# Cold start page-ins the dense weights and builds the graphs; the
# container must not be killed as unhealthy while that happens, and the
# deployer must wait the same order of magnitude.
grep -q '^HealthStartPeriod=1800s' "$UNIT"
grep -q '^TimeoutStartSec=1800$' "$UNIT"
grep -q 'wait_container_healthy frank-primary 1800' \
"$REPO_ROOT/scripts/deploy-remote.sh"
@test "frank-primary is vision-serving but holds the single fleet port" {
grep -q '^PORT=7770$' "$ENV_FILE"
grep -q '^BIND_HOST=0.0.0.0$' "$ENV_FILE"
grep -q -- '--alias frank-primary' "$SERVE"
grep -q -- '--host "$BIND_HOST"' "$SERVE"
grep -q -- '--port "$PORT"' "$SERVE"
}
@test "the deployer refuses to start without image and complete payload" {
# A 377 GiB partial download must never be served: the handler gates on
# the image being present and the model file existing at its exact
# declared byte count (stat -c %s, never wc -c — wc read the whole stream
# for minutes on 2026-09-26).
grep -q 'deploy_llm_primary() {' "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'podman image exists localhost/lucebox-rocm:gfx1201' \
"$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'want_size=405349859328' "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'stat -c %s' "$REPO_ROOT/scripts/deploy-remote.sh"
@test "one stream, the model's full native window" {
local ctx parallel
ctx="$(sed -nE 's/^CONTEXT_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
parallel="$(sed -nE 's/^PARALLEL_SLOTS=([0-9]+)$/\1/p' "$ENV_FILE")"
[ -n "$ctx" ] && [ -n "$parallel" ]
[ "$ctx" -eq 262144 ]
[ "$parallel" -eq 1 ]
grep -q -- '--ctx-size "$CONTEXT_SIZE"' "$SERVE"
grep -q -- '--parallel "$PARALLEL_SLOTS"' "$SERVE"
}
@test "the deployer gates on the port actually being free" {
# 7770 is a host-network bind: the previous holder must release it before
# the new container can be created, or pasta's bind fails at create time
# and the failure hides behind Restart=.
grep -q 'wait_port_free 7770' "$REPO_ROOT/scripts/deploy-remote.sh"
@test "reasoning is extracted to reasoning_content and tools are legal" {
# The harness contract: llama.cpp endpoints emit reasoning_content
# (the deepseek format is the one that fills it), and tools require
# --jinja (the ik_llama 500 lesson). The Qwen3.8 template gates
# reasoning_effort to {xhigh, medium, low} — folded in models.yml.
grep -q -- '--jinja' "$SERVE"
grep -q -- '--reasoning auto' "$SERVE"
grep -q -- '--reasoning-format deepseek' "$SERVE"
grep -q -- '--reasoning-budget -1' "$SERVE"
}
@test "the greedy server default is explicit" {
# Deterministic e2e probes; the harness overrides sampling per request.
grep -q -- '--temp 0' "$SERVE"
grep -q -- '--top-p 1.0' "$SERVE"
grep -q -- '--top-k 0' "$SERVE"
grep -q -- '--min-p 0.0' "$SERVE"
}
@test "both R9700s are visible and the Raphael iGPU is not" {
# The host has a third (Raphael) GPU the server must never place
# tensors on; the bundled ROCm runtime is the only one linked.
grep -q 'export HIP_VISIBLE_DEVICES=0,1$' "$SERVE"
grep -q 'export LD_LIBRARY_PATH="$RUNTIME_DIR' "$SERVE"
}
@test "no bare HIP-reserved env var is defined" {
# HIP reads bare TEMP for its JIT cache path (2026-08-13: TEMP=1.0
# segfaulted every load), and HOST/GPU_MAX_HEAP_SIZE are the same
# class. The bind address is BIND_HOST, not HOST.
! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE"
grep -q '^BIND_HOST=' "$ENV_FILE"
! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE"
}
@test "the serve script refuses to start on a half-finished download" {
# Six split parts plus the head and the projector: a missing tail part
# fails at LOAD time with a confusing partial-mmap error, so every
# artifact is checked before the server execs.
grep -q 'missing asset' "$SERVE"
grep -q 'download-frank-primary.sh' "$SERVE"
}
@test "the prompt-cache cap is set below the RAM the host can spare" {
grep -q '^CACHE_RAM=16384$' "$ENV_FILE"
grep -q -- '--cache-ram "$CACHE_RAM"' "$SERVE"
}
@test "the deployer retires the lucebox quadlet, gates every part and waits out the port" {
# The cutover order: the user-scope quadlet is stopped and its
# generated .container moved to dated home trash BEFORE the root unit
# binds 7770 (nothing starts until the port is released — the 2026-09-26
# and 2026-09-16 cutover failures were both here).
grep -q 'retire_lucebox_primary_quadlet' "$REMOTE_DEPLOY"
grep -q 'systemctl --user stop frank-primary.service' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-primary' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-primary.service' "$REMOTE_DEPLOY"
grep -q 'systemctl restart frank-primary.service' "$REMOTE_DEPLOY"
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
grep -q '127.0.0.1:7770/health' "$REMOTE_DEPLOY"
}
@test "the deployer gates the six-part payload at declared byte counts" {
# stat -c %s, never wc -c — wc read the whole 377 GiB lucebox file for
# minutes on 2026-09-26 before it could reach the cutover. A partial
# download must never be served.
grep -q 'LLM_PRIMARY_PART_SIZES' "$REMOTE_DEPLOY"
grep -q 'stat -c %s' "$REMOTE_DEPLOY"
grep -q '2786568256' "$REMOTE_DEPLOY"
grep -q '907542944' "$REMOTE_DEPLOY"
grep -q 'wait_for_health frank-primary http://127.0.0.1:7770/health 1800' "$REMOTE_DEPLOY"
}
+8 -7
View File
@@ -61,17 +61,18 @@ PY
@test "the LLM status cards track the fleet, not retired service names" {
# This drifted twice: the card probing Dankserver's :7770 was still named
# frank-small long after frank-medium took that endpoint, and Dankclaw's
# second endpoint now exists again as a real service on its own port. The
# probe target is what matters; the name has to follow it.
# frank-small long after frank-medium took that endpoint, and the endpoint
# itself has since been frank-medium (radiance), frank-primary (lucebox)
# and frank-primary (llama.cpp). The probe target is what matters; the
# name has to follow it.
python3 - "$REPO_ROOT/shared/homepage/services.json" <<'PY'
import json, sys
services = json.load(open(sys.argv[1]))
by_probe = {s["probe"]: s["name"] for s in services}
medium = by_probe.get("192.168.0.235:7770")
assert medium == "frank-medium", f"Dankserver :7770 card is named {medium!r}"
# Dankclaw serves two endpoints on two ports: frank-large (halogen, 7770) and
# frank-small (llama.cpp on the RTX 3090, 7771).
primary = by_probe.get("192.168.0.235:7770")
assert primary == "frank-primary", f"Dankserver :7770 card is named {primary!r}"
# Dankclaw serves two endpoints on two ports: frank-large (unsloth llama.cpp
# on the iGPU, 7770) and frank-small (llama.cpp on the RTX 3090, 7771).
assert by_probe.get("127.0.0.1:7770") == "frank-large", by_probe.get("127.0.0.1:7770")
assert by_probe.get("127.0.0.1:7771") == "frank-small", by_probe.get("127.0.0.1:7771")
PY
+15 -10
View File
@@ -315,24 +315,29 @@ PAIRS
@test "prometheus scrapes every LLM endpoint that exports metrics" {
# The LLM endpoints are the services whose behaviour this fleet argues about
# most (KV pressure, prefix-cache reuse, queue depth). Only the two Dankclaw
# llama.cpp endpoints export metrics today: frank-primary (Dankserver, the
# Lucebox DS4.1 engine since the 2026-09-26 cutover) answers 404 on
# /metrics, so a job for it would be a permanently-down target — no job,
# and its LAN address must not sneak back in with one.
# The two scraped endpoints are HOST processes reached through
# most (KV pressure, prefix-cache reuse, queue depth). All three export
# the llamacpp:* family since 2026-09-26, when frank-primary (Dankserver)
# went back to llama.cpp — the unsloth b11030-mix prebuilt serving
# Qwen3.8-Flash-Next Q8_0 — after two days of the Lucebox DS4.1 engine,
# which exported nothing (404) and had no job.
# The two Dankclaw endpoints are HOST processes reached through
# host.containers.internal: this Prometheus is a bridge-networked
# container, and on this netavark host a container can only reach the
# host's LAN IP at a PUBLISHED port — a host-process bind like
# llama-server's 0.0.0.0:7770 is refused at 192.168.0.143:7770 while it
# answers at 169.254.1.2:7770 (why frank-small stayed red by construction
# for days until the 2026-09-20 /targets audit).
# for days until the 2026-09-20 /targets audit). frank-primary is a
# DIFFERENT machine, so that quirk does not apply: plain LAN routing
# reaches 192.168.0.235:7770, and the job must say so (a gateway target
# would point at Dankclaw's own :7770 — frank-large — and double-scrape
# it under the wrong label).
local cfg="$REPO_ROOT/shared/prometheus/prometheus.yml"
! grep -q '"192.168.0.235:7770"' "$cfg" || { echo "frank-primary is scraped but exports no metrics"; return 1; }
grep -q '"host.containers.internal:7771"' "$cfg" || { echo "frank-small not scraped"; return 1; }
grep -q '"host.containers.internal:7770"' "$cfg" || { echo "frank-large not scraped"; return 1; }
for job in frank-small frank-large; do
grep -q '"192.168.0.235:7770"' "$cfg" || { echo "frank-primary not scraped"; return 1; }
grep -q '"host.containers.internal:7770,192.168.0.235:7770"' "$cfg" \
&& { echo "frank-primary folded into a Dankclaw gateway target"; return 1; }
for job in frank-small frank-large frank-primary; do
grep -q "job_name: \"$job\"" "$cfg" || { echo "no scrape job for $job"; return 1; }
done
# None of them is reached through a guarded vhost: Prometheus cannot solve