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:
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+134
@@ -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[@]}"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"* ]]
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user