Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71c6b5e758 |
@@ -1,4 +1,4 @@
|
||||
# Tunables for frank-dragoon (kingjones777/Qwen3.8-Flash-Next-ROCmFP4-
|
||||
# Tunables for frank-large (kingjones777/Qwen3.8-Flash-Next-ROCmFP4-
|
||||
# STRIX-GGUF on the kingjones30/ROCmFPX llama.cpp runtime, Dankclaw,
|
||||
# Strix Halo gfx1151, unified memory).
|
||||
#
|
||||
@@ -56,7 +56,7 @@ CHAT_TEMPLATE_FILE=/home/dank/serverAppConfigs/scripts/chat-template.jinja
|
||||
CHAT_TEMPLATE_KWARGS='{"enable_thinking":true,"preserve_thinking":true,"preserve_reasoning":true}'
|
||||
|
||||
# Alias + network.
|
||||
ALIAS=frank-dragoon
|
||||
ALIAS=frank-large
|
||||
BIND_ADDR=0.0.0.0
|
||||
LISTEN_PORT=7770
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=ROCmFPX (kingjones30 fork) llama.cpp Qwen3.8-Flash-Next ROCmFP4 STRIX 177B MoE (dankclaw, frank-dragoon)
|
||||
Description=ROCmFPX (kingjones30 fork) llama.cpp Qwen3.8-Flash-Next ROCmFP4 STRIX 177B MoE (dankclaw, frank-large)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -7,8 +7,8 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
User=dank
|
||||
Group=dank
|
||||
Environment="FRANK_DRAGOON_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-dragoon.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-dragoon.sh
|
||||
Environment="FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# is reachable on this port regardless of the boot tag. The NPU is a
|
||||
# single-lane device: generations serialize FIFO, and the first request after a
|
||||
# model swap pays the ~6-11 s load cost. The NPU draws from the unified RAM
|
||||
# pool shared with frank-dragoon (iGPU).
|
||||
# pool shared with frank-large (iGPU).
|
||||
FLM_MODEL=nuextract3:4b
|
||||
FLM_HOST=0.0.0.0
|
||||
FLM_PORT=7771
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Serve kingjones777/Qwen3.8-Flash-Next-ROCmFP4-STRIX-GGUF as
|
||||
# frank-dragoon via the kingjones30/ROCmFPX llama.cpp runtime on
|
||||
# frank-large via the kingjones30/ROCmFPX llama.cpp runtime on
|
||||
# Dankclaw (Strix Halo gfx1151, unified memory).
|
||||
#
|
||||
# Runtime: kingjones30/ROCmFPX (fork of charlie12345/ROCmFPX with the
|
||||
@@ -29,7 +29,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-large.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
@@ -44,7 +44,7 @@ fi
|
||||
: "${MMPROJ_PATH:=/home/dank/models/agentionai-Qwen3.8-Flash-Next-ROCmFP4-FAST/mmproj/mmproj-Qwen3.8-Flash-Next-f16.gguf}"
|
||||
: "${CHAT_TEMPLATE_FILE:=$SCRIPT_DIR/chat-template.jinja}"
|
||||
: "${CHAT_TEMPLATE_KWARGS:={}}"
|
||||
: "${ALIAS:=frank-dragoon}"
|
||||
: "${ALIAS:=frank-large}"
|
||||
: "${BIND_ADDR:=0.0.0.0}"
|
||||
: "${LISTEN_PORT:=7770}"
|
||||
: "${CTX_SIZE:=401408}"
|
||||
@@ -4,7 +4,7 @@
|
||||
# Runs `flm serve <tag>` on the XDNA2 NPU (50 TOPS, Dankclaw). The NPU is a
|
||||
# single-lane device: generations serialize in FIFO order, so this is a
|
||||
# background/triage endpoint, not an interactive multi-user chat. The NPU
|
||||
# draws from the unified RAM pool shared with frank-dragoon (the iGPU
|
||||
# draws from the unified RAM pool shared with frank-large (the iGPU
|
||||
# llama.cpp service); see serverAppConfigs-docs/LLM/frank-npu.md
|
||||
# for the memory budget and the model-selection tradeoff.
|
||||
#
|
||||
|
||||
@@ -9,11 +9,11 @@ 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-archon` 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-dragoon` (flash-server
|
||||
container on the gfx1151 iGPU) and `frank-zealot` (llama.cpp CUDA on the
|
||||
Alloy, Pi-hole) plus TWO LLM services: `frank-large` (flash-server
|
||||
container on the gfx1151 iGPU) and `frank-secondary` (llama.cpp CUDA on the
|
||||
RTX 3090). Caddy on
|
||||
Dankclaw reverse-proxies both hosts.
|
||||
- **FrankBook-Pro** (192.168.0.112) — macOS mini running the *arr stack
|
||||
@@ -25,9 +25,9 @@ merge absorbed the former `vllmScripts` repo, history preserved).
|
||||
|
||||
| Target | Service | Runtime | Model | Port |
|
||||
|--------|---------|---------|-------|------|
|
||||
| Dankserver | `frank-archon` | **radiance** (StillDeadcode, **pin 1.0.13** digest-pinned `stilldeadcode/radiance@sha256:4c3f484564…`, plain `podman run`, root systemd unit) | ukisai/Swift1.5-Qwen3.8-Flash-Next BF16 (335.3 GiB, 397/397 sha256-verified) converted by `scripts/convert-swift15-w4dn8.sh` to `qwen4exp-swift15-w4dn8-i8-hc8m.rad` (**116,416 MiB**): **all 49,152 experts GPTQ w4** (10M-token calibration on engine 1.2.0) + **24 whole bf16-protected experts/layer** (energy-share law), int8 trunk/lm_head, fp8 HC, PLE n-gram fp8 fixed-scale, on **BOTH R9700s TP=2** (`--tp-wire wht6`), experts in a **40 GiB/rank pinned host pool** + VRAM cache (`--placement expert_tiered`, explicit `--vram-weights-mib 26071 --vram-kv-mib 3543`), **47.7 GiB n-gram copy pinned to host RAM** (`--ngram-placement ram`; root unit + `LimitMEMLOCK=infinity`), **fp8 KV** (inherited; pool 3.46 GiB), **MTP spec decode depth 2**, `--max-num-seqs 3`, `--max-model-len 200000`, MNBT 2048, Froggeric v22.5 template. KLD-gated (mean KL 0.0131, top-1 97.92 % vs BF16); decode ~190 t/s live (gate bar 30.8). Cutover 2026-10-08 from fp8-native (payload on disk = rollback). | 7770 |
|
||||
| Dankclaw | `frank-dragoon` | **halogen-flash-server 0.16.2** (`ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`, digest-pinned, plain `podman run` via `scripts/serve-frank-dragoon.sh`, root systemd unit) | Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated **`qwen38-flash-next-v2-swift15-abliterated.hgn`** (66,687,678,464 B = compacted r2 of the card's 89,775,868,032 B original, ~62 GiB resident; the v2 `.hgn` format loads in NO other engine), **+ REQUIRED `qwen38-flash-next-ngram.hgn`** (51,200,246,144 B, peonist @5cc17cea — read beside the checkpoint) **+ vision tower** (897,916,416 B, @e053f488 — **vision ON**, `image_url` parts only, ≥256 px probes), **MTP spec decode ALWAYS-ON (`HALOGEN_SPEC_ADAPT=0` — the adaptive default disables the drafter per request and HALVES decode)**, MTP head INSIDE the checkpoint (`HALOGEN_MTP_HEAD` unset; no sidecar fetched), **2 slots × 262,144 ctx** (KV pool 524,288 positions ≈ 14.0 GiB at ~28 KiB/position; the 27B slots>1-disables-drafting law does NOT carry over — batching and speculation coexist, the card's quickstart ships 2 with the head), harness window 217,088 = ceiling − 45k estimate headroom, **server-side sampler 1.0/0.95/20** (card recipe; models.yml extraBody matches), coupled cap/timeout 65536/7200, prompt cache ON by default (warm 100k turn ≈ 2 s vs ≈ 88 s cold)|7770|
|
||||
| Dankclaw | `frank-zealot` | llama.cpp master (0.5.0-dev, commit `5fc4f3c`), CUDA sm_86 build (root systemd unit, relinked) | HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive **Q6_K** (7,359,259,008 B; dense `qwen35` hybrid: 32 layers, 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, vocab 248,320, native 262,144, **no MTP tensors in the file**) + the publisher's own **BF16 mmproj** (921,704,448 B) for **vision**, on the RTX 3090, **TWO 200,704-token slots** (`--ctx-size 401408 --parallel 2`), **exact f16 K + f16 V** under flash attention (32 KiB/token over the 8 full layers = 12.25 GiB pool), batch 4096 / **ubatch 2048** (measured CUDA optimum), `--log-verbosity 4`, **MTP OFF** (forced: no `nextn` tensors exist). Priced from the MiMo Q6_K boots (same geometry, same card, same shape): 21,276/2,849 at ubatch 1024, 21,846/2,279 at 2048. Cutover 2026-10-07 (late) from Gemma4-26B-A4B Q4_K_M 1×200,704. All three endpoints route harness **compaction** here. | 7771 |
|
||||
| Dankserver | `frank-primary` | **radiance** (StillDeadcode, **pin 1.0.13** digest-pinned `stilldeadcode/radiance@sha256:4c3f484564…`, plain `podman run`, root systemd unit) | ukisai/Swift1.5-Qwen3.8-Flash-Next BF16 (335.3 GiB, 397/397 sha256-verified) converted by `scripts/convert-swift15-w4dn8.sh` to `qwen4exp-swift15-w4dn8-i8-hc8m.rad` (**116,416 MiB**): **all 49,152 experts GPTQ w4** (10M-token calibration on engine 1.2.0) + **24 whole bf16-protected experts/layer** (energy-share law), int8 trunk/lm_head, fp8 HC, PLE n-gram fp8 fixed-scale, on **BOTH R9700s TP=2** (`--tp-wire wht6`), experts in a **40 GiB/rank pinned host pool** + VRAM cache (`--placement expert_tiered`, explicit `--vram-weights-mib 26071 --vram-kv-mib 3543`), **47.7 GiB n-gram copy pinned to host RAM** (`--ngram-placement ram`; root unit + `LimitMEMLOCK=infinity`), **fp8 KV** (inherited; pool 3.46 GiB), **MTP spec decode depth 2**, `--max-num-seqs 3`, `--max-model-len 200000`, MNBT 2048, Froggeric v22.5 template. KLD-gated (mean KL 0.0131, top-1 97.92 % vs BF16); decode ~190 t/s live (gate bar 30.8). Cutover 2026-10-08 from fp8-native (payload on disk = rollback). | 7770 |
|
||||
| Dankclaw | `frank-large` | **halogen-flash-server 0.16.2** (`ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`, digest-pinned, plain `podman run` via `scripts/serve-frank-large.sh`, root systemd unit) | Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated **`qwen38-flash-next-v2-swift15-abliterated.hgn`** (66,687,678,464 B = compacted r2 of the card's 89,775,868,032 B original, ~62 GiB resident; the v2 `.hgn` format loads in NO other engine), **+ REQUIRED `qwen38-flash-next-ngram.hgn`** (51,200,246,144 B, peonist @5cc17cea — read beside the checkpoint) **+ vision tower** (897,916,416 B, @e053f488 — **vision ON**, `image_url` parts only, ≥256 px probes), **MTP spec decode ALWAYS-ON (`HALOGEN_SPEC_ADAPT=0` — the adaptive default disables the drafter per request and HALVES decode)**, MTP head INSIDE the checkpoint (`HALOGEN_MTP_HEAD` unset; no sidecar fetched), **2 slots × 262,144 ctx** (KV pool 524,288 positions ≈ 14.0 GiB at ~28 KiB/position; the 27B slots>1-disables-drafting law does NOT carry over — batching and speculation coexist, the card's quickstart ships 2 with the head), harness window 217,088 = ceiling − 45k estimate headroom, **server-side sampler 1.0/0.95/20** (card recipe; models.yml extraBody matches), coupled cap/timeout 65536/7200, prompt cache ON by default (warm 100k turn ≈ 2 s vs ≈ 88 s cold)|7770|
|
||||
| Dankclaw | `frank-secondary` | llama.cpp master (0.5.0-dev, commit `5fc4f3c`), CUDA sm_86 build (root systemd unit, relinked) | HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive **Q6_K** (7,359,259,008 B; dense `qwen35` hybrid: 32 layers, 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, vocab 248,320, native 262,144, **no MTP tensors in the file**) + the publisher's own **BF16 mmproj** (921,704,448 B) for **vision**, on the RTX 3090, **TWO 200,704-token slots** (`--ctx-size 401408 --parallel 2`), **exact f16 K + f16 V** under flash attention (32 KiB/token over the 8 full layers = 12.25 GiB pool), batch 4096 / **ubatch 2048** (measured CUDA optimum), `--log-verbosity 4`, **MTP OFF** (forced: no `nextn` tensors exist). Priced from the MiMo Q6_K boots (same geometry, same card, same shape): 21,276/2,849 at ubatch 1024, 21,846/2,279 at 2048. Cutover 2026-10-07 (late) from Gemma4-26B-A4B Q4_K_M 1×200,704. All three endpoints route harness **compaction** here. | 7771 |
|
||||
|
||||
Retired endpoints (`frank-npu`, and every prior runtime) are in
|
||||
the [runbook index](#runbook-index). The full swap history lives in
|
||||
@@ -35,16 +35,11 @@ the [runbook index](#runbook-index). The full swap history lives in
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `dankserver/` — quadlet units + Caddyfile for the media stack (the
|
||||
compose.yaml inventory retired 2026-10-13; its mounts had diverged from
|
||||
the quadlets)
|
||||
- `dankclaw/` — quadlet units + Caddyfile for the AI stack (compose retired 2026-09-16)
|
||||
- `shared/` — the service inventory (`services.yml`), homepage, Prometheus,
|
||||
Loki, Alloy, SearXNG configs
|
||||
- `scripts/` — `deploy.sh` / `deploy-plan.py` / `deploy-remote.sh` and
|
||||
`service-map.py` (the inventory's only reader), LLM serve scripts + the
|
||||
shared artifact gates (`scripts/lib/llm-gates.sh`), systemd units, env
|
||||
tunables, bench harnesses
|
||||
- `dankserver/` — compose.yaml + Caddyfile + .env.example for the media stack
|
||||
- `dankclaw/` — compose.yaml + Caddyfile + .env.example for the AI stack
|
||||
- `shared/` — homepage, Prometheus, Loki, Alloy, SearXNG configs
|
||||
- `scripts/` — `deploy.sh` / `deploy-plan.py` / `deploy-remote.sh`, LLM serve
|
||||
scripts, systemd units, env tunables, bench harnesses
|
||||
- `old/` — retired LLM scripts (reference only)
|
||||
- `serverAppConfigs-docs/` — knowledge base (Obsidian vault, OKF format).
|
||||
The managed skill library is distilled there one page per skill-cluster:
|
||||
@@ -71,19 +66,14 @@ 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-archon
|
||||
./scripts/deploy.sh dankserver --service frank-primary
|
||||
./scripts/deploy.sh dankclaw --dry-run
|
||||
```
|
||||
|
||||
The inventory (`shared/services.yml`, read only through
|
||||
`scripts/service-map.py`) maps changed paths to services; the remote runner
|
||||
restarts only the selected service and records its revision outside the
|
||||
checkout. Both `--service` allow-lists, the planner, and the changed-test
|
||||
seam mapper ARE the inventory — adding, moving, or retiring a service is a
|
||||
data edit, not a script edit
|
||||
(`serverAppConfigs-docs/Architecture/Service Inventory.md`).
|
||||
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 — every endpoint (`frank-dragoon`/`frank-zealot`/`frank-archon`)
|
||||
their own files — every endpoint (`frank-large`/`frank-secondary`/`frank-primary`)
|
||||
by its unit/env/serve trio in `scripts/` — so
|
||||
unrelated
|
||||
container changes cannot interrupt inference. Set `FRANK_SUDO_PW` for headless
|
||||
@@ -95,7 +85,7 @@ restarts each service whose marker is behind HEAD *and* whose paths changed
|
||||
since that marker — so a backlog accumulated by service-scoped deploys fires
|
||||
all at once, and one failing health gate aborts the run with every later
|
||||
service left pending. Name the service unless a drain is what you want
|
||||
(`./scripts/deploy.sh dankserver --service frank-archon`).
|
||||
(`./scripts/deploy.sh dankserver --service frank-primary`).
|
||||
|
||||
### Deployment completeness (MANDATORY)
|
||||
|
||||
@@ -174,17 +164,12 @@ Tests are mandatory before commit (`bats` required on the dev machine):
|
||||
./tests/run.sh
|
||||
```
|
||||
|
||||
Seams: env consistency (no leaked secrets), Caddyfile
|
||||
Seams: compose validity, env consistency (no leaked secrets), Caddyfile
|
||||
validity, quadlet units, docs completeness **including vault link integrity**,
|
||||
script syntax (shell + python), LLM unit integrity, LLM secret hygiene,
|
||||
LLM artifact gates (`llm_gates`/`llm_deploy_gates` — the shared
|
||||
`scripts/lib/llm-gates.sh` every endpoint's serve and deploy gates run on),
|
||||
deployment flow, service-scoped planning, the **service inventory**
|
||||
(`service_inventory.bats`: inventory soundness, the pinned path->service
|
||||
plan fixture, render drift for the Pi-hole hosts file), homepage
|
||||
config/pages/deploy/checker, and `e2e_scaffold` — which fails if a
|
||||
Playwright spec file's number prefix is in no project, because such a spec
|
||||
silently never runs.
|
||||
deployment flow, service-scoped planning, homepage config/pages/deploy/checker,
|
||||
and `e2e_scaffold` — which fails if a Playwright spec file's number prefix is in
|
||||
no project, because such a spec silently never runs.
|
||||
See `serverAppConfigs-docs/Architecture/Testing Policy.md`.
|
||||
|
||||
## Secret Rules
|
||||
@@ -244,17 +229,17 @@ archive.
|
||||
**STOP the production service BEFORE benchmarking** — these servers serve
|
||||
real users, and a bench beside a live service starves the GPU and corrupts
|
||||
results. Use the runtime's own harness: `llama-bench` for the llama.cpp
|
||||
endpoints; a live llama.cpp server also reports honest per-request numbers in the `/completion` `timings` object (prefill/decode rates, `draft_n`, `draft_n_accepted`), which is how frank-dragoon and frank-zealot were measured. Rolling your own bench
|
||||
endpoints; a live llama.cpp server also reports honest per-request numbers in the `/completion` `timings` object (prefill/decode rates, `draft_n`, `draft_n_accepted`), which is how frank-large and frank-secondary were measured. Rolling your own bench
|
||||
requires asking first.
|
||||
Scratch ports are not used on LLM hosts — a
|
||||
test process cannot share the single GPU anyway; validate against the service
|
||||
itself.
|
||||
|
||||
(A gufo-era law; frank-dragoon ran ROCmFPX llama.cpp 2026-10-02 → 10-05, the
|
||||
(A gufo-era law; frank-large ran ROCmFPX llama.cpp 2026-10-02 → 10-05, the
|
||||
halogen 27B container 2026-10-05 → 10-06, and is flash-server since, but the
|
||||
rollback tags keep it live.) **`gufo bench` cannot rank speculative-decode
|
||||
settings, and will invert the ranking.**
|
||||
Measured 2026-09-28 on frank-dragoon at depth 32768: its corpus
|
||||
Measured 2026-09-28 on frank-large at depth 32768: its corpus
|
||||
reports `acceptance=0.933 ± 0.000` with an **identical trace sha256** across
|
||||
draft widths, i.e. the same highly-predictable text every run, and 93 % of
|
||||
every proposal accepted. It reported 29–37 t/s decode where real traffic
|
||||
@@ -279,12 +264,12 @@ one: `ip neigh show <ip>` from a peer — FAILED means off/NIC dead, while a
|
||||
merely wedged kernel usually still answers ARP.
|
||||
|
||||
1. Stop and mask (self-healing would restart it):
|
||||
- Dankclaw `frank-dragoon` (root unit, halogen podman on the iGPU):
|
||||
`sudo systemctl stop frank-dragoon && sudo systemctl mask frank-dragoon`
|
||||
- Dankclaw `frank-zealot` (root unit, llama.cpp on the 3090):
|
||||
`sudo systemctl stop frank-zealot && sudo systemctl mask frank-zealot`
|
||||
- Dankserver `frank-archon` (root unit, radiance on both R9700s, TP=2):
|
||||
`sudo systemctl stop frank-archon && sudo systemctl mask frank-archon`
|
||||
- Dankclaw `frank-large` (root unit, halogen podman on the iGPU):
|
||||
`sudo systemctl stop frank-large && sudo systemctl mask frank-large`
|
||||
- Dankclaw `frank-secondary` (root unit, llama.cpp on the 3090):
|
||||
`sudo systemctl stop frank-secondary && sudo systemctl mask frank-secondary`
|
||||
- Dankserver `frank-primary` (root unit, radiance on both R9700s, TP=2):
|
||||
`sudo systemctl stop frank-primary && sudo systemctl mask frank-primary`
|
||||
2. Bench.
|
||||
NOTE: `systemctl mask` FAILS on a relinked unit (the unit file in
|
||||
`/etc/systemd/system/` is itself a symlink into the checkout — mask
|
||||
@@ -302,15 +287,14 @@ 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. **Official model ids are
|
||||
the StarCraft unit names (2026-10-08), on the wire since 2026-10-09, and
|
||||
since 2026-10-10 they are also the SERVICE names:** `frank-archon` →
|
||||
`192.168.0.235:7770` (`FRANK_ARCHON_SERVED_NAME`), `frank-dragoon` →
|
||||
`192.168.0.143:7770` (`HALOGEN_MODEL_ID`), `frank-zealot` →
|
||||
`192.168.0.143:7771` (`ALIAS`). The 2026-10-10 cutover renamed the units,
|
||||
scripts, env files, containers, deploy services and runbooks to match —
|
||||
the three former service names are fully retired and exist nowhere in
|
||||
the repo; historical text in runbooks uses the current names. Verify a
|
||||
rename on `/v1/models`, never
|
||||
the StarCraft unit names (2026-10-08) and since 2026-10-09 they are also the
|
||||
ids on the wire:** `frank-archon` → `192.168.0.235:7770`
|
||||
(`FRANK_PRIMARY_SERVED_NAME`), `frank-dragoon` → `192.168.0.143:7770`
|
||||
(`HALOGEN_MODEL_ID`), `frank-zealot` → `192.168.0.143:7771` (`ALIAS`). The
|
||||
SERVICES, units, scripts, env files and runbooks keep the old names
|
||||
(`frank-primary`, `frank-large`, `frank-secondary`); only the served alias,
|
||||
the `models.yml`/`config.yml` ids, the homepage cards, the OpenWebUI grants
|
||||
and the Hermes routing were renamed. Verify a rename on `/v1/models`, never
|
||||
on a chat response: radiance echoes the **requested** model name, so a 200
|
||||
proves nothing about the served id (llama.cpp and halogen ignore it).
|
||||
|
||||
@@ -325,16 +309,16 @@ 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-10-06 **all three endpoints accept images** (`input: [text,
|
||||
image]`) — `frank-dragoon` returned to vision with the flash-server v2 swap
|
||||
image]`) — `frank-large` returned to vision with the flash-server v2 swap
|
||||
(the v2 vision tower loads beside the checkpoint; the text-only halogen
|
||||
27B window closed 2026-10-06) — while
|
||||
`frank-archon` takes them natively (radiance runs the checkpoint's
|
||||
`frank-primary` takes them natively (radiance runs the checkpoint's
|
||||
27-layer Qwen-VL tower — patching, pad-token expansion and 3-part rotary
|
||||
are the engine core; `--mm-max-patches` available),
|
||||
and `frank-zealot` through
|
||||
and `frank-secondary` through
|
||||
the publisher's own BF16 mmproj passed as `--mmproj` — the model is natively
|
||||
multimodal but the Q6_K GGUF carries no vision tensors (same arrangement the
|
||||
Gemma4 era used). `frank-zealot`'s `contextWindow` is
|
||||
Gemma4 era used). `frank-secondary`'s `contextWindow` is
|
||||
**200704** — the PER-SLOT window (`--ctx-size 401408 --parallel 2` since the
|
||||
2026-10-07 Qwen3.5-9B swap), not the pool and not the model's native 262144:
|
||||
the harness must compact before llama.cpp answers a longer prompt with a 400.
|
||||
@@ -349,20 +333,20 @@ endpoint on 2026-09-11, returned 2026-09-17 with the Gemma4 projector's
|
||||
audio encoder, and left again with the Gemma4 retirement on 2026-10-01
|
||||
(neither the MiMo nor the Qwen3.5 tower carries audio).
|
||||
|
||||
### frank-dragoon — halogen-flash-server 0.16.2, Qwen3.8-Flash-Next v2 Swift1.5 ABLITERATED (Dankclaw, ACTIVE)
|
||||
### frank-large — halogen-flash-server 0.16.2, Qwen3.8-Flash-Next v2 Swift1.5 ABLITERATED (Dankclaw, ACTIVE)
|
||||
|
||||
The **flash-server engine** (`ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`,
|
||||
digest-pinned, published tag 0.16.2, 3.6 GB image) as a podman container
|
||||
driven by `scripts/serve-frank-dragoon.sh` under a **root systemd unit**
|
||||
(`scripts/frank-dragoon.service`, `User=dank`, `Restart=always`,
|
||||
`ExecStop=podman stop -t 60 frank-dragoon-halogen`, the script runs `podman
|
||||
driven by `scripts/serve-frank-large.sh` under a **root systemd unit**
|
||||
(`scripts/frank-large.service`, `User=dank`, `Restart=always`,
|
||||
`ExecStop=podman stop -t 60 frank-large-halogen`, the script runs `podman
|
||||
run` FOREGROUND — the container's exit code is the service's exit so
|
||||
`Restart=always` stays real, and the engine ledger lands on disk) on port
|
||||
7770 from the Strix Halo iGPU. Engine stdout goes to
|
||||
`/home/dank/logs/frank_dragoon.log` — **the boot ledger (weights resident, KV
|
||||
`/home/dank/logs/frank_large.log` — **the boot ledger (weights resident, KV
|
||||
pool, MTP head, prompt cache, any clamp) lives there, not in the journal**.
|
||||
Cutover 2026-10-06 from the halogen 27B-dense era
|
||||
([[frank-dragoon-halogen27b-dflash2]], retired the same window).
|
||||
([[frank-large-halogen27b-dflash2]], retired the same window).
|
||||
|
||||
Serving `Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated`:
|
||||
`qwen38-flash-next-v2-swift15-abliterated.hgn` (66,687,678,464 B ≈ 62.1
|
||||
@@ -384,10 +368,10 @@ sidecar is deliberately not fetched).
|
||||
**Product facts vs the 27B era, both regressions FIXED:** the base is
|
||||
**abliterated** (the 27B was a censored-base finetune; refusals were an
|
||||
accepted trade there) and **vision is ON** (the 27B engine silently
|
||||
ignored its encoder — frank-zealot owned images for exactly one day at this
|
||||
ignored its encoder — frank-secondary owned images for exactly one day at this
|
||||
endpoint).
|
||||
|
||||
**Engine laws that shape the config** (all in `scripts/frank-dragoon.env`
|
||||
**Engine laws that shape the config** (all in `scripts/frank-large.env`
|
||||
with evidence): `HALOGEN_SPEC_ADAPT=0` is MANDATORY — the default adaptive
|
||||
policy disables the MTP head for whole requests after low acceptance and
|
||||
HALVES decode; the head stays on and temp-1.0 traffic pays some acceptance
|
||||
@@ -429,7 +413,7 @@ page cache beside the checkpoint — the card's quickstart arithmetic does
|
||||
not bill it as resident, but READ the boot ledger and re-size if the
|
||||
engine prints a clamp.
|
||||
|
||||
Fetch: `scripts/download-frank-dragoon-flashnext.sh` into
|
||||
Fetch: `scripts/download-frank-large-flashnext.sh` into
|
||||
`/home/dank/models/halogen-flashnext-v2/` (`-f` + exact byte counts + LFS
|
||||
sha256 + `--verify-only`, no token on the command line; resume goes
|
||||
against the RESOLVED CDN URL because curl drops `Range` across HF's 302).
|
||||
@@ -438,18 +422,18 @@ n-gram + vision tower, flat tokenizer with all six files as regular
|
||||
files) — the 2026-10-01 crash-loop lesson.
|
||||
|
||||
Runbook:
|
||||
`serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md`.
|
||||
`serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md`.
|
||||
Rollback: revert the swap commit and redeploy — the 27B payload
|
||||
(`/home/dank/models/halogen-qwen3.8-27b/`), its fetch script and the
|
||||
halogen 0.1.4 image digest all remain on disk; the ROCmFPX door behind
|
||||
that (`scripts/build-rocmfpx.sh`, pin `36e9acd`) is untouched.
|
||||
|
||||
### frank-zealot — llama.cpp on the RTX 3090 (Dankclaw, ACTIVE)
|
||||
### frank-secondary — llama.cpp on the RTX 3090 (Dankclaw, ACTIVE)
|
||||
|
||||
**llama.cpp master** (0.5.0-dev, commit `5fc4f3c`; CUDA sm_86 build
|
||||
`/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server`,
|
||||
`GGML_CUDA_FA_ALL_QUANTS=ON`) as a **root systemd unit**
|
||||
(`scripts/frank-zealot.service`, `relink_unit`ed into
|
||||
(`scripts/frank-secondary.service`, `relink_unit`ed into
|
||||
`/etc/systemd/system/`), serving
|
||||
`HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive` at **Q6_K**
|
||||
(7,359,259,008 B, sha256 `c0ba7beb…` computed locally — the Hub carries no
|
||||
@@ -505,7 +489,7 @@ tokens). With `--parallel 2` a compaction request no longer serializes behind
|
||||
a live generation (the Gemma4 era's `--parallel 1` law).
|
||||
|
||||
Runbook:
|
||||
`serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md`.
|
||||
`serverAppConfigs-docs/LLM/frank-secondary-llama-qwen35-9b-q6k-2x200704.md`.
|
||||
Rollback: revert the swap commit — the Gemma4 payload
|
||||
(`/home/dank/models/Gemma4-26B-A4B/`), the MiMo Q6_K payload
|
||||
(`/home/dank/models/MiMo-Q6K/`), the Swift payload
|
||||
@@ -513,7 +497,7 @@ Rollback: revert the swap commit — the Gemma4 payload
|
||||
(`/home/dank/models/Ornith-Q8/`) all remain on disk.
|
||||
|
||||
|
||||
### frank-archon — Radiance (StillDeadcode), Swift1.5-Qwen3.8-Flash-Next GPTQ w4, TP=2 (Dankserver, ACTIVE)
|
||||
### frank-primary — Radiance (StillDeadcode), Swift1.5-Qwen3.8-Flash-Next GPTQ w4, TP=2 (Dankserver, ACTIVE)
|
||||
|
||||
The **radiance engine** (StillDeadcode, Apache-2.0; image
|
||||
`stilldeadcode/radiance@sha256:4c3f484564…` digest-pinned, **pin 1.0.13**
|
||||
@@ -540,61 +524,41 @@ the throughput gate (decode ≥ 30.8 t/s) was passed at 4.5× the bar
|
||||
Rollback: revert the swap commit — the fp8-native `.rad`
|
||||
(`qwen4exp-fp8-native.rad`) and every prior payload remain on disk.
|
||||
|
||||
**Shape (both R9700s, TP=2)**: `--tp-wire wht6`, `--max-num-seqs 2`,
|
||||
**Shape (both R9700s, TP=2)**: `--tp-wire wht6`, `--max-num-seqs 3`,
|
||||
`--max-model-len 200000` (the harness contextWindow must quote this),
|
||||
`--placement expert_tiered --host-pool-mib 44928` + **explicit VRAM
|
||||
partition `--vram-weights-mib 22208 --vram-kv-mib 6912`** (the vram-pair
|
||||
replaced `--expert-vs-cache-ratio`; the two are ONE budget —
|
||||
`weights + kv ≤ 29,120 MiB`, from the measured card 31,856 minus 1,710
|
||||
consumed outside both budgets minus 1,024 to keep free), `--kv-cache-dtype`
|
||||
**`bf16` — unquantized KV since 2026-10-11**, `--num-speculative-tokens 2`,
|
||||
`--max-num-batched-tokens 2048`, prefix cache 8 GiB pinned host + 64 GiB
|
||||
`--placement expert_tiered --host-pool-mib 40960` + **explicit VRAM
|
||||
partition `--vram-weights-mib 26071 --vram-kv-mib 3543`** (the vram-pair
|
||||
replaced `--expert-vs-cache-ratio`; the pair sizes the fp8-KV shape at the
|
||||
deployed boot), `--kv-cache-dtype fp8` (inherited from the fp8-native
|
||||
env), `--num-speculative-tokens 2`,
|
||||
`--max-num-batched-tokens 2048`, prefix cache 4 GiB pinned host + 64 GiB
|
||||
disk at `/home/dank/radiance-state`, **`--ngram-placement ram`**, Froggeric
|
||||
v22.5 chat template vendored (`scripts/qwen38-froggeric-v22.5.jinja`,
|
||||
`--override-chat-template`). Boot ledger: KV pool 6.75 GiB = **462,752 bf16
|
||||
tokens, "backs 2 sequences all at 200000 tokens at once"** (the fp8 pool's
|
||||
386,904 backed 1.93), weights slab filled to 147 KiB of slack, host pool
|
||||
10,375 slots, cards settled 1.85 GiB free.
|
||||
`--override-chat-template`). Boot ledger: KV pool 3.46 GiB (~480k fp8
|
||||
tokens), two expert pools 57.84 GiB, cards settled 1.41 GiB free.
|
||||
|
||||
Shape notes carried from the fp8-native era and unchanged: wht6 gains,
|
||||
MNDT 2048 law (2560 collapses decode to 2.6–7.7 t/s), the 64 GiB disk
|
||||
prefix cache, and the rule that a prompt over `--max-model-len` is
|
||||
refused, never truncated. What changed: the fp8-native era's NVMe expert
|
||||
streaming (the 3.5 GB/s wall) is gone — the w4 resident set fits the
|
||||
host pool, so decode ran 122–173 t/s in the campaign arms and ~190 t/s
|
||||
on the fp8 shape (512-token streamed probe). The campaign also
|
||||
40 GiB pool, so decode ran 122–173 t/s in the campaign arms and ~190 t/s
|
||||
on the deployed shape (512-token streamed probe). The campaign also
|
||||
measured the int8-KV + spec-3 arm (136.0 t/s median at 44.5k depth,
|
||||
accept 0.7246).
|
||||
|
||||
**The bf16 KV law (2026-10-11)**: `--kv-cache-dtype` takes ONLY `bf16|fp8` —
|
||||
one dtype for the whole cache, so llama.cpp's exact-K/quantized-V pair (what
|
||||
frank-zealot runs) has NO radiance equivalent; the rung under bf16 is fp8 on
|
||||
both halves. Measured per rank from the two boot ledgers: `kv_attn` (13
|
||||
layers, 4-token blocks) pages 26.00 → 52.00 KiB, while `kv_qsa_bkey` (13
|
||||
layers) stays 3.25 KiB at BOTH dtypes (the QSA block key is an index, not
|
||||
attention state) — so a token costs 7.3125 KiB at fp8 and 13.8125 KiB at
|
||||
bf16: **1.89×, not 2×**. Pool sizing must also pay the dtype-independent terms
|
||||
that live in the same pool (~110 MiB a sequence of recurrent state + 448 MiB
|
||||
of prefix checkpoints): `(vram_kv − 110·seqs − 448) MiB / 13.8125 KiB` =
|
||||
462,946 where the engine reported 462,752 (0.04 % page rounding). The cost is
|
||||
paid twice on this engine, and half-refunded: idle KV VRAM is also the expert
|
||||
cache's spillover, and the bigger pool *lends* 6.08 GiB to the slab where the
|
||||
fp8 pool lent 2.69 GiB. `--max-num-seqs 3 → 2` came with the change (two slots
|
||||
× the window is the capacity contract; the "seqs 2 cannot boot" law belongs to
|
||||
the retired tcclaviger fork's torch.compile, not radiance) and it also drops
|
||||
the fused all-reduce to 2 × 3 = 6 rows, under the 25-row bound where wht6 is
|
||||
bit-exact. Disk-tier sizing law: the engine forces ~9.6 GiB of disk-tier weight
|
||||
accept 0.7246); production shipped the fp8-KV + spec-2 pair at cutover —
|
||||
the KLD gate scored at the fp8-KV shape, so the gate and the boot agree.
|
||||
Disk-tier sizing law: the engine forces ~9.6 GiB of disk-tier weight
|
||||
buffers + arena onto the weights side, so `weights + kv` is capped near
|
||||
29.6 GiB/card; boot with the pair, never `--kv-cache-size-mib`.
|
||||
|
||||
**The pinned-RAM ledger is why this is a root unit**: 2 × 44 GiB expert
|
||||
**The pinned-RAM ledger is why this is a root unit**: 2 × 40 GiB expert
|
||||
pools + the 47.7 GiB n-gram copy (`--ngram-placement ram` is pinned RAM
|
||||
the card reads) + 8 GiB prefix host ≈ **147 GiB** charged against
|
||||
the card reads) + 4 GiB prefix host ≈ **132 GiB** charged against
|
||||
`RLIMIT_MEMLOCK` — the TTM floor is set just above it.
|
||||
`ttm.pages_limit`/`pages_max` cap GPU-mappable pinned pages; the serve
|
||||
script's TTM preflight (`FRANK_ARCHON_TTM_MIN_MIB=150000`, cap on this host
|
||||
204,800) refuses to start until both knobs are raised in the stop window
|
||||
(sysfs write first, kernel cmdline + reboot only if read-only).
|
||||
script's TTM preflight (`FRANK_PRIMARY_TTM_MIN_MIB=140000`) refuses to
|
||||
start until both knobs are raised in the stop window (sysfs write first,
|
||||
kernel cmdline + reboot only if read-only).
|
||||
|
||||
**Contract facts** (source-verified): the HTTP listener binds AFTER the
|
||||
full load, so a bare `curl /health` gate is honest (`/health` == `/ping`,
|
||||
@@ -606,11 +570,11 @@ chat format — no parser flags; vision native (the 27-layer Qwen-VL tower,
|
||||
`--mm-max-patches`); `/metrics` carries vLLM-compatible names +
|
||||
`radiance:*` families — the Dankclaw Prometheus scrape is re-added.
|
||||
|
||||
Runbook: `serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md`.
|
||||
Retired fp8-native era: `serverAppConfigs-docs/LLM/frank-archon-radiance-qwen38-native-fp8.md`.
|
||||
Retired FreeToken era: `serverAppConfigs-docs/LLM/frank-archon-freetoken-pr587.md`.
|
||||
Runbook: `serverAppConfigs-docs/LLM/frank-primary-radiance-swift15-w4dn8.md`.
|
||||
Retired fp8-native era: `serverAppConfigs-docs/LLM/frank-primary-radiance-qwen38-native-fp8.md`.
|
||||
Retired FreeToken era: `serverAppConfigs-docs/LLM/frank-primary-freetoken-pr587.md`.
|
||||
Retired vLLM era: `serverAppConfigs-docs/LLM/dankserver-qwen38-fp8-expert-offload-tuning.md`.
|
||||
Retired GLM experiment: `serverAppConfigs-docs/LLM/frank-archon-glm53-flash-w4a16.md`.
|
||||
Retired GLM experiment: `serverAppConfigs-docs/LLM/frank-primary-glm53-flash-w4a16.md`.
|
||||
|
||||
## Key Lessons Learned
|
||||
Durable operational lessons from the fleet's history. Full detail in the
|
||||
@@ -772,11 +736,11 @@ linked runbooks.
|
||||
- Swap resize ordering: create and `swapon` the replacement BEFORE `swapoff`
|
||||
of the old one, or live pages must land in RAM that `MemAvailable`
|
||||
overstates. Dankclaw swap (64 GiB at `/swap.img`, fstab-driven) protects
|
||||
the co-tenants, not frank-dragoon — pinned weights don't swap, and paging
|
||||
the co-tenants, not frank-large — pinned weights don't swap, and paging
|
||||
the KV pool is worse than the OOM it prevents.
|
||||
- Host swap state is invisible to `deploy.sh` (no repo file) — check
|
||||
`swapon --show` before reasoning about headroom.
|
||||
- **Silent RAM corruption outranks every software theory.** frank-archon's
|
||||
- **Silent RAM corruption outranks every software theory.** frank-primary's
|
||||
~40k-token output corruption (2026-10-04/05, repetition/garbage in long
|
||||
agent conversations) was marginal DDR5 at 4800 MT/s on four DIMMs, fixed
|
||||
by 4600. Consumer non-ECC memory raises **no MCE** for a silent flip, and
|
||||
@@ -799,7 +763,7 @@ linked runbooks.
|
||||
`LimitMEMLOCK=infinity` (systemd is privileged; the value flows through
|
||||
podman to runc without any raise). Upstream halogen's own run lines pass
|
||||
NO `--ulimit` at all — copy vendor run lines before adding flags
|
||||
(measured 2026-10-05 on the frank-dragoon cutover).
|
||||
(measured 2026-10-05 on the frank-large cutover).
|
||||
- **Kairic Edge** (if ever re-deployed): the pinned CK commit needs
|
||||
`third_party/composable_kernel/include/ck/config.h` generated manually
|
||||
(untracked; `git clean` removes it), and its greedy-argmax fastpath rejects
|
||||
@@ -825,7 +789,7 @@ linked runbooks.
|
||||
121 GiB box, pushed `available` to 3 GiB, and every later rate was suspect.
|
||||
Record the child pid and kill it in a `finally`, then prove `free` came back
|
||||
before believing a number. Kill by pid — never `pkill -f llama-server`,
|
||||
which also hits frank-zealot.
|
||||
which also hits frank-secondary.
|
||||
|
||||
**llama.cpp serving**
|
||||
|
||||
@@ -849,7 +813,7 @@ linked runbooks.
|
||||
builds its context against whatever model the draft context holds. Two
|
||||
context-backed types cannot share one process, and MTP's head only loads
|
||||
when MTP **is** the draft model. **A LEGAL chain can still lose on a
|
||||
recurrent model:** `draft-mtp,ngram-mod` on frank-dragoon's qwen35 hybrid
|
||||
recurrent model:** `draft-mtp,ngram-mod` on frank-large's qwen35 hybrid
|
||||
measured −15 % prose / −7 % block-copy (2026-10-03) — adding the ngram layer
|
||||
logs `recurrent state rollback is not compatible with 'ngram-mod' -
|
||||
disabling rollback support`, and MTP verification depends on that rollback,
|
||||
@@ -891,7 +855,7 @@ linked runbooks.
|
||||
free. That was a CONTIGUITY failure, not a total-bytes one, and no totals
|
||||
model — the sweep's or the seam's — can see it.)
|
||||
- **On the CUDA 3090 the same knob has NO interior optimum up to 2048, so
|
||||
frank-zealot ships 4096/2048.** Measured 2026-10-05 by boot-pair A/B on the
|
||||
frank-secondary ships 4096/2048.** Measured 2026-10-05 by boot-pair A/B on the
|
||||
live service: at 8,928 prompt tokens ubatch 1024 gives 3,972 / 4,077 t/s and
|
||||
ubatch 2048 gives 4,207 / 4,212 — **+4.6 %** — for **616 MiB** (free 2,849 →
|
||||
2,279). Decode did not move (91.9–93.0 vs 93.8–94.3, i.e. noise), and a
|
||||
@@ -900,9 +864,9 @@ linked runbooks.
|
||||
than the ubatch cannot see it. The logical `--batch-size` was again free. And
|
||||
the predecessor's "ubatch 2048 failed the scratch boot" was a shape
|
||||
observation, not a law: the same file boots 2048 clean at 2 × 200,704. Bench
|
||||
it by restarting the SERVICE through a transient `FRANK_ZEALOT_ENV_FILE`
|
||||
it by restarting the SERVICE through a transient `FRANK_SECONDARY_ENV_FILE`
|
||||
drop-in, never a scratch port — one card holds one server. See
|
||||
[[frank-zealot-llama-mimo-q6k-2x200704]].
|
||||
[[frank-secondary-llama-mimo-q6k-2x200704]].
|
||||
- **One `-ts` step is one LAYER, and a hybrid stack's layers are not the same
|
||||
size.** qwen35 is 16 x (3 Gated DeltaNet + 1 Gated Attention), so a step is
|
||||
worth ~0.43 GiB on a DeltaNet boundary and ~1.43 GiB on a full-attention one
|
||||
@@ -924,7 +888,7 @@ linked runbooks.
|
||||
CTX_SIZE / PARALLEL`, and llama.cpp refuses a prompt longer than the SLOT
|
||||
(`400 exceed_context_size_error: request (184001 tokens) exceeds the
|
||||
available context size (180224 tokens)`) even when the pool has room. So
|
||||
frank-zealot's 2 → 3 reslice moved each conversation from the model's native
|
||||
frank-secondary's 2 → 3 reslice moved each conversation from the model's native
|
||||
262,144 down to 180,224, and `models.yml` `contextWindow` had to follow or
|
||||
the harness would discover the ceiling as a 400 on a long conversation. Side
|
||||
effect worth knowing: the shorter slot made decode ~5 % FASTER (106.8 vs
|
||||
@@ -935,9 +899,9 @@ linked runbooks.
|
||||
day the freed space also bought the Q6_K rung (now 94.4 tok/s — the rung
|
||||
itself is a further measured −10 %, weight-bandwidth, not slot geometry).
|
||||
All moves measured; both slot directions cost or buy window, never pool.
|
||||
See [[frank-zealot-llama-mimo-q6k-2x200704]].
|
||||
See [[frank-secondary-llama-mimo-q6k-2x200704]].
|
||||
- **Price a VRAM budget from two boots, never from a formula — and an mmproj
|
||||
costs ~1.5x its file size.** The frank-zealot 3090 fit was solved from two
|
||||
costs ~1.5x its file size.** The frank-secondary 3090 fit was solved from two
|
||||
measurements: 2 × 262,144 with no projector = 20,190 MiB used (KV 12,800 ⇒
|
||||
**non-KV 7,390**), 3 × 196,608 with the f16 projector = 22,972 used (KV
|
||||
14,113 ⇒ **non-KV 8,859**). The difference is the vision tower: a 918 MB file
|
||||
@@ -945,24 +909,24 @@ linked runbooks.
|
||||
ride with it. Subtracting the two rows is exact; extrapolating weights+KV+
|
||||
"a bit for buffers" is what leaves 1.1 GiB free when you meant 2.7. Then
|
||||
solve `KV ≤ card − non-KV − headroom` and round to a multiple of
|
||||
`PARALLEL × 4096`. Recorded in `scripts/frank-zealot.env` so the next resize
|
||||
`PARALLEL × 4096`. Recorded in `scripts/frank-secondary.env` so the next resize
|
||||
starts from measurement.
|
||||
- **`images` and `image_url` are NOT interchangeable: only the content part
|
||||
reaches the model.** `--mmproj` loads, `usage.prompt_tokens` rises, and the
|
||||
model still invents an answer if the client used the legacy top-level
|
||||
`images` field on `/v1/chat/completions` — frank-zealot answered "Brown." for
|
||||
`images` field on `/v1/chat/completions` — frank-secondary answered "Brown." for
|
||||
a solid-red PNG that way and "Red." through the OpenAI `image_url` part. The
|
||||
failure is silent because a hallucinated color is a plausible answer. Probe
|
||||
vision with a solid color and expect an exact word.
|
||||
- **`--jinja` is what `tools` needs.** On 2026-09-16 a subagent died with
|
||||
`500 tools param requires --jinja flag`, from the ik_llama.cpp runtime that
|
||||
then served frank-dragoon: every tool-using agent on that endpoint was broken
|
||||
then served frank-large: every tool-using agent on that endpoint was broken
|
||||
and the failure read like a model fault. Mainline llama.cpp 0.4.1 defaults
|
||||
`--jinja` on and the serve script pins it; halogen also accepts `tools` and
|
||||
returns `tool_calls`. Check tool support before routing a role at an endpoint.
|
||||
- **Quantized KV on CUDA needs `GGML_CUDA_FA_ALL_QUANTS=ON`** — the same
|
||||
option and the same ~12x prefill collapse described in the ROCm note above.
|
||||
f16 KV needs nothing; frank-zealot ran f16 for that reason until
|
||||
f16 KV needs nothing; frank-secondary ran f16 for that reason until
|
||||
2026-09-17, when `build-cuda` was rebuilt with the option ON so the
|
||||
endpoint could take q8_0 KV.
|
||||
- **llama-bench separates a LIST with `/` and SWEEP values with `,`.** Both
|
||||
@@ -980,7 +944,7 @@ linked runbooks.
|
||||
raises on anything else — HTTP 500, not a clamp — so the harness's levels
|
||||
must be folded onto that set in `models.yml` (`compat.reasoningEffortMap`).
|
||||
A role whose level is unmapped fails on its FIRST request, and the failure
|
||||
names Jinja, not the model: every subagent on `frank-zealot` died with
|
||||
names Jinja, not the model: every subagent on `frank-secondary` died with
|
||||
`Unexpected reasoning effort high` until `high: xhigh` was added on
|
||||
2026-09-16. Halogen accepts the wider set, so the two entries legitimately
|
||||
differ.
|
||||
@@ -1000,7 +964,7 @@ linked runbooks.
|
||||
times (`RuntimeError: No HIP GPUs are available`).
|
||||
- SearXNG outbound traffic must stay on the Gluetun tunnel; do not move its
|
||||
ports off the gluetun service.
|
||||
- `amd_iommu=off` on Dankclaw (bought 13–16 % prefill for frank-dragoon,
|
||||
- `amd_iommu=off` on Dankclaw (bought 13–16 % prefill for frank-large,
|
||||
2026-09-11) permanently retired the XDNA2 NPU: the driver's SVA/PASID bind
|
||||
needs an enabled IOMMU (`/dev/accel/accel0` fails `err=-19`). Revert path
|
||||
in `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md`.
|
||||
@@ -1054,9 +1018,9 @@ Active:
|
||||
|
||||
| Service | Runbook |
|
||||
|---------|---------|
|
||||
| frank-dragoon (flash-server 0.16.2, abliterated Swift1.5 v2 .hgn + MTP always-on, 2×262,144, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md` |
|
||||
| frank-zealot (llama.cpp CUDA, Qwen3.5-9B-Uncensored Q6_K, 2×200,704 exact f16 KV MTP-off, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-zealot-llama-qwen35-9b-q6k-2x200704.md` |
|
||||
| frank-archon (radiance StillDeadcode, Swift1.5-Qwen3.8-Flash-Next GPTQ w4 + bf16-protected experts, int8 trunk, bf16 KV, 2×R9700 TP=2, Dankserver) | `serverAppConfigs-docs/LLM/frank-archon-radiance-swift15-w4dn8.md` |
|
||||
| frank-large (flash-server 0.16.2, abliterated Swift1.5 v2 .hgn + MTP always-on, 2×262,144, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md` |
|
||||
| frank-secondary (llama.cpp CUDA, Qwen3.5-9B-Uncensored Q6_K, 2×200,704 exact f16 KV MTP-off, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-secondary-llama-qwen35-9b-q6k-2x200704.md` |
|
||||
| frank-primary (radiance StillDeadcode, Qwen3.8-Flash-Next-FP8 native, 2×R9700 TP=2, Dankserver) | `serverAppConfigs-docs/LLM/frank-primary-radiance-qwen38-native-fp8.md` |
|
||||
| Dankclaw kernel posture (`amd_iommu=off`) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
|
||||
| Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` |
|
||||
|
||||
@@ -1064,42 +1028,41 @@ Retired (rollback targets and history):
|
||||
|
||||
| Runtime | Host | Window | Runbook |
|
||||
|---------|------|--------|---------|
|
||||
| llama.cpp CUDA Gemma4-26B-A4B QAT Q4_K_M, 1×200,704 f16-K/q8_0-V MTP-off (`frank-zealot`) | Dankclaw | 2026-10-07 → 10-07 (same-day swap; rollback target — payload on disk; owns the `attention_k_eq_v` pair-billing KV law and the gemma4-assistant drafter-cost boots) | `frank-zealot-llama-gemma4-q4km-200704.md` |
|
||||
| radiance StillDeadcode Qwen3.8-Flash-Next native-FP8, 2×R9700 TP=2 (`frank-archon`) | Dankserver | 2026-10-08 → w4dn8 swap (rollback target — `.rad` payload on disk) | `frank-archon-radiance-qwen38-native-fp8.md` |
|
||||
| ROCmFPX llama.cpp fork Qwen3.8-27B Uncensored Q6_0_ROCMFPX_AGENT + MTP (`frank-dragoon`) | Dankclaw | 2026-10-03 → 10-05 (rollback target, payload + fork build on disk) | `frank-dragoon-rocmfpx-q6agent-uncensored-mtp.md` |
|
||||
| tcclaviger vLLM fork Qwen3.8-Flash-Next-Uncensored FP8, TP=2, shape G (`frank-archon`) | Dankserver | 2026-09-29 → 10-05 (rollback target — tag `frank-archon-vllm-shapeG-20261003`; trio + env lore in the tag, image in rootless storage) | `dankserver-qwen38-fp8-expert-offload-tuning.md` |
|
||||
| FreeToken PR #587 ROCm, Qwen3.8-Flash-Next-Uncensored-FP8, ONE R9700 (`frank-archon`) | Dankserver | 2026-10-05 → 10-05 (rollback target — tag `frank-archon-freetoken-pr587-20261005`; trio + Dockerfile in the tag, image `localhost/frank-archon-freetoken:pr587-77a7201` and orcarouter payload on disk) | `frank-archon-freetoken-pr587.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K 2×200,704 f16 KV (`frank-zealot`) | Dankclaw | 2026-10-03 → 10-05 (rollback target — payload on disk; owns the ubatch-2048 A/B; the Ornith swap of 10-05 was committed but never deployed, so this was the LIVE payload Swift replaced) | `frank-zealot-llama-mimo-q6k-2x200704.md` |
|
||||
| llama.cpp Ornith-1.5-9B Q8_0, ONE 262,144 f16 KV (`frank-zealot`) | Dankclaw | 2026-10-05 (committed 77decb5→380aafa, never deployed — superseded by Swift the same evening; payload on disk) | trail in `frank-zealot-llama-swift15-unc-iq3xxs-mtp-114688.md` |
|
||||
| llama.cpp CUDA Swift-1.5-27B Uncensored RCO IQ3_XXS-mtp, 2×114,688 draft-mtp q8_0 KV (`frank-zealot`) | Dankclaw | 2026-10-06 → 2026-10-07 (rollback target — payload on disk; owns the 3090 ubatch ladder A/B and the 34 KiB/token qwen35 KV law) | `frank-zealot-llama-swift15-unc-iq3xxs-mtp-114688.md` |
|
||||
| ROCmFPX llama.cpp fork Qwen3.8-27B Q8_0_ROCMFPX_AGENT + MTP (`frank-dragoon`) | Dankclaw | 2026-10-02 → 10-03 (rollback target, payload on disk) | `frank-dragoon-rocmfpx-q8agent-mtp.md` |
|
||||
| gufo v0.1.1 Qwen3.8-27B UD-Q8_K_XL/UD-Q4_K_XL + DFlash2 (`frank-dragoon`) | Dankclaw | 2026-09-28 → 10-01, again 10-01 → 10-02 (payloads removed from disk 2026-10-03; rollback = re-download) | `frank-dragoon-gufo-qwen38-27b.md` |
|
||||
| vLLM TheRock MiMo-V2.6-Distill-Qwen-9B BF16 + cross-model MTP (`frank-dragoon`) | Dankclaw | 2026-10-01 (same-day rollback to gufo) | `frank-dragoon-vllm-mimo-v26-bf16.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K (`frank-zealot`) | Dankclaw | 2026-10-01 → 10-02 (the rung returned ACTIVE 2026-10-03 at the 2×200,704 shape — `frank-zealot-llama-mimo-q6k-2x200704.md`) | `frank-zealot-llama-mimo-q6k.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q5_K_M 2×200,704 f16 KV (`frank-zealot`) | Dankclaw | 2026-10-02 → 10-03 (rollback target — one `MODEL_FILE` line, buys back 875 MiB + the −10 % rung tax; owns the boot-pair table) | `frank-zealot-llama-mimo-q5km.md` |
|
||||
| SGLang MiMo-V2.6 INT8-W8A16-AutoRound (`frank-zealot`) | Dankclaw | 2026-10-01 (same-day swap to the GGUF path) | `frank-zealot-sglang-mimo-v26.md` |
|
||||
|tcclaviger vLLM GLM-5.3-Flash W4A16 int4, +/- DFlash2 drafter (`frank-archon`)|Dankserver|2026-09-30 → 10-01|`frank-archon-glm53-flash-w4a16.md`|
|
||||
| llama.cpp b11030 Qwen3.8-27B UD-Q8_K_XL + DFlash2 (`frank-dragoon`) | Dankclaw | 2026-09-21 → 09-28 | `frank-dragoon-unsloth-qwen38-27b-q8xl.md` |
|
||||
| R9V radiance-vLLM Qwen3.8-Flash-Next-Uncensored IQ4_XS (`frank-archon`) | Dankserver | 2026-09-27 (same-day swap to Q8_0) | `frank-archon-r9v-radiance-qwen38-uncensored.md` |
|
||||
| llama.cpp b11030 Qwen3.8-Flash-Next Q8_0 + shared MTP (`frank-archon`) | Dankserver | 2026-09-26 → 09-27 | `frank-archon-llamacpp-qwen38-flash-next-q8.md` |
|
||||
| ik_llama.cpp Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M (`frank-dragoon`) | Dankclaw | 2026-09-15 → 09-16 | `frank-dragoon-ik-uncensored.md` |
|
||||
| llama.cpp CUDA Gemma4-26B-A4B QAT Q4_K_M, 1×200,704 f16-K/q8_0-V MTP-off (`frank-secondary`) | Dankclaw | 2026-10-07 → 10-07 (same-day swap; rollback target — payload on disk; owns the `attention_k_eq_v` pair-billing KV law and the gemma4-assistant drafter-cost boots) | `frank-secondary-llama-gemma4-q4km-200704.md` |
|
||||
| ROCmFPX llama.cpp fork Qwen3.8-27B Uncensored Q6_0_ROCMFPX_AGENT + MTP (`frank-large`) | Dankclaw | 2026-10-03 → 10-05 (rollback target, payload + fork build on disk) | `frank-large-rocmfpx-q6agent-uncensored-mtp.md` |
|
||||
| tcclaviger vLLM fork Qwen3.8-Flash-Next-Uncensored FP8, TP=2, shape G (`frank-primary`) | Dankserver | 2026-09-29 → 10-05 (rollback target — tag `frank-primary-vllm-shapeG-20261003`; trio + env lore in the tag, image in rootless storage) | `dankserver-qwen38-fp8-expert-offload-tuning.md` |
|
||||
| FreeToken PR #587 ROCm, Qwen3.8-Flash-Next-Uncensored-FP8, ONE R9700 (`frank-primary`) | Dankserver | 2026-10-05 → 10-05 (rollback target — tag `frank-primary-freetoken-pr587-20261005`; trio + Dockerfile in the tag, image `localhost/frank-primary-freetoken:pr587-77a7201` and orcarouter payload on disk) | `frank-primary-freetoken-pr587.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K 2×200,704 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-03 → 10-05 (rollback target — payload on disk; owns the ubatch-2048 A/B; the Ornith swap of 10-05 was committed but never deployed, so this was the LIVE payload Swift replaced) | `frank-secondary-llama-mimo-q6k-2x200704.md` |
|
||||
| llama.cpp Ornith-1.5-9B Q8_0, ONE 262,144 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-05 (committed 77decb5→380aafa, never deployed — superseded by Swift the same evening; payload on disk) | trail in `frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md` |
|
||||
| llama.cpp CUDA Swift-1.5-27B Uncensored RCO IQ3_XXS-mtp, 2×114,688 draft-mtp q8_0 KV (`frank-secondary`) | Dankclaw | 2026-10-06 → 2026-10-07 (rollback target — payload on disk; owns the 3090 ubatch ladder A/B and the 34 KiB/token qwen35 KV law) | `frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md` |
|
||||
| ROCmFPX llama.cpp fork Qwen3.8-27B Q8_0_ROCMFPX_AGENT + MTP (`frank-large`) | Dankclaw | 2026-10-02 → 10-03 (rollback target, payload on disk) | `frank-large-rocmfpx-q8agent-mtp.md` |
|
||||
| gufo v0.1.1 Qwen3.8-27B UD-Q8_K_XL/UD-Q4_K_XL + DFlash2 (`frank-large`) | Dankclaw | 2026-09-28 → 10-01, again 10-01 → 10-02 (payloads removed from disk 2026-10-03; rollback = re-download) | `frank-large-gufo-qwen38-27b.md` |
|
||||
| vLLM TheRock MiMo-V2.6-Distill-Qwen-9B BF16 + cross-model MTP (`frank-large`) | Dankclaw | 2026-10-01 (same-day rollback to gufo) | `frank-large-vllm-mimo-v26-bf16.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K (`frank-secondary`) | Dankclaw | 2026-10-01 → 10-02 (the rung returned ACTIVE 2026-10-03 at the 2×200,704 shape — `frank-secondary-llama-mimo-q6k-2x200704.md`) | `frank-secondary-llama-mimo-q6k.md` |
|
||||
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q5_K_M 2×200,704 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-02 → 10-03 (rollback target — one `MODEL_FILE` line, buys back 875 MiB + the −10 % rung tax; owns the boot-pair table) | `frank-secondary-llama-mimo-q5km.md` |
|
||||
| SGLang MiMo-V2.6 INT8-W8A16-AutoRound (`frank-secondary`) | Dankclaw | 2026-10-01 (same-day swap to the GGUF path) | `frank-secondary-sglang-mimo-v26.md` |
|
||||
|tcclaviger vLLM GLM-5.3-Flash W4A16 int4, +/- DFlash2 drafter (`frank-primary`)|Dankserver|2026-09-30 → 10-01|`frank-primary-glm53-flash-w4a16.md`|
|
||||
| llama.cpp b11030 Qwen3.8-27B UD-Q8_K_XL + DFlash2 (`frank-large`) | Dankclaw | 2026-09-21 → 09-28 | `frank-large-unsloth-qwen38-27b-q8xl.md` |
|
||||
| R9V radiance-vLLM Qwen3.8-Flash-Next-Uncensored IQ4_XS (`frank-primary`) | Dankserver | 2026-09-27 (same-day swap to Q8_0) | `frank-primary-r9v-radiance-qwen38-uncensored.md` |
|
||||
| llama.cpp b11030 Qwen3.8-Flash-Next Q8_0 + shared MTP (`frank-primary`) | Dankserver | 2026-09-26 → 09-27 | `frank-primary-llamacpp-qwen38-flash-next-q8.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-archon`) | Dankserver | 2026-09-26 (same-day swap) | `frank-archon-lucebox-ds4.md` |
|
||||
| Lucebox DS4.1 DeepSeek-V4.1-Flash ROCmFP23 (`frank-primary`) | Dankserver | 2026-09-26 (same-day swap) | `frank-primary-lucebox-ds4.md` |
|
||||
| kingjones STRIX Qwen3.8-Flash-Next ROCmFP4 | Dankclaw | 2026-08-30 → 09-11 | `kingjones-strix-rocmfp4.md` |
|
||||
| halogen-flash-server Qwen3.8-Flash-Next w4b (`frank-dragoon`) | Dankclaw | 2026-09-16 → 09-19 | `halogen-flash-server.md` |
|
||||
| CIRU v4.4.1 Orca Qwen3.8-Flash (`frank-dragoon`) | Dankclaw | 2026-09-19 → 09-21 | `frank-dragoon-ciru-orca.md` |
|
||||
| unsloth llama.cpp b11030 Qwen3.8-Flash-Next UD-IQ4_XS (`frank-dragoon`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-dragoon-unsloth-qwen38-flash-next.md` |
|
||||
| Kairic Edge Qwen3.8-27B IU4 W4A4 (`frank-dragoon`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-dragoon-kairic-edge-qwen38-27b.md` |
|
||||
| ISTA-DASLab Qwen3.8-27B-GSQ-RCO IQ3_S (`frank-zealot`) | Dankclaw | 2026-09-16 → 09-17 | `frank-zealot-qwen38-gsq-rco.md` |
|
||||
| vLLM Gemma4-12B w4a16 (`frank-zealot`) | Dankclaw | 2026-09-19 (same-day revert) | `frank-zealot-vllm-gemma4.md` |
|
||||
| llama.cpp Gemma4-12B QAT Q4_K_M (`frank-zealot`) | Dankclaw | 2026-09-17 → 10-01 | `frank-zealot-gemma4-12b.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` |
|
||||
| Kairic Edge Qwen3.8-27B IU4 W4A4 (`frank-large`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-large-kairic-edge-qwen38-27b.md` |
|
||||
| ISTA-DASLab Qwen3.8-27B-GSQ-RCO IQ3_S (`frank-secondary`) | Dankclaw | 2026-09-16 → 09-17 | `frank-secondary-qwen38-gsq-rco.md` |
|
||||
| vLLM Gemma4-12B w4a16 (`frank-secondary`) | Dankclaw | 2026-09-19 (same-day revert) | `frank-secondary-vllm-gemma4.md` |
|
||||
| llama.cpp Gemma4-12B QAT Q4_K_M (`frank-secondary`) | Dankclaw | 2026-09-17 → 10-01 | `frank-secondary-gemma4-12b.md` |
|
||||
| agentionai ROCmFP4-FAST imatrix | Dankclaw | 2026-08-29 → 08-30 | `agentionai-fast-imatrix.md` |
|
||||
| eemin Qwen3.8-27B ROCmI4 W4A4 MTP | Dankclaw | 2026-08-18 → 08-25 | `eemin-rocmi4-mtp.md` |
|
||||
| Kairic Edge IU4 | Dankclaw | 2026-08-22 (same-day revert) | `kairic-edge.md` |
|
||||
| CIRU ActiveFPX v3 / PromptForge | Dankclaw | through 2026-08-22 | `ciru-activefpx-promptforge.md` |
|
||||
| DFlash2 Vulkan | Dankclaw | 2026-08-21 (same-day revert) | `dflash2-large-vulkan.md` |
|
||||
| FastFlowLM NPU (`frank-npu`) | Dankclaw | twice, retired 2026-09-11 | `frank-npu.md`, `FastFlowLM NPU Inference.md` |
|
||||
| FreeToken (official + Huihui FP8, `frank-zealot`) | Dankserver | 2026-09-02 → 09-15 | `freetoken-frank-small.md` |
|
||||
| FreeToken (official + Huihui FP8, `frank-secondary`) | Dankserver | 2026-09-02 → 09-15 | `freetoken-frank-small.md` |
|
||||
| FastMTP patched llama.cpp | Dankserver | retired 2026-08-18 | `qwen3.8-spec-matrix.md` |
|
||||
| ik_llama.cpp Ornith CRACK | Dankserver | retired 2026-09-02 | `ikllama-ornith-dankserver.md` |
|
||||
| llama.cpp layer split + DFlash2 (`frank-medium`) | Dankserver | 2026-09-16 → 09-20 | `frank-medium-llamacpp-dflash2.md` |
|
||||
|
||||
@@ -13,7 +13,7 @@ one deploy script.
|
||||
| Host | User | IP | Podman stack | LLM service | LLM port |
|
||||
|------|------|----|--------------|-------------|----------|
|
||||
| dankserver | dank | 192.168.0.235 | media and library | frank-medium (ROCmFPX llama.cpp, Qwen3.8-27B AGENT) | 7770 |
|
||||
| dankclaw | dank | 192.168.0.143 | AI, search, development, observability | frank-dragoon (gufo, Qwen3.8-27B UD-Q8_K_XL) | 7770 |
|
||||
| dankclaw | dank | 192.168.0.143 | AI, search, development, observability | frank-large (gufo, Qwen3.8-27B UD-Q8_K_XL) | 7770 |
|
||||
|
||||
## Active Services
|
||||
|
||||
@@ -30,7 +30,7 @@ one deploy script.
|
||||
| Gitea | dankclaw | gitea.dankclaw.local | 3001 |
|
||||
| Grafana | dankclaw | grafana.dankclaw.local | 3002 |
|
||||
| Prometheus | dankclaw | prometheus.dankclaw.local | 9090 |
|
||||
| frank-dragoon | dankclaw | — (bare endpoint) | 7770 |
|
||||
| frank-large | dankclaw | — (bare endpoint) | 7770 |
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
+9
-9
@@ -1,26 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# autoresearch.sh — canonical frank-archon benchmark entry point (dev box).
|
||||
# autoresearch.sh — canonical frank-primary benchmark entry point (dev box).
|
||||
#
|
||||
# The service runs on dankserver (192.168.0.235:7770), the two R9700s live
|
||||
# there, and the benchmark must own those cards, so the whole stop -> boot
|
||||
# scratch -> bench -> restore sequence runs ON dankserver (see
|
||||
# scripts/autoresearch-frank-archon.sh). This wrapper:
|
||||
# 1. copies the candidate config (the repo's scripts/frank-archon.env, the
|
||||
# scripts/autoresearch-frank-primary.sh). This wrapper:
|
||||
# 1. copies the candidate config (the repo's scripts/frank-primary.env, the
|
||||
# file the autoresearch loop edits) and the bench script to dankserver,
|
||||
# 2. runs the bench over ssh, relaying its stdout (the METRIC lines) and
|
||||
# exit code back here verbatim.
|
||||
# It deliberately does NOT push to origin: the scp dirties dankserver's
|
||||
# working tree, and the loop's uncommitted candidate config must not be
|
||||
# pushed. Only this file and scripts/autoresearch-frank-archon.sh are
|
||||
# pushed. Only this file and scripts/autoresearch-frank-primary.sh are
|
||||
# committed; the candidate env rides over ssh each run.
|
||||
set -uo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REMOTE=dankserver
|
||||
REMOTE_REPO=/home/dank/serverAppConfigs
|
||||
|
||||
scp -q "$DIR/scripts/frank-archon.env" \
|
||||
"$REMOTE:$REMOTE_REPO/scripts/frank-archon.env" || { echo "scp env failed" >&2; exit 1; }
|
||||
scp -q "$DIR/scripts/autoresearch-frank-archon.sh" \
|
||||
"$REMOTE:$REMOTE_REPO/scripts/autoresearch-frank-archon.sh" || { echo "scp script failed" >&2; exit 1; }
|
||||
scp -q "$DIR/scripts/frank-primary.env" \
|
||||
"$REMOTE:$REMOTE_REPO/scripts/frank-primary.env" || { echo "scp env failed" >&2; exit 1; }
|
||||
scp -q "$DIR/scripts/autoresearch-frank-primary.sh" \
|
||||
"$REMOTE:$REMOTE_REPO/scripts/autoresearch-frank-primary.sh" || { echo "scp script failed" >&2; exit 1; }
|
||||
|
||||
ssh "$REMOTE" "bash $REMOTE_REPO/scripts/autoresearch-frank-archon.sh $REMOTE_REPO/scripts/frank-archon.env"
|
||||
ssh "$REMOTE" "bash $REMOTE_REPO/scripts/autoresearch-frank-primary.sh $REMOTE_REPO/scripts/frank-primary.env"
|
||||
|
||||
+27
-24
@@ -56,21 +56,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Named snippet: the pre-auth guard chain. Anubis (:8923) challenges,
|
||||
# the embedded Authentik outpost authenticates, and authenticated
|
||||
# traffic returns to the http://:8017 listener's post-auth handlers.
|
||||
# Every guarded vhost is `import guarded` — the chain is EDITED HERE,
|
||||
# never inlined per site (rank 6: it used to be nine verbatim copies).
|
||||
# Exemptions live in their own vhosts (auth. is the IdP itself; git
|
||||
# smart-HTTP and self-authenticating services document their reason).
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
(guarded) {
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Bare-IP access (health checks, convenience) ---
|
||||
:80 {
|
||||
redir https://www.franksdank.cloud/ 308
|
||||
@@ -87,7 +72,9 @@ franksdank.cloud {
|
||||
}
|
||||
|
||||
www.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Legacy name: the old homepage name redirects straight onto www ---
|
||||
@@ -113,7 +100,9 @@ auth.franksdank.cloud {
|
||||
|
||||
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
|
||||
search.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- OpenWebUI (quadlet on slirp4netns; published 3005) ---
|
||||
@@ -137,22 +126,30 @@ gitea.franksdank.cloud {
|
||||
# never reach Grafana — RP mode needs none and works regardless of topology.
|
||||
# The basicauth edge this replaces (CADDY_BASICAUTH_HASH) is retired.
|
||||
grafana.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Prometheus ---
|
||||
prometheus.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Loki (guarded: Anubis → Authentik; loki-proxy provider, internal_host caddy:8017) ---
|
||||
loki.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Seerr (guarded: Anubis → Authentik; seerr provider, internal_host caddy:8017) ---
|
||||
seerr.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Marreta (guarded: Anubis → Authentik; marreta provider, internal_host caddy:8017) ---
|
||||
@@ -160,7 +157,9 @@ seerr.franksdank.cloud {
|
||||
# group gate). Egress rides the searxng gluetun tunnel; reached via the
|
||||
# host-published port 8082 (gluetun netns), not by container name.
|
||||
marreta.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Pi-hole (quadlet on slirp4netns; published 8083) ---
|
||||
@@ -234,12 +233,16 @@ books.franksdank.cloud {
|
||||
|
||||
# --- RomM ---
|
||||
romm.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Copyparty ---
|
||||
files.franksdank.cloud {
|
||||
import guarded
|
||||
reverse_proxy anubis:8923 {
|
||||
header_up X-Real-Ip {remote_host}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Jellyfin ---
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
# Quadlet unit: node-exporter (rootless, boot-persistent) — RENDERED ARTIFACT.
|
||||
# Regenerate: python3 scripts/service-map.py render node-exporter-unit --host dankclaw
|
||||
# Source: shared/services.yml (entry node-exporter) + this template.
|
||||
# Quadlet unit: node-exporter (rootless, boot-persistent).
|
||||
#
|
||||
# Install path on dankclaw: ~/.config/containers/systemd/node-exporter.container
|
||||
# Unit name: node-exporter.service (container name: node-exporter)
|
||||
#
|
||||
# Host metrics source for Prometheus. Replaces the compose service of the same
|
||||
# name (the "Observability" block): Dankclaw moved to this quadlet on
|
||||
# 2026-09-11, Dankserver on 2026-09-16 — its compose-only copy died at a reboot
|
||||
# and stayed dead (rootless Podman does not restart bare containers after the
|
||||
# explicit stop shutdown performs), dead for a day and a half before anyone
|
||||
# noticed. One owner per container name; no compose file remains anywhere.
|
||||
# Host metrics source for Prometheus. This replaces the compose service of the
|
||||
# same name in dankclaw/compose.yaml ("Observability" block).
|
||||
#
|
||||
# HOST NAMESPACE. It must run in the host network namespace to report the
|
||||
# host's own interfaces/connections, and --pid=host for the processes and
|
||||
# cgroups collectors. "localhost" inside the container is therefore the host's
|
||||
# loopback, which is what the homepage status timer probes.
|
||||
#
|
||||
# LISTEN ADDRESSES. Exactly two: loopback for the homepage status probe, and
|
||||
# the LAN address, because Prometheus — which runs in a container on
|
||||
# dankclaw_default — scrapes http://192.168.0.143:9100/metrics
|
||||
# (shared/prometheus/prometheus.yml, job "node-dankclaw"). node_exporter has no
|
||||
# authentication, so the wildcard bind stays off the table; this is the
|
||||
# pairing that removes the unauthenticated observability surface on every
|
||||
# host, and both consumers are accounted for.
|
||||
# LISTEN ADDRESSES. Until 2026-09-11 this bound :9100 on every interface. It
|
||||
# now binds exactly two: loopback for the homepage status probe, and the LAN
|
||||
# address because Prometheus — which runs in a container on dankclaw_default —
|
||||
# scrapes http://192.168.0.143:9100 (shared/prometheus/prometheus.yml,
|
||||
# job "node-dankclaw"). node_exporter itself has no authentication, so keeping
|
||||
# the wildcard bind off the table matters: this is the second half of the
|
||||
# pairing that removes the unauthenticated observability surface, and both
|
||||
# consumers are accounted for. Dankserver runs its own copy of this container
|
||||
# (scraped by the "node-dankserver" job) — since 2026-09-16 as a Quadlet unit in
|
||||
# the same shape (dankserver/observability/quadlet/), because the compose-only
|
||||
# version it replaced had been dead for a day and a half before anyone noticed.
|
||||
#
|
||||
# MOUNTS. The container reads the host's /proc, /sys and / as /host/*
|
||||
# read-only and is pointed at them with --path.*; it writes nothing anywhere.
|
||||
# MOUNTS. The container reads the host's /proc, /sys and / as /host/* read-only
|
||||
# and is pointed at them with --path.*; it writes nothing anywhere.
|
||||
|
||||
[Unit]
|
||||
Description=Prometheus node exporter (Dankclaw host metrics)
|
||||
|
||||
+34
-30
@@ -1,43 +1,47 @@
|
||||
# A records for the flat franksdank.cloud scheme (dnsmasq addn-hosts
|
||||
# format: <IP> <name>). RENDERED — do not edit by hand:
|
||||
# A records for the flat franksdank.name scheme (dnsmasq addn-hosts format:
|
||||
# <IP> <name> [<name> ...]). Served by the Pi-hole quadlet (FTL is a
|
||||
# dnsmasq fork): the quadlet mounts this file into /etc/pihole/hosts/ read-only.
|
||||
#
|
||||
# scripts/service-map.py render pihole-hosts > dankclaw/pihole/addn-hosts
|
||||
# Caddy is consolidated on dankclaw (192.168.0.143): it fronts every service
|
||||
# on both hosts, so every name resolves to that one IP. Keep in sync with the
|
||||
# Caddyfile (tests/pihole.bats cross-checks this).
|
||||
#
|
||||
# The source of truth is the `dns:` field of shared/services.yml; the
|
||||
# drift gate in tests/service_inventory.bats fails when this file and
|
||||
# the inventory disagree. Caddy is consolidated on Dankclaw, so every
|
||||
# name — including the services running on dankserver and
|
||||
# frankbook-pro — resolves at the Dankclaw edge.
|
||||
#
|
||||
# dnsmasq strips ONLY whole-line comments in addn-hosts: a trailing
|
||||
# comment on a data line becomes a HOSTNAME token. No inline notes
|
||||
# here; the service attribution lives in the inventory.
|
||||
# 192.168.0.143 = dankclaw (runs the Caddy container + the AI/dev stack)
|
||||
# 192.168.0.235 = dankserver (media stack; reached by Caddy via published
|
||||
# LAN ports — no DNS records point here)
|
||||
|
||||
192.168.0.143 search.franksdank.cloud
|
||||
192.168.0.143 marreta.franksdank.cloud
|
||||
192.168.0.143 pihole.franksdank.cloud
|
||||
192.168.0.143 ai.franksdank.cloud
|
||||
192.168.0.143 gitea.franksdank.cloud
|
||||
192.168.0.143 comfyui.franksdank.cloud
|
||||
192.168.0.143 seerr.franksdank.cloud
|
||||
192.168.0.143 hermes.franksdank.cloud
|
||||
192.168.0.143 hermesadmin.franksdank.cloud
|
||||
192.168.0.143 prometheus.franksdank.cloud
|
||||
192.168.0.143 grafana.franksdank.cloud
|
||||
192.168.0.143 loki.franksdank.cloud
|
||||
192.168.0.143 auth.franksdank.cloud
|
||||
# --- Homepage (naked domain + www) + legacy names ---
|
||||
192.168.0.143 franksdank.cloud
|
||||
192.168.0.143 www.franksdank.cloud
|
||||
192.168.0.143 dankclaw.franksdank.cloud
|
||||
192.168.0.143 dankserver.franksdank.cloud
|
||||
|
||||
# --- Authentik (SSO / OIDC provider) ---
|
||||
192.168.0.143 auth.franksdank.cloud
|
||||
|
||||
# --- Media services (dankserver, proxied) ---
|
||||
192.168.0.143 plex.franksdank.cloud
|
||||
192.168.0.143 plex.franksdank.casa
|
||||
192.168.0.143 books.franksdank.cloud
|
||||
192.168.0.143 romm.franksdank.cloud
|
||||
192.168.0.143 files.franksdank.cloud
|
||||
192.168.0.143 jelly.franksdank.cloud
|
||||
192.168.0.143 books.franksdank.cloud
|
||||
192.168.0.143 plex.franksdank.cloud
|
||||
192.168.0.143 plex.franksdank.casa
|
||||
192.168.0.143 prowlarr.franksdank.cloud
|
||||
192.168.0.143 qbittorrent.franksdank.cloud
|
||||
192.168.0.143 radarr.franksdank.cloud
|
||||
192.168.0.143 lidarr.franksdank.cloud
|
||||
192.168.0.143 readarr.franksdank.cloud
|
||||
192.168.0.143 prowlarr.franksdank.cloud
|
||||
192.168.0.143 qbittorrent.franksdank.cloud
|
||||
|
||||
# --- AI / search / dev services (dankclaw, local) ---
|
||||
192.168.0.143 search.franksdank.cloud
|
||||
192.168.0.143 comfyui.franksdank.cloud
|
||||
192.168.0.143 ai.franksdank.cloud
|
||||
192.168.0.143 hermes.franksdank.cloud
|
||||
192.168.0.143 hermesadmin.franksdank.cloud
|
||||
192.168.0.143 gitea.franksdank.cloud
|
||||
192.168.0.143 grafana.franksdank.cloud
|
||||
192.168.0.143 prometheus.franksdank.cloud
|
||||
192.168.0.143 loki.franksdank.cloud
|
||||
192.168.0.143 pihole.franksdank.cloud
|
||||
192.168.0.143 seerr.franksdank.cloud
|
||||
192.168.0.143 marreta.franksdank.cloud
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
# Quadlet unit: node-exporter (rootless, boot-persistent) — RENDERED ARTIFACT.
|
||||
# Regenerate: python3 scripts/service-map.py render node-exporter-unit --host dankserver
|
||||
# Source: shared/services.yml (entry node-exporter) + this template.
|
||||
# Quadlet unit: node-exporter (rootless, boot-persistent) — DANKSERVER.
|
||||
#
|
||||
# Install path on dankserver: ~/.config/containers/systemd/node-exporter.container
|
||||
# Unit name: node-exporter.service (container name: node-exporter)
|
||||
#
|
||||
# Host metrics source for Prometheus. Replaces the compose service of the same
|
||||
# name (the "Observability" block): Dankclaw moved to this quadlet on
|
||||
# 2026-09-11, Dankserver on 2026-09-16 — its compose-only copy died at a reboot
|
||||
# and stayed dead (rootless Podman does not restart bare containers after the
|
||||
# explicit stop shutdown performs), dead for a day and a half before anyone
|
||||
# noticed. One owner per container name; no compose file remains anywhere.
|
||||
# WHY THIS FILE EXISTS. Until 2026-09-16 the Dankserver node-exporter ran only
|
||||
# from `dankserver/compose.yaml` (the "Observability" block, `restart:
|
||||
# unless-stopped`). Rootless Podman does not restart bare containers after the
|
||||
# explicit stop that shutdown performs, so it died at a reboot and stayed dead:
|
||||
# `podman ps -a` showed it `Exited (143)` and Prometheus' `node-dankserver` job
|
||||
# had been scraping nothing since. The compose entry also bound the default
|
||||
# wildcard address, which is the unauthenticated-metrics-API-on-every-interface
|
||||
# posture that was already removed on Dankclaw. This unit replaces both: one
|
||||
# owner per container name (the compose service is deleted), explicit listen
|
||||
# addresses, and a generator-provided boot path.
|
||||
#
|
||||
# HOST NAMESPACE. It must run in the host network namespace to report the
|
||||
# host's own interfaces/connections, and --pid=host for the processes and
|
||||
# cgroups collectors. "localhost" inside the container is therefore the host's
|
||||
# loopback, which is what the homepage status timer probes.
|
||||
# HOST NAMESPACE. Networking and PID in the host's namespaces: the collectors
|
||||
# report the host's own interfaces, connections, processes and cgroups. The
|
||||
# host's /proc, /sys and / are mounted read-only under /host and /rootfs, and
|
||||
# node_exporter writes nothing.
|
||||
#
|
||||
# LISTEN ADDRESSES. Exactly two: loopback for the homepage status probe, and
|
||||
# the LAN address, because Prometheus — which runs in a container on
|
||||
# dankclaw_default — scrapes http://192.168.0.235:9100/metrics
|
||||
# (shared/prometheus/prometheus.yml, job "node-dankserver"). node_exporter has no
|
||||
# authentication, so the wildcard bind stays off the table; this is the
|
||||
# pairing that removes the unauthenticated observability surface on every
|
||||
# host, and both consumers are accounted for.
|
||||
# LISTEN ADDRESSES. Exactly two:
|
||||
# - 127.0.0.1:9100 for host-local probes
|
||||
# - 192.168.0.235:9100 for Prometheus, which scrapes this host from Dankclaw
|
||||
# (shared/prometheus/prometheus.yml, job "node-dankserver"). It cannot use
|
||||
# loopback across hosts, and node_exporter has no authentication, so the
|
||||
# wildcard bind stays off the table. This is the same deliberate LAN
|
||||
# listener posture the observability runbook documents.
|
||||
#
|
||||
# MOUNTS. The container reads the host's /proc, /sys and / as /host/*
|
||||
# read-only and is pointed at them with --path.*; it writes nothing anywhere.
|
||||
# MIRROR. Keep this file in step with dankclaw/observability/quadlet/
|
||||
# node-exporter.container; only the Description and the LAN address differ.
|
||||
|
||||
[Unit]
|
||||
Description=Prometheus node exporter (Dankserver host metrics)
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the z-lab Qwen3.5-9B-DFlash drafter for frank-dragoon's DFlash
|
||||
# Download the z-lab Qwen3.5-9B-DFlash drafter for frank-large's DFlash
|
||||
# speculative-decode arm (vLLM --speculative-config method=dflash). Public
|
||||
# repo — no auth header on the command line.
|
||||
#
|
||||
@@ -10,7 +10,7 @@
|
||||
# the draft for that target.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-dragoon-dflash.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-large-dflash.sh [DEST_DIR] [--verify-only]
|
||||
set -euo pipefail
|
||||
|
||||
REPO="z-lab/Qwen3.5-9B-DFlash"
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the BF16 MiMo-V2.6-Distill-Qwen-9B payload for frank-dragoon
|
||||
# Download the BF16 MiMo-V2.6-Distill-Qwen-9B payload for frank-large
|
||||
# (vLLM/kyuz0 gfx1151, Strix Halo iGPU). Public repo — no auth header on the
|
||||
# command line (a Bearer token in argv is world-readable via /proc).
|
||||
#
|
||||
# Per-file FRESH downloads, no resume: the house rule (see
|
||||
# download-frank-zealot.sh); -f/--fail-with-body plus size+sha256 against the
|
||||
# download-frank-secondary.sh); -f/--fail-with-body plus size+sha256 against the
|
||||
# Hub LFS oid is what makes an access error fail instead of "succeeding".
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-dragoon-mimo.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-large-mimo.sh [DEST_DIR] [--verify-only]
|
||||
#
|
||||
# The verified set below is exactly what the 2026-10-01 bench window served:
|
||||
# 17.53 GiB of BF16 weights (4 shards, 18,819,720,848 B total with the rest).
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the source Qwen3.5-9B checkpoint — the ONLY place this model
|
||||
# family's MTP head (15 mtp.* tensors) exists. frank-dragoon runs it as the
|
||||
# family's MTP head (15 mtp.* tensors) exists. frank-large runs it as the
|
||||
# vLLM cross-model MTP draft (MiMo-V2.6-Distill-Qwen-9B itself ships zero
|
||||
# mtp tensors; that is why SGLang's NEXTN on frank-zealot draws 0.00
|
||||
# mtp tensors; that is why SGLang's NEXTN on frank-secondary draws 0.00
|
||||
# acceptance). Public repo — no auth header on the command line.
|
||||
#
|
||||
# NOTE the odd upstream shard names (model.safetensors-0000N-of-00004...).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-dragoon-qwen35.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-large-qwen35.sh [DEST_DIR] [--verify-only]
|
||||
set -euo pipefail
|
||||
|
||||
REPO="Qwen/Qwen3.5-9B"
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# download-frank-archon-fp8.sh — fetch/verify the Qwen3.8-Flash-Next-FP8
|
||||
# download-frank-primary-fp8.sh — fetch/verify the Qwen3.8-Flash-Next-FP8
|
||||
# payload (131 safetensors shards, 172.78 GiB total, plus aux files) for the
|
||||
# vLLM frank-archon deployment on dankserver.
|
||||
# vLLM frank-primary deployment on dankserver.
|
||||
#
|
||||
# bash scripts/download-frank-archon-fp8.sh [dest_dir]
|
||||
# bash scripts/download-frank-primary-fp8.sh [dest_dir]
|
||||
#
|
||||
# The manifest (scripts/manifests/qwen3.8-flash-next-fp8.manifest) carries
|
||||
# per-file sizes + LFS sha256s from the Hub; hf-fetch.sh is idempotent — an
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound for frank-zealot
|
||||
# Download MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound for frank-secondary
|
||||
# (SGLang, RTX 3090). Public repo — no auth header on the command line.
|
||||
#
|
||||
# Per-file FRESH downloads, no resume: `curl -C -` once produced a 213 MB
|
||||
@@ -7,7 +7,7 @@
|
||||
# the house rule.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-zealot.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-secondary.sh [DEST_DIR] [--verify-only]
|
||||
#
|
||||
# LFS shards are verified against the Hub LFS sha256; non-LFS text files
|
||||
# against their declared byte size. Idempotent: complete files are skipped.
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# bench-frank-dragoon.sh — 64k-context benchmark for the frank-default-large stack.
|
||||
# bench-frank-large.sh — 64k-context benchmark for the frank-default-large stack.
|
||||
#
|
||||
# Measures generation throughput of Qwen3.6-27B Q8_K_P + DFlash + ngram-mod on
|
||||
# a single backend at 64k context. It starts llama-server on a scratch port,
|
||||
@@ -7,15 +7,15 @@
|
||||
# prompt, and prints tokens/sec.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/bench-frank-dragoon.sh <ROCm0|Vulkan0> [--generations N]
|
||||
# ./scripts/bench-frank-large.sh <ROCm0|Vulkan0> [--generations N]
|
||||
#
|
||||
# Example:
|
||||
# ./scripts/bench-frank-dragoon.sh ROCm0
|
||||
# ./scripts/bench-frank-dragoon.sh Vulkan0 --generations 3
|
||||
# ./scripts/bench-frank-large.sh ROCm0
|
||||
# ./scripts/bench-frank-large.sh Vulkan0 --generations 3
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEVICE="${1:?usage: bench-frank-dragoon.sh <ROCm0|Vulkan0> [--generations N]}"
|
||||
DEVICE="${1:?usage: bench-frank-large.sh <ROCm0|Vulkan0> [--generations N]}"
|
||||
GENERATIONS=1
|
||||
if [ "${2:-}" = "--generations" ]; then
|
||||
GENERATIONS="${3:?--generations needs a count}"
|
||||
@@ -41,7 +41,7 @@ SERVICE_HEALTH[dankclaw]="http://127.0.0.1:7770/health"
|
||||
# Drop page caches before restart: with the model's pages fully cached, the
|
||||
# KV/graph allocation stalls at load (VRAM stuck at 28 GiB). See AGENTS.md.
|
||||
DROP_CACHES[dankclaw]=1
|
||||
# Passwordless restart helper (root-owned, installed via /etc/sudoers.d/frank-dragoon).
|
||||
# Passwordless restart helper (root-owned, installed via /etc/sudoers.d/frank-large).
|
||||
# When present, deploy.sh uses it instead of inline sudo commands.
|
||||
HELPER[dankclaw]="/usr/local/sbin/frank-restart-large.sh"
|
||||
# Health wait in iterations of 2s. Cold loads after the cache drop measured
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tunables for the Ling-3.0-flash TRAIL on dankclaw (frank-dragoon-ling).
|
||||
# Tunables for the Ling-3.0-flash TRAIL on dankclaw (frank-large-ling).
|
||||
# This REPLACES frank-default-large (Qwen3.6-27B) temporarily. To switch
|
||||
# back: systemctl stop frank-dragoon-ling && systemctl start frank-default-large.
|
||||
# back: systemctl stop frank-large-ling && systemctl start frank-default-large.
|
||||
# Runtime: llama.cpp FORK bailingmoe3-support; Vulkan + AMD GTT.
|
||||
#
|
||||
# Memory model (amdgpu Strix Halo gfx1151, 8x16GB LPDDR5 = 128 GiB physical):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=llama.cpp Ling-3.0-flash Q6_K Vulkan+MTP (frank-dragoon-ling TRIAL)
|
||||
Description=llama.cpp Ling-3.0-flash Q6_K Vulkan+MTP (frank-large-ling TRIAL)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
@@ -8,7 +8,7 @@
|
||||
# Vulkan exposes as an ~111 GiB heap). MTP drafter is bundled in the same
|
||||
# GGUF (block 42) - no separate --spec-draft-model needed.
|
||||
#
|
||||
# Tie-back: to restore the Qwen3.6-27B service, stop frank-dragoon-ling and
|
||||
# Tie-back: to restore the Qwen3.6-27B service, stop frank-large-ling and
|
||||
# start frank-default-large (its env/unit are committed and unchanged).
|
||||
#
|
||||
# Tunables come from frank-default-large.ling.env in this directory unless
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
# Runtime: llama.cpp llama-server (>= b10411).
|
||||
#
|
||||
# Tunables come from frank-default-large.env in this directory unless the
|
||||
# FRANK_DRAGOON_ENV_FILE environment variable overrides that path.
|
||||
# FRANK_LARGE_ENV_FILE environment variable overrides that path.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-default-large.env}"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-default-large.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Decode throughput benchmark for frank-dragoon.
|
||||
Decode throughput benchmark for frank-large.
|
||||
Starts the server with a given --threads-batch value, sends an 8K+ token prompt,
|
||||
generates 256 tokens, measures generation tok/s.
|
||||
|
||||
@@ -13,12 +13,12 @@ SERVE = "/home/dank/GitPackages/ROCmFPX/build-qwen38/bin/llama-server"
|
||||
PORT = 7770
|
||||
N_GEN = 256
|
||||
PROMPT_FILE = "/tmp/bench-prompts/prompt_code.txt"
|
||||
ENV_FILE = os.path.join(os.path.dirname(__file__), "frank-dragoon.env")
|
||||
ENV_FILE = os.path.join(os.path.dirname(__file__), "frank-large.env")
|
||||
SERVER_DIR = os.path.dirname(SERVE)
|
||||
ROCM_PATH = "/opt/rocm"
|
||||
|
||||
def make_env():
|
||||
"""Build the same env as serve-frank-dragoon.sh."""
|
||||
"""Build the same env as serve-frank-large.sh."""
|
||||
env = {**os.environ, "HSA_OVERRIDE_GFX_VERSION": "11.5.1", "HIP_VISIBLE_DEVICES": "0"}
|
||||
|
||||
# Source env file for tunables
|
||||
@@ -59,7 +59,7 @@ def wait_ready():
|
||||
def send_request(prompt, max_tokens=N_GEN):
|
||||
"""Send chat completion, measure generation throughput."""
|
||||
body = json.dumps({
|
||||
"model": "frank-dragoon",
|
||||
"model": "frank-large",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Benchmark n-gram speculative decoding vs no-spec for frank-dragoon.
|
||||
Benchmark n-gram speculative decoding vs no-spec for frank-large.
|
||||
|
||||
Tests prompt-processing throughput with 16K token prompts (manageable request size).
|
||||
Code-heavy prompt (high n-gram redundancy) vs prose prompt (low redundancy).
|
||||
@@ -103,7 +103,7 @@ def start_server(port, spec_type):
|
||||
def run_test(prompt_text, spec_type, port):
|
||||
"""Send a request and extract prompt processing metrics."""
|
||||
data = json.dumps({
|
||||
"model": "frank-dragoon",
|
||||
"model": "frank-large",
|
||||
"messages": [{"role": "user", "content": prompt_text}],
|
||||
"max_tokens": TEST_TOKENS,
|
||||
"stream": False,
|
||||
@@ -189,7 +189,7 @@ def main():
|
||||
print(f" Prose: {prose_prompt_file.stat().st_size:,} bytes")
|
||||
|
||||
print("=" * 60)
|
||||
print(" N-gram Mod Benchmark — frank-dragoon")
|
||||
print(" N-gram Mod Benchmark — frank-large")
|
||||
print(f" STRIX ROCmFP4 · 64K context · 16K prompts")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Benchmark n-gram speculative decoding vs no-spec for frank-dragoon.
|
||||
# Benchmark n-gram speculative decoding vs no-spec for frank-large.
|
||||
#
|
||||
# This tests prompt-processing throughput at 64k context with two distinct
|
||||
# prompts: one code-heavy and one prose-heavy. Uses llama-server on scratch
|
||||
# ports with the OpenAI-compatible API to measure prompt_eval_duration_ms.
|
||||
#
|
||||
# Run on Dankclaw after stopping the production service:
|
||||
# sudo systemctl stop frank-dragoon
|
||||
# sudo systemctl stop frank-large
|
||||
# ./scripts/bench-ngram-mod.sh
|
||||
# sudo systemctl start frank-dragoon
|
||||
# sudo systemctl start frank-large
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-large.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a; source "$ENV_FILE"; set +a
|
||||
fi
|
||||
@@ -184,7 +184,7 @@ run_single_test() {
|
||||
response=$(curl -s -X POST "http://127.0.0.1:$port/v1/completions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"model\": \"frank-dragoon\",
|
||||
\"model\": \"frank-large\",
|
||||
\"prompt\": \"$prompt_text\",
|
||||
\"max_tokens\": 128,
|
||||
\"temperature\": 0,
|
||||
@@ -220,7 +220,7 @@ run_single_test() {
|
||||
}
|
||||
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ N-gram Mod Benchmark — frank-dragoon ║"
|
||||
echo "║ N-gram Mod Benchmark — frank-large ║"
|
||||
echo "║ STRIX ROCmFP4 · 64k context · two prompts ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
@@ -15,9 +15,9 @@ import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
SERVICE = "frank-dragoon"
|
||||
ENV_FILE = "/home/dank/serverAppConfigs/scripts/frank-dragoon.env"
|
||||
ALIAS = "frank-dragoon"
|
||||
SERVICE = "frank-large"
|
||||
ENV_FILE = "/home/dank/serverAppConfigs/scripts/frank-large.env"
|
||||
ALIAS = "frank-large"
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 7770
|
||||
DEFAULT_MAX_TOKENS = 16384
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Tunables for frank-dragoon (mradermacher i1-Q4_K_M of
|
||||
# Tunables for frank-large (mradermacher i1-Q4_K_M of
|
||||
# orcarouter/Qwen3.8-Flash-Next-Uncensored on ik_llama.cpp, Dankclaw,
|
||||
# RTX 3090 CUDA + Strix Halo CPU path, since 2026-09-15).
|
||||
#
|
||||
@@ -65,7 +65,7 @@ MMPROJ_PATH=/home/dank/models/mradermacher-Qwen3.8-Flash-Next-Uncensored-i1-Q4_K
|
||||
CHAT_TEMPLATE_FILE=/home/dank/serverAppConfigs/scripts/chat-template.jinja
|
||||
|
||||
# Alias + network.
|
||||
ALIAS=frank-dragoon
|
||||
ALIAS=frank-large
|
||||
BIND_ADDR=0.0.0.0
|
||||
LISTEN_PORT=7770
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=ik_llama.cpp Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M 177B MoE (dankclaw, frank-dragoon)
|
||||
Description=ik_llama.cpp Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M 177B MoE (dankclaw, frank-large)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -12,8 +12,8 @@ Group=dank
|
||||
# budget still needs room. Parity with the halogen quadlet's
|
||||
# Ulimit=memlock=-1:-1.
|
||||
LimitMEMLOCK=infinity
|
||||
Environment="FRANK_DRAGOON_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-dragoon.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-dragoon.sh
|
||||
Environment="FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Serve mradermacher/Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M as frank-dragoon
|
||||
# Serve mradermacher/Qwen3.8-Flash-Next-Uncensored i1-Q4_K_M as frank-large
|
||||
# via ik_llama.cpp on Dankclaw.
|
||||
#
|
||||
# Topology (ikawrakow's hybrid recipe; upstream issue #2402 for the PLE):
|
||||
@@ -26,7 +26,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-large.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
@@ -66,7 +66,7 @@ fi
|
||||
# CPU-overridden weights mmapped (src/llama-load-tensors.cpp); the env file
|
||||
# sets it. Refuse to run without it.
|
||||
if [[ "${GGML_CUDA_NO_PINNED_WEIGHTS:-}" != "1" ]]; then
|
||||
echo "frank-dragoon: refusing to start — GGML_CUDA_NO_PINNED_WEIGHTS=1 is mandatory on this host" >&2
|
||||
echo "frank-large: refusing to start — GGML_CUDA_NO_PINNED_WEIGHTS=1 is mandatory on this host" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -112,7 +112,7 @@ fi
|
||||
: "${SPEC_DRAFT_POLL_BATCH:=1}"
|
||||
|
||||
# Text only: the server must not try to autoload the vision tower. Vision
|
||||
# stays on frank-dragoon (Dankclaw), which is where the fleet routes images.
|
||||
# stays on frank-large (Dankclaw), which is where the fleet routes images.
|
||||
NO_MMPROJ="${NO_MMPROJ:-1}"
|
||||
|
||||
export HIP_VISIBLE_DEVICES
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""autoresearch-bench.py — deterministic greedy battery for frank-dragoon.
|
||||
"""autoresearch-bench.py — deterministic greedy battery for frank-large.
|
||||
|
||||
Runs ON Dankclaw (no LAN jitter) against the candidate container on :7770.
|
||||
Fixed embedded corpus, temperature 0, streaming so decode speed is measured
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# autoresearch-frank-archon.sh — RUNS ON DANKSERVER, driven over ssh by the
|
||||
# dev-box autoresearch.sh. It is the canonical frank-archon benchmark entry
|
||||
# autoresearch-frank-primary.sh — RUNS ON DANKSERVER, driven over ssh by the
|
||||
# dev-box autoresearch.sh. It is the canonical frank-primary benchmark entry
|
||||
# point for the autoresearch loop.
|
||||
#
|
||||
# Method (mandated by the Benchmarking Policy): the production service serves
|
||||
# real users and shares the two R9700s, so a bench beside it starves the GPUs
|
||||
# and corrupts results. This script therefore:
|
||||
# 1. stops the production frank-archon unit (root, relink symlink: stop,
|
||||
# 1. stops the production frank-primary unit (root, relink symlink: stop,
|
||||
# NOT mask — mask refuses to replace a symlinked unit file),
|
||||
# 2. boots a scratch radiance container named `arbench` on the SAME two
|
||||
# cards with the candidate engine argv (the env file the loop edits),
|
||||
@@ -18,7 +18,7 @@
|
||||
# prefill and decode rates for that single request.
|
||||
# 5. prints METRIC lines and tears the scratch down,
|
||||
# 6. restores production (systemctl start), via a trap so a failed bench
|
||||
# never leaves frank-archon down (the stale-probe-tail lesson).
|
||||
# never leaves frank-primary down (the stale-probe-tail lesson).
|
||||
#
|
||||
# Determinism: the prompt is built from a fixed, sorted corpus of repo files
|
||||
# with a fixed random seed, so prompt_n is stable across runs. Sampling is
|
||||
@@ -32,10 +32,10 @@
|
||||
# /dev/dri, the .rad ro, the state dir rw, the tune cache mount. The ONLY
|
||||
# differences: the container name (arbench, so prod's identity is untouched)
|
||||
# and a FRESH prefix-cache disk tier (arbench/, wiped per run) so the cold
|
||||
# prefill is cold — the prod tier at kvcache/frank-archon is left intact.
|
||||
# prefill is cold — the prod tier at kvcache/frank-primary is left intact.
|
||||
set -uo pipefail
|
||||
|
||||
ENV_FILE="${1:-/home/dank/serverAppConfigs/scripts/frank-archon.env}"
|
||||
ENV_FILE="${1:-/home/dank/serverAppConfigs/scripts/frank-primary.env}"
|
||||
SCRATCH=arbench
|
||||
PORT=7770
|
||||
HEALTH="http://127.0.0.1:$PORT/health"
|
||||
@@ -45,7 +45,7 @@ LOG=/home/dank/logs/arbench.log
|
||||
|
||||
restore() {
|
||||
podman rm -f "$SCRATCH" >/dev/null 2>&1 || true
|
||||
sudo systemctl start frank-archon >/dev/null 2>&1 || true
|
||||
sudo systemctl start frank-primary >/dev/null 2>&1 || true
|
||||
}
|
||||
trap restore EXIT
|
||||
|
||||
@@ -57,13 +57,13 @@ fi
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
|
||||
# --- stop production; the scratch needs both cards to itself ----------------
|
||||
sudo systemctl stop frank-archon >/dev/null 2>&1 || true
|
||||
sudo systemctl stop frank-primary >/dev/null 2>&1 || true
|
||||
podman rm -f "$SCRATCH" >/dev/null 2>&1 || true
|
||||
|
||||
# --- fresh prefix-cache tier for a cold prefill (prod's tier untouched) ------
|
||||
FRESH_KV="$FRANK_ARCHON_STATE_DIR/kvcache/$SCRATCH"
|
||||
FRESH_KV="$FRANK_PRIMARY_STATE_DIR/kvcache/$SCRATCH"
|
||||
rm -rf "$FRESH_KV"; mkdir -p "$FRESH_KV"
|
||||
mkdir -p "$FRANK_ARCHON_STATE_DIR/tune"
|
||||
mkdir -p "$FRANK_PRIMARY_STATE_DIR/tune"
|
||||
|
||||
# --- boot the scratch: the production run line, scratch name + fresh KV ------
|
||||
podman run -d --name "$SCRATCH" \
|
||||
@@ -72,31 +72,31 @@ podman run -d --name "$SCRATCH" \
|
||||
--security-opt seccomp=unconfined \
|
||||
--ulimit memlock=-1:-1 \
|
||||
--device /dev/kfd --device /dev/dri \
|
||||
-v "$FRANK_ARCHON_MODEL_DIR":/models:ro \
|
||||
-v "$FRANK_ARCHON_STATE_DIR":/data \
|
||||
-v "$FRANK_ARCHON_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
||||
"$FRANK_ARCHON_IMAGE" \
|
||||
--model "/models/$FRANK_ARCHON_MODEL_FILE" \
|
||||
-v "$FRANK_PRIMARY_MODEL_DIR":/models:ro \
|
||||
-v "$FRANK_PRIMARY_STATE_DIR":/data \
|
||||
-v "$FRANK_PRIMARY_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||
-v "$FRANK_PRIMARY_TEMPLATE":/templates/template.jinja:ro \
|
||||
"$FRANK_PRIMARY_IMAGE" \
|
||||
--model "/models/$FRANK_PRIMARY_MODEL_FILE" \
|
||||
--host :: --port "$PORT" \
|
||||
--served-model-name "$FRANK_ARCHON_SERVED_NAME" \
|
||||
--served-model-name "$FRANK_PRIMARY_SERVED_NAME" \
|
||||
--override-chat-template /templates/template.jinja \
|
||||
--tp "$FRANK_ARCHON_TP" --tp-wire "$FRANK_ARCHON_TP_WIRE" \
|
||||
--max-num-seqs "$FRANK_ARCHON_MAX_SEQS" \
|
||||
--max-model-len "$FRANK_ARCHON_MAX_MODEL_LEN" \
|
||||
--placement "$FRANK_ARCHON_PLACEMENT" \
|
||||
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
||||
--gpu-headroom-mib "$FRANK_ARCHON_GPU_HEADROOM_MIB" \
|
||||
--vram-weights-mib "$FRANK_ARCHON_VRAM_WEIGHTS_MIB" \
|
||||
--vram-kv-mib "$FRANK_ARCHON_VRAM_KV_MIB" \
|
||||
--kv-cache-dtype "$FRANK_ARCHON_KV_DTYPE" \
|
||||
--prefix-cache-host-mib "$FRANK_ARCHON_PREFIX_HOST_MIB" \
|
||||
--tp "$FRANK_PRIMARY_TP" --tp-wire "$FRANK_PRIMARY_TP_WIRE" \
|
||||
--max-num-seqs "$FRANK_PRIMARY_MAX_SEQS" \
|
||||
--max-model-len "$FRANK_PRIMARY_MAX_MODEL_LEN" \
|
||||
--placement "$FRANK_PRIMARY_PLACEMENT" \
|
||||
--host-pool-mib "$FRANK_PRIMARY_HOST_POOL_MIB" \
|
||||
--gpu-headroom-mib "$FRANK_PRIMARY_GPU_HEADROOM_MIB" \
|
||||
--vram-weights-mib "$FRANK_PRIMARY_VRAM_WEIGHTS_MIB" \
|
||||
--vram-kv-mib "$FRANK_PRIMARY_VRAM_KV_MIB" \
|
||||
--kv-cache-dtype "$FRANK_PRIMARY_KV_DTYPE" \
|
||||
--prefix-cache-host-mib "$FRANK_PRIMARY_PREFIX_HOST_MIB" \
|
||||
--prefix-cache-dir "/data/kvcache/$SCRATCH" \
|
||||
--prefix-cache-disk-mib "$FRANK_ARCHON_PREFIX_DISK_MIB" \
|
||||
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
||||
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
||||
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
||||
${FRANK_ARCHON_EXTRA_ARGS:+$FRANK_ARCHON_EXTRA_ARGS} \
|
||||
--prefix-cache-disk-mib "$FRANK_PRIMARY_PREFIX_DISK_MIB" \
|
||||
--ngram-placement "$FRANK_PRIMARY_NGRAM_PLACEMENT" \
|
||||
--num-speculative-tokens "$FRANK_PRIMARY_SPEC_TOKENS" \
|
||||
--max-num-batched-tokens "$FRANK_PRIMARY_MAX_BATCHED_TOKENS" \
|
||||
${FRANK_PRIMARY_EXTRA_ARGS:+$FRANK_PRIMARY_EXTRA_ARGS} \
|
||||
> "$LOG" 2>&1
|
||||
if [ $? -ne 0 ]; then echo "scratch boot failed; see $LOG" >&2; exit 1; fi
|
||||
|
||||
+32
-32
@@ -1,19 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# frank-archon-arms.sh — run a SEQUENCE of frank-archon bench arms and keep
|
||||
# frank-primary-arms.sh — run a SEQUENCE of frank-primary bench arms and keep
|
||||
# every arm's evidence. Runs on the operator box; each arm is one full window
|
||||
# of ./autoresearch.sh (stop+mask -> boot the arm as the service -> measure ->
|
||||
# restore production and verify), so the fleet is only ever down for one arm at
|
||||
# a time and the production unit is back up between arms.
|
||||
#
|
||||
# An arm is the repo's own frank-archon.env plus an OVERLAY of engine knobs
|
||||
# An arm is the repo's own frank-primary.env plus an OVERLAY of engine knobs
|
||||
# appended after it (later assignment wins when the serve script sources it),
|
||||
# staged through FRANK_ARCHON_AR_ENV_FILE so the checkout is never mutated and
|
||||
# staged through FRANK_PRIMARY_AR_ENV_FILE so the checkout is never mutated and
|
||||
# each arm gets its own fingerprint (the memo can never answer one arm with
|
||||
# another arm's numbers).
|
||||
#
|
||||
# Usage: scripts/autoresearch/frank-archon-arms.sh [arm ...]
|
||||
# scripts/autoresearch/frank-archon-arms.sh --list
|
||||
# Output, under /tmp/frank-archon-arms-<UTC stamp>/ (override with ARMS_DIR):
|
||||
# Usage: scripts/autoresearch/frank-primary-arms.sh [arm ...]
|
||||
# scripts/autoresearch/frank-primary-arms.sh --list
|
||||
# Output, under /tmp/frank-primary-arms-<UTC stamp>/ (override with ARMS_DIR):
|
||||
#
|
||||
# <arm>.out full autoresearch output (metrics + engine evidence)
|
||||
# <arm>.env the exact staged env
|
||||
@@ -26,8 +26,8 @@ set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd -- "$SCRIPT_DIR/../.." && pwd)"
|
||||
BASE_ENV="$REPO/scripts/frank-archon.env"
|
||||
ARMS_DIR="${ARMS_DIR:-/tmp/frank-archon-arms-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||
BASE_ENV="$REPO/scripts/frank-primary.env"
|
||||
ARMS_DIR="${ARMS_DIR:-/tmp/frank-primary-arms-$(date -u +%Y%m%dT%H%M%SZ)}"
|
||||
|
||||
# --- arms ------------------------------------------------------------------
|
||||
# baseline: the shipped shape E, measured with THIS harness so the comparison is
|
||||
@@ -45,18 +45,18 @@ LADDER3='{"cudagraph_capture_sizes":[4,8,12],"max_cudagraph_capture_size":12,"in
|
||||
# the engine as {temperature:0.7}.
|
||||
GEN07='{"temperature":0.7}'
|
||||
OVER_B=(
|
||||
"FRANK_ARCHON_SPEC='$SPEC3'"
|
||||
"FRANK_ARCHON_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_ARCHON_GEN_CONFIG='"'$GEN07'"
|
||||
"FRANK_PRIMARY_SPEC='$SPEC3'"
|
||||
"FRANK_PRIMARY_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_PRIMARY_GEN_CONFIG='"'$GEN07'"
|
||||
)
|
||||
# B2: identical to B with the default (7.60 GiB) overhead, i.e. precise memory
|
||||
# OFF. It is the fallback if B dies at graph capture, and it prices the arena
|
||||
# that precise memory buys (7,522 slots -> ~6,496).
|
||||
OVER_B2=(
|
||||
"FRANK_ARCHON_SPEC='$SPEC3'"
|
||||
"FRANK_ARCHON_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_ARCHON_GEN_CONFIG='"'$GEN07'"
|
||||
'FRANK_ARCHON_PRECISE_MEMORY='
|
||||
"FRANK_PRIMARY_SPEC='$SPEC3'"
|
||||
"FRANK_PRIMARY_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_PRIMARY_GEN_CONFIG='"'$GEN07'"
|
||||
'FRANK_PRIMARY_PRECISE_MEMORY='
|
||||
)
|
||||
# C: the vendor's concurrency shape — SEQS 16 with the [4..32] ladder and util
|
||||
# 0.95. This is also the direct test of the theory that pinning the capture
|
||||
@@ -64,12 +64,12 @@ OVER_B2=(
|
||||
# the constant 3, which is NOT in the default ladder; 4 is).
|
||||
LADDER16='{"cudagraph_capture_sizes":[4,8,12,16,20,24,28,32],"max_cudagraph_capture_size":32,"inductor_compile_config":{"combo_kernels":false,"benchmark_combo_kernel":false}}'
|
||||
OVER_C=(
|
||||
"FRANK_ARCHON_SPEC='$SPEC3'"
|
||||
"FRANK_ARCHON_COMPCONFIG='$LADDER16'"
|
||||
'FRANK_ARCHON_GEN_CONFIG='"'$GEN07'"
|
||||
'FRANK_ARCHON_SEQS=16'
|
||||
'FRANK_ARCHON_MNBT=4096'
|
||||
'FRANK_ARCHON_UTIL=0.95'
|
||||
"FRANK_PRIMARY_SPEC='$SPEC3'"
|
||||
"FRANK_PRIMARY_COMPCONFIG='$LADDER16'"
|
||||
'FRANK_PRIMARY_GEN_CONFIG='"'$GEN07'"
|
||||
'FRANK_PRIMARY_SEQS=16'
|
||||
'FRANK_PRIMARY_MNBT=4096'
|
||||
'FRANK_PRIMARY_UTIL=0.95'
|
||||
)
|
||||
|
||||
# D/E: the CONTEXT arms. Measured 2026-10-02 (arms run 20261002T233855Z): the
|
||||
@@ -83,11 +83,11 @@ OVER_C=(
|
||||
# D is arm B with EXACTLY ONE variable changed (the KV bytes), so its result
|
||||
# says whether the 4k-depth collapse (B 23.09 vs baseline 33.99) was KV or the
|
||||
# batch shape.
|
||||
KV401K='FRANK_ARCHON_KV_BYTES=4050000000'
|
||||
KV401K='FRANK_PRIMARY_KV_BYTES=4050000000'
|
||||
OVER_D=(
|
||||
"FRANK_ARCHON_SPEC='$SPEC3'"
|
||||
"FRANK_ARCHON_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_ARCHON_GEN_CONFIG='"'$GEN07'"
|
||||
"FRANK_PRIMARY_SPEC='$SPEC3'"
|
||||
"FRANK_PRIMARY_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_PRIMARY_GEN_CONFIG='"'$GEN07'"
|
||||
"$KV401K"
|
||||
)
|
||||
# E: D with C's max_num_batched_tokens (4096 — C is the only arm that did NOT
|
||||
@@ -97,12 +97,12 @@ OVER_D=(
|
||||
# only 0.32 GiB above the allowance while the CUDA-graph cost is an ANALYTIC
|
||||
# estimate (C's was 1.47 GiB over 24 graphs), so it is measured, not assumed.
|
||||
OVER_E=(
|
||||
"FRANK_ARCHON_SPEC='$SPEC3'"
|
||||
"FRANK_ARCHON_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_ARCHON_GEN_CONFIG='"'$GEN07'"
|
||||
"FRANK_PRIMARY_SPEC='$SPEC3'"
|
||||
"FRANK_PRIMARY_COMPCONFIG='$LADDER3'"
|
||||
'FRANK_PRIMARY_GEN_CONFIG='"'$GEN07'"
|
||||
"$KV401K"
|
||||
'FRANK_ARCHON_MNBT=4096'
|
||||
'FRANK_ARCHON_UTIL=0.99'
|
||||
'FRANK_PRIMARY_MNBT=4096'
|
||||
'FRANK_PRIMARY_UTIL=0.99'
|
||||
)
|
||||
|
||||
arm_overlay() {
|
||||
@@ -142,7 +142,7 @@ for arm in "${ARMS[@]}"; do
|
||||
# holds JSON must still be one valid JSON object after the file is SOURCED
|
||||
# (that is the step that silently eats unquoted braces).
|
||||
if ! bash -c 'set -a; . "$1" >/dev/null; set +a
|
||||
for k in FRANK_ARCHON_SPEC FRANK_ARCHON_COMPCONFIG FRANK_ARCHON_GEN_CONFIG; do
|
||||
for k in FRANK_PRIMARY_SPEC FRANK_PRIMARY_COMPCONFIG FRANK_PRIMARY_GEN_CONFIG; do
|
||||
v="${!k:-}"
|
||||
[ -n "$v" ] || continue
|
||||
case "$v" in
|
||||
@@ -158,7 +158,7 @@ for arm in "${ARMS[@]}"; do
|
||||
fi
|
||||
|
||||
echo "[arms] === $arm: opening window $(date -u +%H:%M:%S)Z ===" >&2
|
||||
FRANK_ARCHON_AR_ENV_FILE="$ARMS_DIR/$arm.env" \
|
||||
FRANK_PRIMARY_AR_ENV_FILE="$ARMS_DIR/$arm.env" \
|
||||
"$REPO/autoresearch.sh" >"$ARMS_DIR/$arm.out" 2>&1
|
||||
rc=$?
|
||||
echo "$rc" >"$ARMS_DIR/$arm.rc"
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""frank-archon decode measurement — runs INSIDE the engine container.
|
||||
"""frank-primary decode measurement — runs INSIDE the engine container.
|
||||
|
||||
The method is the fleet's established one (the 2026-09-29 / 2026-10-01
|
||||
tuning campaigns, /home/dank/mtp_measure.py): SSE chunk counting is invalid
|
||||
@@ -28,7 +28,7 @@ Workload (fixed, deterministic, offline):
|
||||
|
||||
Env:
|
||||
AR_BASE endpoint (default http://127.0.0.1:7770)
|
||||
AR_MODEL served name (default frank-archon)
|
||||
AR_MODEL served name (default frank-primary)
|
||||
AR_PROMPT_CEILING hard prompt ceiling in tokens (default 100000)
|
||||
AR_TEMPS comma-separated sampling temperatures (default "0,0.7")
|
||||
|
||||
+14
-14
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# frank-archon-window.sh — one full bench window for frank-archon, run ON
|
||||
# frank-primary-window.sh — one full bench window for frank-primary, run ON
|
||||
# DANKSERVER as the deploy user (invoked over ssh by ./autoresearch.sh).
|
||||
#
|
||||
# Discipline (AGENTS.md Benchmarking Policy): the production unit is
|
||||
@@ -12,9 +12,9 @@
|
||||
#
|
||||
# Staging layout (created by the orchestrator, ephemeral — never a config
|
||||
# source; the repo checkout is):
|
||||
# /home/dank/.autoresearch-frank-archon/
|
||||
# /home/dank/.autoresearch-frank-primary/
|
||||
# serve.sh staged candidate serve script
|
||||
# frank-archon.env staged candidate env (the candidate knobs)
|
||||
# frank-primary.env staged candidate env (the candidate knobs)
|
||||
# measure.py the in-container measure client
|
||||
# hash candidate fingerprint
|
||||
# run.json result (metrics + boot + kv + fingerprint)
|
||||
@@ -27,16 +27,16 @@
|
||||
# outcome; a failed restore forces exit 1).
|
||||
set -uo pipefail
|
||||
|
||||
STAGE="${FRANK_ARCHON_AR_STAGE:-/home/dank/.autoresearch-frank-archon}"
|
||||
UNIT="${FRANK_ARCHON_AR_UNIT:-frank-archon}"
|
||||
CTR="${FRANK_ARCHON_AR_CTR:-frank-archon-qwen}"
|
||||
STAGE="${FRANK_PRIMARY_AR_STAGE:-/home/dank/.autoresearch-frank-primary}"
|
||||
UNIT="${FRANK_PRIMARY_AR_UNIT:-frank-primary}"
|
||||
CTR="${FRANK_PRIMARY_AR_CTR:-frank-primary-qwen}"
|
||||
HEALTH_URL="http://127.0.0.1:7770/health"
|
||||
MODELS_URL="http://127.0.0.1:7770/v1/models"
|
||||
BOOT_TIMEOUT="${FRANK_ARCHON_AR_BOOT_TIMEOUT:-1200}"
|
||||
RESTORE_TIMEOUT="${FRANK_ARCHON_AR_RESTORE_TIMEOUT:-1200}"
|
||||
MEASURE_TIMEOUT="${FRANK_ARCHON_AR_MEASURE_TIMEOUT:-1800}"
|
||||
SERVED="$(sed -n 's/^FRANK_ARCHON_SERVED_NAME=//p' "$STAGE/frank-archon.env" 2>/dev/null | head -1)"
|
||||
SERVED="${SERVED:-frank-archon}"
|
||||
BOOT_TIMEOUT="${FRANK_PRIMARY_AR_BOOT_TIMEOUT:-1200}"
|
||||
RESTORE_TIMEOUT="${FRANK_PRIMARY_AR_RESTORE_TIMEOUT:-1200}"
|
||||
MEASURE_TIMEOUT="${FRANK_PRIMARY_AR_MEASURE_TIMEOUT:-1800}"
|
||||
SERVED="$(sed -n 's/^FRANK_PRIMARY_SERVED_NAME=//p' "$STAGE/frank-primary.env" 2>/dev/null | head -1)"
|
||||
SERVED="${SERVED:-frank-primary}"
|
||||
|
||||
RESTORED=0
|
||||
|
||||
@@ -72,7 +72,7 @@ capture_bench_failure() {
|
||||
echo "=== staged serve.out (tail 60) ==="
|
||||
tail -60 "$STAGE/serve.out" 2>/dev/null || true
|
||||
echo "=== engine log (tail 60) ==="
|
||||
tail -60 /home/dank/logs/frank_archon.log 2>/dev/null || true
|
||||
tail -60 /home/dank/logs/frank_primary.log 2>/dev/null || true
|
||||
} >"$STAGE/fail.log" 2>&1 || true
|
||||
grep -E 'OutOfMemory|Tried to allocate|Engine core initialization failed|Error|error' \
|
||||
"$STAGE/fail.log" 2>/dev/null | tail -25 >&2 || true
|
||||
@@ -114,7 +114,7 @@ trap 'restore; rc=$?; [ "$rc" -ne 0 ] && exit 1' EXIT
|
||||
|
||||
main() {
|
||||
local f
|
||||
for f in serve.sh frank-archon.env measure.py hash; do
|
||||
for f in serve.sh frank-primary.env measure.py hash; do
|
||||
[ -f "$STAGE/$f" ] || { log "staged file missing: $STAGE/$f"; exit 1; }
|
||||
done
|
||||
mkdir -p "$STAGE/last"
|
||||
@@ -133,7 +133,7 @@ main() {
|
||||
|
||||
t0="$(date +%s)"
|
||||
log "starting bench instance from staged config (fingerprint $(tr -d '[:space:]' <"$STAGE/hash"))"
|
||||
FRANK_ARCHON_ENV_FILE="$STAGE/frank-archon.env" \
|
||||
FRANK_PRIMARY_ENV_FILE="$STAGE/frank-primary.env" \
|
||||
setsid nohup bash "$STAGE/serve.sh" >"$STAGE/serve.out" 2>&1 </dev/null &
|
||||
echo "$!" >"$STAGE/serve.pid"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Decode throughput benchmark for frank-dragoon.
|
||||
# Decode throughput benchmark for frank-large.
|
||||
# Measures generation tok/s with 8K+ token prompts and 256 generated tokens.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -75,7 +75,7 @@ print(json.dumps(' '.join(words)))
|
||||
t0=$(date +%s%N)
|
||||
resp=$(curl -s -m 600 http://127.0.0.1:$PORT/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"model\":\"frank-dragoon\",\"messages\":[{\"role\":\"user\",\"content\":$prompt}],\"max_tokens\":$N_GEN,\"stream\":false}" 2>&1)
|
||||
-d "{\"model\":\"frank-large\",\"messages\":[{\"role\":\"user\",\"content\":$prompt}],\"max_tokens\":$N_GEN,\"stream\":false}" 2>&1)
|
||||
t1=$(date +%s%N)
|
||||
|
||||
if ! echo "$resp" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP-based prefill-throughput sweep for frank-dragoon.
|
||||
HTTP-based prefill-throughput sweep for frank-large.
|
||||
Starts the server with a given --threads-batch value, sends a 64K-prompt via
|
||||
the chat API (max_tokens=0), measures prompt_tokens/wall_time.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Threads-batch sweep for frank-dragoon on dankclaw
|
||||
# Threads-batch sweep for frank-large on dankclaw
|
||||
# Benches prefill throughput at 65536 prompt tokens across threads-batch values.
|
||||
set -uo pipefail
|
||||
|
||||
@@ -10,7 +10,7 @@ TB_LIST=(16 32 48 64 80 96 128)
|
||||
ENV_FILE="/home/dank/GitPackages/ROCmFPX/build-qwen38/bin"
|
||||
|
||||
echo "========================================"
|
||||
echo "THREADS-BATCH SWEEP — frank-dragoon"
|
||||
echo "THREADS-BATCH SWEEP — frank-large"
|
||||
echo "Model: $(basename "$MODEL") ($(du -h "$MODEL" | cut -f1))"
|
||||
echo "Threads-batch values: ${TB_LIST[*]}"
|
||||
echo "Prompt: 65536 tokens, n-gen: 0 (prefill-only)"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Benchmark ubatch with llama-bench for frank-dragoon.
|
||||
# Benchmark ubatch with llama-bench for frank-large.
|
||||
#
|
||||
# This is intentionally a ubatch-only benchmark. It uses one llama-bench
|
||||
# process, an 8K prompt workload, and equal batch/ubatch values. No server
|
||||
# process, HTTP requests, or log scraping are used.
|
||||
#
|
||||
# Run on Dankclaw after stopping frank-dragoon:
|
||||
# sudo systemctl stop frank-dragoon
|
||||
# Run on Dankclaw after stopping frank-large:
|
||||
# sudo systemctl stop frank-large
|
||||
# ./scripts/bench-ubatch.sh
|
||||
# sudo systemctl start frank-dragoon
|
||||
# sudo systemctl start frank-large
|
||||
set -euo pipefail
|
||||
|
||||
: "${LLAMA_BENCH:=/home/dank/GitPackages/ROCmFPX/build-strix-rocmfp4/bin/llama-bench}"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-frank-archon-rad.sh — build frank-archon's .rad container from the
|
||||
# build-frank-primary-rad.sh — build frank-primary's .rad container from the
|
||||
# OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint, at NATIVE precision.
|
||||
#
|
||||
# bash scripts/build-frank-archon-rad.sh # convert
|
||||
# bash scripts/build-frank-archon-rad.sh --plan-only # declare, no I/O
|
||||
# bash scripts/build-frank-archon-rad.sh --restage # redo staging
|
||||
# bash scripts/build-frank-archon-rad.sh --force # rebuild existing
|
||||
# bash scripts/build-frank-primary-rad.sh # convert
|
||||
# bash scripts/build-frank-primary-rad.sh --plan-only # declare, no I/O
|
||||
# bash scripts/build-frank-primary-rad.sh --restage # redo staging
|
||||
# bash scripts/build-frank-primary-rad.sh --force # rebuild existing
|
||||
#
|
||||
# WHY A STAGING DIR (the one non-obvious step): rad-convert selects its
|
||||
# architecture plugin by the checkpoint's quantisation DESCRIPTOR —
|
||||
@@ -55,8 +55,8 @@ OUT="$OUT_DIR/qwen4exp-fp8-native.rad"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/frank-archon.env"
|
||||
IMAGE="$FRANK_ARCHON_IMAGE"
|
||||
source "$SCRIPT_DIR/frank-primary.env"
|
||||
IMAGE="$FRANK_PRIMARY_IMAGE"
|
||||
|
||||
plan_only=0 restage=0 force=0
|
||||
for a in "$@"; do
|
||||
@@ -69,11 +69,11 @@ for a in "$@"; do
|
||||
done
|
||||
|
||||
if [ ! -f "$SRC/config.json" ]; then
|
||||
echo "ERROR: raw checkpoint missing: $SRC (run scripts/download-frank-archon.sh --fetch)" >&2
|
||||
echo "ERROR: raw checkpoint missing: $SRC (run scripts/download-frank-primary.sh --fetch)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "verifying raw checkpoint first..."
|
||||
bash "$SCRIPT_DIR/download-frank-archon.sh"
|
||||
bash "$SCRIPT_DIR/download-frank-primary.sh"
|
||||
|
||||
if [ -f "$OUT" ] && [ "$plan_only" -eq 0 ] && [ "$force" -eq 0 ]; then
|
||||
echo "container already exists: $OUT"
|
||||
@@ -83,7 +83,7 @@ fi
|
||||
|
||||
# ---- staging ----------------------------------------------------------------
|
||||
if [ "$restage" -eq 1 ] && [ -e "$STAGE" ]; then
|
||||
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-archon-stage"
|
||||
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-primary-stage"
|
||||
mkdir -p "$TRASH"
|
||||
mv "$STAGE" "$TRASH/stage-$(date +%H%M%S)"
|
||||
echo "old staging dir moved to $TRASH"
|
||||
@@ -138,7 +138,7 @@ CONVERT_ARGS=(/models/stage-qwen4exp-fp8 -o /models/rad/qwen4exp-fp8-native.rad
|
||||
# exactly and the plugin's one-element scale plane is produced.
|
||||
CONVERT_ARGS+=(--quant "blk.*.ple_ngram.weight=rtn:codes=fp8_e4m3,block=*x*,scale=bf16,rule=fixed,scale_value=$PLE_SCALE")
|
||||
[ "$force" -eq 1 ] && [ -f "$OUT" ] && {
|
||||
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-archon-rad"
|
||||
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-primary-rad"
|
||||
mkdir -p "$TRASH"
|
||||
mv "$OUT" "$TRASH/qwen4exp-fp8-native.rad.$(date +%H%M%S)"
|
||||
echo "previous container moved to $TRASH"
|
||||
@@ -158,8 +158,8 @@ echo "rad-info summary:"
|
||||
podman run --rm --entrypoint rad-info -v /home/dank/models:/models:ro \
|
||||
"$IMAGE" "/models/rad/qwen4exp-fp8-native.rad" | head -20
|
||||
MIB=$(( $(stat -c %s "$OUT") / 1048576 ))
|
||||
echo "container: $OUT (${MIB} MiB; env floor $FRANK_ARCHON_RAD_MIN_MIB MiB)"
|
||||
if [ "$MIB" -lt "$FRANK_ARCHON_RAD_MIN_MIB" ]; then
|
||||
echo "container: $OUT (${MIB} MiB; env floor $FRANK_PRIMARY_RAD_MIN_MIB MiB)"
|
||||
if [ "$MIB" -lt "$FRANK_PRIMARY_RAD_MIN_MIB" ]; then
|
||||
echo "ERROR: container below the env floor — the convert was truncated" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the gufo runtime that serves frank-dragoon on Dankclaw.
|
||||
# Build the gufo runtime that serves frank-large on Dankclaw.
|
||||
#
|
||||
# bash scripts/build-gufo.sh # build + install the pinned tag
|
||||
# bash scripts/build-gufo.sh --verify # only check the installed tree
|
||||
@@ -93,4 +93,4 @@ cmake --install build/release
|
||||
|
||||
echo "== verify =="
|
||||
"$PREFIX/bin/gufo" diagnose
|
||||
echo "frank-dragoon runtime ready: $PREFIX/bin/gufo ($GUFO_TAG @ $commit)"
|
||||
echo "frank-large runtime ready: $PREFIX/bin/gufo ($GUFO_TAG @ $commit)"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the ROCmFPX runtime that serves frank-dragoon on Dankclaw.
|
||||
# Build the ROCmFPX runtime that serves frank-large on Dankclaw.
|
||||
#
|
||||
# bash scripts/build-rocmfpx.sh # clone/fetch + build the pin
|
||||
# bash scripts/build-rocmfpx.sh --verify # only check the installed tree
|
||||
#
|
||||
# Runtime: kingjones30/ROCmFPX — charlie12345/ROCmFPX plus the model-card
|
||||
# patches, i.e. the fork that owns the ROCmFPx tensor formats. frank-dragoon's
|
||||
# patches, i.e. the fork that owns the ROCmFPx tensor formats. frank-large's
|
||||
# payload is ftype 115 (`Q8_0_ROCMFPX_AGENT`): 311 tensors of
|
||||
# GGML_TYPE_Q8_0_ROCMFPX (103), a type that exists ONLY in this fork, so
|
||||
# stock llama.cpp, gufo and every other loader on the host reject the file.
|
||||
@@ -30,7 +30,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROCMFPX_REPO="${ROCMFPX_REPO:-https://github.com/kingjones30/ROCmFPX.git}"
|
||||
# The commit frank-dragoon was qualified with (measured 2026-10-02).
|
||||
# The commit frank-large was qualified with (measured 2026-10-02).
|
||||
ROCMFPX_COMMIT="${ROCMFPX_COMMIT:-36e9acd40e10a87cd3c3ef8ec734668757dc8520}"
|
||||
SRC_DIR="${ROCMFPX_SRC_DIR:-/home/dank/GitPackages/kingjones-ROCmFPX}"
|
||||
BUILD_DIR="${ROCMFPX_BUILD_DIR:-$SRC_DIR/build}"
|
||||
@@ -86,4 +86,4 @@ cmake --build "$BUILD_DIR" --target llama-server llama-quantize -j "$JOBS"
|
||||
|
||||
echo "== verify =="
|
||||
"$BUILD_DIR/bin/llama-server" --version | head -1
|
||||
echo "frank-dragoon runtime ready: $BUILD_DIR/bin/llama-server ($ROCMFPX_COMMIT)"
|
||||
echo "frank-large runtime ready: $BUILD_DIR/bin/llama-server ($ROCMFPX_COMMIT)"
|
||||
|
||||
@@ -26,8 +26,8 @@ RECIPE=/home/dank/serverAppConfigs-swift15/scripts/recipes/qwen4exp-swift15-w4dn
|
||||
CALIB=/home/dank/corpora/calib
|
||||
OUT=/home/dank/models/rad/qwen4exp-swift15-w4dn8-i8-hc8m.rad
|
||||
LOG=/home/dank/logs/swift15_convert.log
|
||||
# gate_up ~37.6 + down ~18.8 + PLE 47.7 + trunk/embeddings/head ~10 GiB (all-w4 expert
|
||||
# table; one encoding per expert table rules out a block-fp8 down tier -- see the recipe header).
|
||||
# gate_up ~37.6 + down ~18.8 + PLE 47.7 + trunk/embeddings/head ~10 GiB (all-w4 expert table;
|
||||
# the plugin's one-encoding law dropped the block-fp8 down tier -- see the recipe header).
|
||||
MIN_MIB=${FRANK_SWIFT15_RAD_MIN_MIB:-108000}
|
||||
|
||||
# Gate 1: inputs. A missing Gram directory converts UNCALIBRATED with a
|
||||
|
||||
@@ -57,9 +57,9 @@ else
|
||||
echo " none"
|
||||
fi
|
||||
|
||||
section "frank-archon unit"
|
||||
printf ' active: %s\n' "$(systemctl is-active frank-archon 2>/dev/null | head -1)"
|
||||
printf ' enabled: %s\n' "$(systemctl is-enabled frank-archon 2>/dev/null | head -1)"
|
||||
section "frank-primary unit"
|
||||
printf ' active: %s\n' "$(systemctl is-active frank-primary 2>/dev/null | head -1)"
|
||||
printf ' enabled: %s\n' "$(systemctl is-enabled frank-primary 2>/dev/null | head -1)"
|
||||
|
||||
section "glm serve tail"
|
||||
if podman ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "$GLM_CTR"; then
|
||||
|
||||
+461
-54
@@ -1,68 +1,475 @@
|
||||
#!/usr/bin/env python3
|
||||
"""deploy-plan.py — map changed paths to deployable services.
|
||||
"""Plan runtime services affected by repository changes.
|
||||
|
||||
Thin wrapper over scripts/service-map.py, which owns the path->service
|
||||
semantics in the shared inventory (shared/services.yml): per-service path
|
||||
rules, the quadlet netns/network coupling graph, host scoping (a host's
|
||||
directory can only select that host's services), and canonical restart
|
||||
order. This file keeps the deployer's CLI contract only.
|
||||
|
||||
Usage:
|
||||
deploy-plan.py --target NAME --repo DIR --base REV --head REV
|
||||
deploy-plan.py --target NAME [paths...] (paths also on stdin)
|
||||
|
||||
Prints one service name per line (canonical restart order); exit 0 with no
|
||||
output when nothing relevant changed.
|
||||
The planner is intentionally side-effect free. It accepts either an explicit
|
||||
path list or two Git revisions. The latter lets compose-file changes resolve
|
||||
to only the service definitions that actually changed instead of restarting an
|
||||
entire compose project.
|
||||
"""
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SERVICE_MAP = os.path.join(SCRIPT_DIR, "service-map.py")
|
||||
import yaml
|
||||
|
||||
# Dependency-sensitive services come before their consumers. Independent
|
||||
# compose services follow. LLM services are deliberately last and are never
|
||||
# implied by a generic host, stack, documentation, or config change.
|
||||
SERVICE_ORDER = (
|
||||
"gluetun",
|
||||
"searxng-valkey",
|
||||
"searxng",
|
||||
"marreta",
|
||||
"pihole",
|
||||
"open-webui",
|
||||
"gitea-postgres",
|
||||
"gitea-server",
|
||||
"comfyui",
|
||||
"seerr",
|
||||
"hermes",
|
||||
"hermes-dashboard",
|
||||
"hermes-webui",
|
||||
"prometheus",
|
||||
"grafana",
|
||||
"loki",
|
||||
"alloy",
|
||||
"node-exporter",
|
||||
"alertmanager",
|
||||
"authentik-postgres",
|
||||
"authentik-server",
|
||||
"authentik-worker",
|
||||
"authentik-outpost",
|
||||
"authentik-outpost-ldap",
|
||||
"caddy",
|
||||
"homepage",
|
||||
"romm-mariadb",
|
||||
"romm-redis",
|
||||
"romm",
|
||||
"copyparty",
|
||||
"jellyfin",
|
||||
"calibre-web",
|
||||
"calibre-server",
|
||||
"plex",
|
||||
"tautulli",
|
||||
"prowlarr",
|
||||
"qbittorrent",
|
||||
"radarr",
|
||||
"sonarr",
|
||||
"lidarr",
|
||||
"readarr",
|
||||
"frank-large",
|
||||
"frank-secondary",
|
||||
"frank-primary",
|
||||
)
|
||||
|
||||
DANKCLAW_SERVICES = {
|
||||
"hermes-webui",
|
||||
"gluetun",
|
||||
"searxng-valkey",
|
||||
"searxng",
|
||||
"marreta",
|
||||
"pihole",
|
||||
"open-webui",
|
||||
"gitea-postgres",
|
||||
"gitea-server",
|
||||
"comfyui",
|
||||
"seerr",
|
||||
"hermes",
|
||||
"hermes-dashboard",
|
||||
"prometheus",
|
||||
"grafana",
|
||||
"loki",
|
||||
"alloy",
|
||||
"node-exporter",
|
||||
"alertmanager",
|
||||
"authentik-postgres",
|
||||
"authentik-server",
|
||||
"authentik-worker",
|
||||
"authentik-outpost",
|
||||
"authentik-outpost-ldap",
|
||||
"caddy",
|
||||
"homepage",
|
||||
"frank-large",
|
||||
"frank-secondary",
|
||||
}
|
||||
|
||||
DANKSERVER_SERVICES = {
|
||||
"romm-mariadb",
|
||||
"romm-redis",
|
||||
"romm",
|
||||
"copyparty",
|
||||
"jellyfin",
|
||||
"calibre-web",
|
||||
"calibre-server",
|
||||
"plex",
|
||||
"tautulli",
|
||||
"prowlarr",
|
||||
"qbittorrent",
|
||||
"radarr",
|
||||
"sonarr",
|
||||
"lidarr",
|
||||
"readarr",
|
||||
"frank-primary",
|
||||
"node-exporter",
|
||||
}
|
||||
|
||||
# FrankBook-Pro runs the *arr stack + qBittorrent inside a podman machine on
|
||||
# macOS. Service names overlap the Linux hosts (gluetun, qbittorrent, ...);
|
||||
# the per-target sets and the frankbook-pro intercept in deploy-remote.sh
|
||||
# keep dispatch unambiguous.
|
||||
FRANKBOOK_SERVICES = {
|
||||
"gluetun",
|
||||
"qbittorrent",
|
||||
"prowlarr",
|
||||
"radarr",
|
||||
"sonarr",
|
||||
"lidarr",
|
||||
"readarr",
|
||||
}
|
||||
|
||||
DANKCLAW_COMPOSE_SERVICES = set()
|
||||
# Dankserver's compose file is still the source of truth for its media stack;
|
||||
# Dankclaw's is gone (deleted 2026-09-16). Every service on Dankclaw is a
|
||||
# Quadlet unit now — the last compose services were the hermes trio, whose units
|
||||
# have owned their containers since 2026-09-12 and whose compose blocks were
|
||||
# dead weight that could only create a duplicate container.
|
||||
|
||||
DANKSERVER_COMPOSE_SERVICES = {
|
||||
"mariadb",
|
||||
"redis",
|
||||
"romm",
|
||||
"copyparty",
|
||||
"jellyfin",
|
||||
"calibre-web",
|
||||
}
|
||||
COMPOSE_NAME_MAP = {"mariadb": "romm-mariadb", "redis": "romm-redis"}
|
||||
|
||||
|
||||
def git_paths(repo: str, base: str, head: str) -> list[str]:
|
||||
proc = subprocess.run(
|
||||
["git", "-C", repo, "diff", "--name-only", base, head],
|
||||
text=True, capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
sys.stderr.write(f"deploy-plan: git diff failed: {proc.stderr}")
|
||||
raise SystemExit(1)
|
||||
return [line for line in proc.stdout.splitlines() if line]
|
||||
|
||||
def _add(services: set[str], *names: str) -> None:
|
||||
services.update(names)
|
||||
|
||||
|
||||
def target_services(target: str) -> set[str]:
|
||||
if target == "dankclaw":
|
||||
return DANKCLAW_SERVICES
|
||||
if target == "dankserver":
|
||||
return DANKSERVER_SERVICES
|
||||
if target == "frankbook-pro":
|
||||
return FRANKBOOK_SERVICES
|
||||
raise ValueError(f"unknown target: {target}")
|
||||
|
||||
|
||||
def _compose_services(target: str) -> set[str]:
|
||||
return DANKCLAW_COMPOSE_SERVICES if target == "dankclaw" else DANKSERVER_COMPOSE_SERVICES
|
||||
|
||||
|
||||
def _git_show(repo: str, revision: str, path: str) -> str | None:
|
||||
result = subprocess.run(
|
||||
["git", "-C", repo, "show", f"{revision}:{path}"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout if result.returncode == 0 else None
|
||||
|
||||
|
||||
def _yaml_document(contents: str | None) -> dict[str, Any]:
|
||||
if contents is None:
|
||||
return {}
|
||||
try:
|
||||
data = yaml.safe_load(contents) or {}
|
||||
except yaml.YAMLError:
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _yaml_services(contents: str | None) -> dict[str, Any]:
|
||||
services = _yaml_document(contents).get("services", {})
|
||||
return services if isinstance(services, dict) else {}
|
||||
|
||||
|
||||
def compose_changes(repo: str, target: str, base: str, head: str) -> set[str]:
|
||||
"""Return only compose services whose YAML definitions or dependencies changed.
|
||||
|
||||
A target with no compose file (Dankclaw since 2026-09-16) plans nothing:
|
||||
its services are Quadlet units, selected by their own paths.
|
||||
"""
|
||||
path = f"{target}/compose.yaml"
|
||||
if not _git_show(repo, head, path) and not _git_show(repo, base, path):
|
||||
return set()
|
||||
old_doc = _yaml_document(_git_show(repo, base, path))
|
||||
new_doc = _yaml_document(_git_show(repo, head, path))
|
||||
old = old_doc.get("services", {}) if isinstance(old_doc.get("services", {}), dict) else {}
|
||||
new = new_doc.get("services", {}) if isinstance(new_doc.get("services", {}), dict) else {}
|
||||
names = set(old) | set(new)
|
||||
allowed = _compose_services(target)
|
||||
|
||||
# An unavailable or malformed revision is safer as a broad compose plan
|
||||
# than as a silent no-op. It still cannot include LLM units.
|
||||
if (old_doc and not new_doc) or (new_doc and not old_doc and names):
|
||||
return {COMPOSE_NAME_MAP.get(n, n) for n in allowed}
|
||||
|
||||
# Top-level networks/configuration can affect every compose service even
|
||||
# when no individual service mapping changed. Ignore the services key while
|
||||
# comparing the rest of the Compose document.
|
||||
old_globals = {key: value for key, value in old_doc.items() if key != "services"}
|
||||
new_globals = {key: value for key, value in new_doc.items() if key != "services"}
|
||||
if old_globals != new_globals:
|
||||
return {COMPOSE_NAME_MAP.get(n, n) for n in allowed}
|
||||
|
||||
changed = {name for name in names if name in allowed and old.get(name) != new.get(name)}
|
||||
return {COMPOSE_NAME_MAP.get(n, n) for n in changed}
|
||||
|
||||
|
||||
def services_for_path(path: str, target: str) -> set[str]:
|
||||
"""Return services affected by one repository-relative path."""
|
||||
path = path.replace("\\", "/")
|
||||
p = PurePosixPath(path)
|
||||
parts = p.parts
|
||||
services: set[str] = set()
|
||||
if not parts:
|
||||
return services
|
||||
|
||||
# Non-runtime material is deliberately a no-op.
|
||||
if parts[0] in {"docs", "tests", "old", ".githooks"}:
|
||||
return services
|
||||
if path in {"README.md", "AGENTS.md"} or parts[0] == ".obsidian":
|
||||
return services
|
||||
|
||||
if parts[0] == "shared":
|
||||
if len(parts) < 2:
|
||||
return services
|
||||
group = parts[1]
|
||||
if group == "searxng":
|
||||
_add(services, "searxng")
|
||||
elif group == "prometheus":
|
||||
_add(services, "prometheus")
|
||||
if len(parts) >= 3 and parts[2] == "alertmanager.yml":
|
||||
# The Alertmanager config ships from this directory: a change
|
||||
# to it must redeploy the service that installs it.
|
||||
_add(services, "alertmanager")
|
||||
elif group == "loki":
|
||||
_add(services, "loki")
|
||||
elif group == "alloy":
|
||||
_add(services, "alloy")
|
||||
elif group == "grafana":
|
||||
# Provisioned datasources and dashboards: git is the source of
|
||||
# truth, so a change here must restart the service that reads them.
|
||||
_add(services, "grafana")
|
||||
elif group == "homepage":
|
||||
# Homepage is a Dankclaw-owned deployment. Shared homepage files
|
||||
# are intentionally not copied during a Dankserver-only run.
|
||||
if target == "dankclaw":
|
||||
_add(services, "homepage")
|
||||
return services
|
||||
|
||||
if parts[0] == "dankclaw":
|
||||
if len(parts) >= 2 and parts[1] == "compose.yaml":
|
||||
# Dankclaw has no compose file since 2026-09-16: every service is a
|
||||
# Quadlet unit, each selected by its own path. A change (or a
|
||||
# deletion) of this path therefore restarts nothing.
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "Caddyfile":
|
||||
_add(services, "caddy")
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "Containerfile.comfyui":
|
||||
_add(services, "comfyui")
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] in {"caddy", "comfyui", "loki", "alloy", "hermes", "anubis", "seerr"}:
|
||||
_add(services, parts[1])
|
||||
return services
|
||||
if len(parts) >= 4 and parts[1] == "authentik" and parts[2] == "quadlet":
|
||||
name = parts[3].removesuffix(".container")
|
||||
if name in {"authentik-postgres", "authentik-server", "authentik-worker", "authentik-outpost", "authentik-outpost-ldap"}:
|
||||
_add(services, name)
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "searxng":
|
||||
if len(parts) >= 4 and parts[2] == "quadlet":
|
||||
name = parts[3]
|
||||
if name == "gluetun.container":
|
||||
# SearXNG and Marreta (+ its browser companion) share
|
||||
# Gluetun's network namespace. A Gluetun recreation
|
||||
# tears the netns down, so every sharer must restart.
|
||||
_add(services, "gluetun", "searxng", "marreta")
|
||||
elif name == "searxng.container":
|
||||
_add(services, "searxng")
|
||||
elif name == "searxng-valkey.container":
|
||||
_add(services, "searxng-valkey", "searxng")
|
||||
elif name == "searxng.network":
|
||||
_add(services, "gluetun", "searxng-valkey", "searxng", "marreta")
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "marreta":
|
||||
# One deployable owns both the app and its browser companion;
|
||||
# both live in gluetun's netns. A change to either unit selects
|
||||
# the marreta service, which restarts browser-then-app.
|
||||
_add(services, "marreta")
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "pihole":
|
||||
_add(services, "pihole")
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "open-webui":
|
||||
_add(services, "open-webui")
|
||||
return services
|
||||
# frank-large left the quadlet shape on 2026-09-19 (CIRU v4.4.1 host
|
||||
# build, root unit): its files live in scripts/ and map there.
|
||||
if len(parts) >= 2 and parts[1] == "observability":
|
||||
# The five observability containers became Quadlet units on
|
||||
# 2026-09-11. A unit change selects exactly its own service;
|
||||
# SERVICE_ORDER already puts each dependency ahead of its
|
||||
# consumer (prometheus before grafana, loki before alloy).
|
||||
if len(parts) >= 4 and parts[2] == "quadlet":
|
||||
name = parts[3].removesuffix(".container")
|
||||
if name in {"prometheus", "grafana", "loki", "alloy", "node-exporter", "alertmanager"}:
|
||||
_add(services, name)
|
||||
return services
|
||||
if len(parts) >= 2 and parts[1] == "gitea":
|
||||
if len(parts) >= 4 and parts[2] == "quadlet":
|
||||
name = parts[3]
|
||||
if name == "gitea-postgres.container":
|
||||
_add(services, "gitea-postgres")
|
||||
elif name == "gitea-server.container":
|
||||
_add(services, "gitea-server")
|
||||
elif name == "gitea-caddy.container":
|
||||
# Removing the old split proxy requires the canonical
|
||||
# Caddy handler to retire its installed unit/container
|
||||
# before it claims ports 80/443.
|
||||
_add(services, "caddy")
|
||||
elif name == "gitea.network":
|
||||
_add(services, "gitea-postgres", "gitea-server")
|
||||
return services
|
||||
return services
|
||||
|
||||
if parts[0] == "dankserver":
|
||||
if len(parts) >= 2 and parts[1] == "compose.yaml":
|
||||
# Compose service keys map onto the deployed unit names.
|
||||
_add(services, *(COMPOSE_NAME_MAP.get(n, n) for n in DANKSERVER_COMPOSE_SERVICES))
|
||||
return services
|
||||
if len(parts) >= 3 and parts[2] == "quadlet":
|
||||
quadlet_services = {
|
||||
"romm": {"romm", "romm-mariadb", "romm-redis"},
|
||||
"jellyfin": {"jellyfin"},
|
||||
"calibre-web": {"calibre-web"},
|
||||
"calibre-server": {"calibre-server"},
|
||||
"copyparty": {"copyparty"},
|
||||
"plex": {"plex"},
|
||||
"tautulli": {"tautulli"},
|
||||
}
|
||||
_add(services, *quadlet_services.get(parts[1], set()))
|
||||
if len(parts) >= 2 and parts[1] == "observability":
|
||||
# Dankserver's node-exporter became a Quadlet unit on 2026-09-16
|
||||
# (serverAppConfigs-docs/Infrastructure/Observability.md): its
|
||||
# compose service died at a reboot and stayed dead, so Prometheus'
|
||||
# node-dankserver job had nothing to scrape.
|
||||
if len(parts) >= 4 and parts[2] == "quadlet":
|
||||
name = parts[3].removesuffix(".container")
|
||||
if name == "node-exporter":
|
||||
_add(services, "node-exporter")
|
||||
return services
|
||||
# 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":
|
||||
if len(parts) >= 3 and parts[2] == "quadlet":
|
||||
name = parts[3].removesuffix(".container")
|
||||
if name == "gluetun":
|
||||
# Every *arr + qBittorrent container shares gluetun's
|
||||
# network namespace; a gluetun recreation kills all of
|
||||
# them, so the plan restarts the whole netns family.
|
||||
_add(services, "gluetun", "qbittorrent",
|
||||
"prowlarr", "sonarr", "radarr", "lidarr", "readarr")
|
||||
elif name in FRANKBOOK_SERVICES:
|
||||
_add(services, name)
|
||||
return services
|
||||
|
||||
if parts[0] == "scripts":
|
||||
name = p.name
|
||||
if name in {"homepage-status.py", "homepage-status.service", "homepage-status.timer"}:
|
||||
_add(services, "homepage")
|
||||
if name in {"signal-relay.py", "signal-relay.service"}:
|
||||
# The Signal relay is installed by the alertmanager deploy, so a
|
||||
# change to it redeploying alertmanager is the only path that
|
||||
# reaches it on the host.
|
||||
_add(services, "alertmanager")
|
||||
if name in {"frank-secondary.env", "frank-secondary.service", "serve-frank-secondary.sh"}:
|
||||
# llama.cpp frank-secondary (2026-09-16) keeps its unit, env and serve
|
||||
# files here; each one selects frank-secondary only. The ik_llama.cpp
|
||||
# trio that lived here for two days is in scripts/.AGENTTRASH/,
|
||||
# which is archive material and plans nothing.
|
||||
_add(services, "frank-secondary")
|
||||
if name in {"frank-large.env", "frank-large.service", "serve-frank-large.sh"}:
|
||||
# CIRU v4.4.1 frank-large (2026-09-19) keeps its unit, env and
|
||||
# serve files here the same way: a root unit on a host-built
|
||||
# runtime. The download script fetches the payload, which is
|
||||
# host-side state, so it selects nothing.
|
||||
_add(services, "frank-large")
|
||||
if name == "qwen-sharp-v22.4.0.jinja":
|
||||
# The Qwen-Sharp chat template frank-large loads via
|
||||
# --chat-template-file. It lived under dankserver/frank-medium/ until
|
||||
# the 2026-09-26 cutover retired that directory; editing it changes
|
||||
# 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
|
||||
|
||||
# Examples and env templates are not deployed runtime state.
|
||||
if p.name in {".env.example", "env.example"}:
|
||||
return services
|
||||
return services
|
||||
|
||||
def changed_paths(repo: str, base: str, head: str) -> list[str]:
|
||||
result = subprocess.run(
|
||||
["git", "-C", repo, "diff", "--name-only", "--diff-filter=ACDMRTUXB", base, head],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
return [line for line in result.stdout.splitlines() if line]
|
||||
|
||||
|
||||
def plan(paths: list[str], target: str, repo: str | None = None,
|
||||
base: str | None = None, head: str | None = None) -> list[str]:
|
||||
services: set[str] = set()
|
||||
for path in paths:
|
||||
if path == f"{target}/compose.yaml" and repo and base and head:
|
||||
services.update(compose_changes(repo, target, base, head))
|
||||
else:
|
||||
services.update(services_for_path(path, target))
|
||||
allowed = target_services(target)
|
||||
return [name for name in SERVICE_ORDER if name in services and name in allowed]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = sys.argv[1:]
|
||||
if not args or args[0] != "--target":
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
if len(args) < 2:
|
||||
print("missing target name after --target", file=sys.stderr)
|
||||
return 2
|
||||
target = args[1]
|
||||
# `--paths` is the historic seam keyword: a flag, not an option — the
|
||||
# paths are the positional arguments that follow it.
|
||||
rest = [a for a in args[2:] if a != "--paths"]
|
||||
if "--repo" in rest:
|
||||
try:
|
||||
repo = rest[rest.index("--repo") + 1]
|
||||
base = rest[rest.index("--base") + 1]
|
||||
head = rest[rest.index("--head") + 1]
|
||||
except (ValueError, IndexError):
|
||||
print("--repo, --base and --head must appear together", file=sys.stderr)
|
||||
return 2
|
||||
paths = git_paths(repo, base, head)
|
||||
else:
|
||||
paths = rest or [line.strip() for line in sys.stdin if line.strip()]
|
||||
if not paths:
|
||||
return 0
|
||||
proc = subprocess.run([sys.executable, SERVICE_MAP, "plan", "--host", target] + paths,
|
||||
text=True, capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
sys.stderr.write(proc.stderr)
|
||||
return proc.returncode
|
||||
sys.stdout.write(proc.stdout)
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--target", choices=("dankclaw", "dankserver", "frankbook-pro"), required=True)
|
||||
parser.add_argument("--paths", nargs="+")
|
||||
parser.add_argument("--repo", help="repository path for revision-aware planning")
|
||||
parser.add_argument("--base", help="base Git revision")
|
||||
parser.add_argument("--head", help="head Git revision")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.paths and not (args.repo and args.base and args.head):
|
||||
parser.error("provide --paths or all of --repo, --base, and --head")
|
||||
paths = args.paths or changed_paths(args.repo, args.base, args.head)
|
||||
print("\n".join(plan(paths, args.target, args.repo, args.base, args.head)))
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+280
-210
@@ -9,13 +9,6 @@
|
||||
# pending until its service is explicitly deployed.
|
||||
set -euo pipefail
|
||||
|
||||
# The LLM handlers' artifact gates are the serve scripts' own library, so
|
||||
# the deployer and the unit refuse the same missing payload with the same
|
||||
# words — no deploy can pass a gate the unit would fail. GATE_PREFIX is set
|
||||
# per handler.
|
||||
# shellcheck disable=SC1091
|
||||
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/llm-gates.sh"
|
||||
|
||||
TARGET="${1:-}"
|
||||
REVISION="${2:-}"
|
||||
shift 2 || true
|
||||
@@ -87,10 +80,31 @@ cd "$REMOTE_DIR"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
service_is_allowed() {
|
||||
# The inventory (shared/services.yml via scripts/service-map.py) is the
|
||||
# only allow-list: a service is deployable on a target iff the inventory
|
||||
# places it there. Retiring a service from a host is a data edit.
|
||||
python3 scripts/service-map.py allows --host "$TARGET" --service "$1"
|
||||
local service="$1"
|
||||
case "$TARGET:$service" in
|
||||
dankclaw:gluetun|dankclaw:searxng-valkey|dankclaw:searxng|dankclaw:pihole|\
|
||||
dankclaw:open-webui|dankclaw:gitea-postgres|dankclaw:gitea-server|\
|
||||
dankclaw:comfyui|dankclaw:hermes|\
|
||||
dankclaw:hermes-dashboard|dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|\
|
||||
dankclaw:loki|dankclaw:alloy|dankclaw:node-exporter|dankclaw:alertmanager|\
|
||||
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
||||
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
||||
dankclaw:frank-large|dankclaw:frank-secondary|\
|
||||
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
|
||||
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
|
||||
dankserver:sonarr|dankserver:lidarr|dankserver:readarr|\
|
||||
dankserver:frank-primary|dankserver:node-exporter|\
|
||||
frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\
|
||||
frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\
|
||||
frankbook-pro:readarr)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
mark_status() {
|
||||
@@ -178,7 +192,7 @@ add_pending_services() {
|
||||
continue
|
||||
fi
|
||||
[[ -n "$recorded" && "$recorded" != "$REVISION" ]] || continue
|
||||
if [[ "$TARGET" == dankserver && "$service" == frank-dragoon ]]; then continue; fi
|
||||
if [[ "$TARGET" == dankserver && "$service" == frank-large ]]; then continue; fi
|
||||
if ! changed="$(python3 scripts/deploy-plan.py --target "$TARGET" \
|
||||
--repo "$REMOTE_DIR" --base "$recorded" --head "$REVISION")"; then
|
||||
echo "WARN: unable to compare state for $service; retaining it as pending" >&2
|
||||
@@ -260,11 +274,13 @@ install_signal_relay() {
|
||||
}
|
||||
|
||||
copy_loki_config() {
|
||||
copy_config /opt/loki/config shared/loki/loki-config.yaml
|
||||
run_privileged mkdir -p /opt/loki/config
|
||||
run_privileged cp shared/loki/loki-config.yaml /opt/loki/config/loki-config.yaml
|
||||
}
|
||||
|
||||
copy_alloy_config() {
|
||||
copy_config /opt/alloy/config shared/alloy/config.alloy
|
||||
run_privileged mkdir -p /opt/alloy/config
|
||||
run_privileged cp shared/alloy/config.alloy /opt/alloy/config/config.alloy
|
||||
}
|
||||
|
||||
copy_grafana_provisioning() {
|
||||
@@ -273,11 +289,14 @@ copy_grafana_provisioning() {
|
||||
# datasource would be invisible to review, lost with the data volume, and
|
||||
# unknown to anyone reading the repository. The unit already mounts
|
||||
# /opt/grafana/provisioning read-only, so this only has to populate it.
|
||||
copy_config /opt/grafana/provisioning/datasources \
|
||||
shared/grafana/provisioning/datasources/prometheus.yml
|
||||
copy_config /opt/grafana/provisioning/dashboards \
|
||||
shared/grafana/provisioning/dashboards/provider.yml \
|
||||
shared/grafana/provisioning/dashboards/llm-fleet.json
|
||||
run_privileged mkdir -p /opt/grafana/provisioning/datasources \
|
||||
/opt/grafana/provisioning/dashboards
|
||||
run_privileged cp shared/grafana/provisioning/datasources/prometheus.yml \
|
||||
/opt/grafana/provisioning/datasources/prometheus.yml
|
||||
run_privileged cp shared/grafana/provisioning/dashboards/provider.yml \
|
||||
/opt/grafana/provisioning/dashboards/provider.yml
|
||||
run_privileged cp shared/grafana/provisioning/dashboards/llm-fleet.json \
|
||||
/opt/grafana/provisioning/dashboards/llm-fleet.json
|
||||
}
|
||||
|
||||
render_caddyfile() {
|
||||
@@ -422,6 +441,31 @@ deploy_caddy() {
|
||||
fi
|
||||
}
|
||||
|
||||
deploy_compose_service() {
|
||||
local service="$1"
|
||||
if [[ "$TARGET" == dankserver ]]; then
|
||||
# Dankserver's compose file is the source-of-truth inventory, but its
|
||||
# production containers are individually systemd-managed/bare. Do not
|
||||
# use compose up here: it can create a second container with a wrong
|
||||
# name and can interfere with a running workload.
|
||||
echo "WARN: $service is recorded in dankserver/compose.yaml; production container management is host-specific and no restart was attempted."
|
||||
return 0
|
||||
fi
|
||||
if [[ "$TARGET" == dankclaw ]]; then
|
||||
# Dankclaw has no compose file since 2026-09-16: every service is a
|
||||
# Quadlet unit with its own handler above. Reaching this means the
|
||||
# dispatcher is missing a case, and the old fallback (`podman-compose
|
||||
# up -d`) would now be a silent no-op or a duplicate container.
|
||||
echo "no compose services on dankclaw; $service has no quadlet handler" >&2
|
||||
return 2
|
||||
fi
|
||||
case "$service" in
|
||||
*)
|
||||
echo "unknown compose service: $service" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# grafana is the one observability service that holds a credential: the
|
||||
# Authentik OAuth2 client secret. It is seeded into a per-service env file (the
|
||||
@@ -524,35 +568,6 @@ quadlet_unit_dir="$HOME/.config/containers/systemd"
|
||||
# dankserver's user manager had no systemd container generator dir before the
|
||||
# quadlet migration; every handler that installs a unit must find it present.
|
||||
mkdir -p "$quadlet_unit_dir"
|
||||
# copy_config DEST SRC... — the privileged config copy every service uses:
|
||||
# ensure DEST, copy each tracked SRC into it. The deployer owns /opt state;
|
||||
# the files in git are the source of truth, so a service config that only
|
||||
# exists on the host is a gap by policy.
|
||||
copy_config() {
|
||||
local dest="$1"; shift
|
||||
run_privileged mkdir -p "$dest"
|
||||
local src
|
||||
for src in "$@"; do
|
||||
run_privileged cp "$src" "$dest/"
|
||||
done
|
||||
}
|
||||
|
||||
# retire_unit_file PATH TRASH [UNIT] — the repo Deletion & Trash policy, in
|
||||
# code: stop and disable the named unit, move the unit FILE into the home
|
||||
# trash (never rm), report. The dragoon quadlet retirement and the
|
||||
# compose-era comfyui.service retirement were the same six lines twice.
|
||||
retire_unit_file() {
|
||||
local path="$1" trash="$2" unit="${3:-}"
|
||||
[[ -e "$path" ]] || return 0
|
||||
if [[ -n "$unit" ]]; then
|
||||
systemctl --user stop "$unit" 2>/dev/null || true
|
||||
systemctl --user disable "$unit" 2>/dev/null || true
|
||||
fi
|
||||
mkdir -p "$trash"
|
||||
mv -- "$path" "$trash/$(basename "$path")"
|
||||
echo "retired $(basename "$path") -> $trash"
|
||||
}
|
||||
|
||||
install_quadlet() {
|
||||
local service="$1"
|
||||
local source_dir="$2"
|
||||
@@ -640,8 +655,15 @@ deploy_comfyui() {
|
||||
# generator produces, and it is what kept a stale, device-less container
|
||||
# alive through `restart: unless-stopped` (9,146 failures on
|
||||
# "No HIP GPUs are available"). Never `rm`: it moves into the home trash.
|
||||
retire_unit_file "$HOME/.config/systemd/user/comfyui.service" \
|
||||
"$HOME/.AGENTTRASH/20260916-comfyui-compose-unit" comfyui.service
|
||||
local legacy="$HOME/.config/systemd/user/comfyui.service"
|
||||
if [[ -e "$legacy" ]]; then
|
||||
local trash="$HOME/.AGENTTRASH/20260916-comfyui-compose-unit"
|
||||
mkdir -p "$trash"
|
||||
systemctl --user stop comfyui.service 2>/dev/null || true
|
||||
systemctl --user disable comfyui.service 2>/dev/null || true
|
||||
mv -- "$legacy" "$trash/comfyui.service"
|
||||
echo "retired the compose-era comfyui.service -> $trash"
|
||||
fi
|
||||
# The stale container carries the old config (no /dev/kfd, no /dev/dri) and
|
||||
# would keep the quadlet's container from taking the name. It is replaced
|
||||
# by the unit in the next step, so nothing is lost.
|
||||
@@ -699,15 +721,9 @@ deploy_gitea_unit() {
|
||||
systemctl --user restart "$service.service"
|
||||
}
|
||||
|
||||
# The root units are symlinks from the systemd system directory into the
|
||||
# checkout. SYSTEMD_SYSTEM_DIR is production (/etc/systemd/system); the knob
|
||||
# exists so the deploy seam can run a whole handler — relink included — in a
|
||||
# sandbox and read the unit links as evidence, unprivileged.
|
||||
SYSTEMD_SYSTEM_DIR="${SYSTEMD_SYSTEM_DIR:-/etc/systemd/system}"
|
||||
|
||||
relink_unit() {
|
||||
local service="$1"
|
||||
run_privileged ln -sfn "$REMOTE_DIR/scripts/$service.service" "$SYSTEMD_SYSTEM_DIR/$service.service"
|
||||
run_privileged ln -sfn "$REMOTE_DIR/scripts/$service.service" "/etc/systemd/system/$service.service"
|
||||
}
|
||||
|
||||
# Authentik runs as three boot-persistent Quadlet units (postgres -> server ->
|
||||
@@ -926,17 +942,17 @@ deploy_plex() {
|
||||
systemctl --user restart plex.service
|
||||
# Readiness gate: /identity answers MediaContainer XML once PMS is up.
|
||||
#
|
||||
# HTTPS, NOT HTTP. PMS serves plain HTTP on loopback only: a probe
|
||||
# arriving on the container's interface gets its connection accepted and
|
||||
# then an empty reply (curl 52) from a perfectly healthy server, so an
|
||||
# http:// probe would burn the whole timeout and fail a service that
|
||||
# answers https://127.0.0.1:32400/identity in milliseconds. The Caddyfile
|
||||
# dials the same upstream over TLS for the same reason
|
||||
# (tests/caddyfile.bats pins it).
|
||||
# HTTPS, NOT HTTP. PMS drops plain HTTP off-loopback: the probe's
|
||||
# connection arrives on the container's eth0, so an http:// request is
|
||||
# accepted and then answered with nothing (curl 52, Empty reply) against a
|
||||
# perfectly healthy server. Measured 2026-10-02 — the gate burned its full
|
||||
# 600s and failed a plex whose https://127.0.0.1:32400/identity answered
|
||||
# 200 in 44 ms the whole time. The same law is why the Caddyfile dials the
|
||||
# plex upstream over TLS (tests/caddyfile.bats pins it).
|
||||
# -k: PMS presents its own self-signed cert on 32400.
|
||||
#
|
||||
# The budget accounts for a first boot of a large pre-existing library,
|
||||
# which spends minutes indexing before it answers anything.
|
||||
# Reality check from the 2026-09 cutover: the FIRST container boot of a
|
||||
# years-old library ran ~10 min, so the gate still allows 600s.
|
||||
# Fail the deploy (pending state preserved) rather than mark it deployed blind.
|
||||
for ((i = 1; i <= 120; i++)); do
|
||||
curl -fsSk --max-time 3 https://127.0.0.1:32400/identity >/dev/null && return 0
|
||||
@@ -1076,21 +1092,21 @@ deploy_anubis() {
|
||||
wait_container_healthy anubis 60 || return 1
|
||||
}
|
||||
|
||||
# frank-dragoon is the halogen-flash-server CONTAINER (0.16.2, digest-pinned
|
||||
# through scripts/frank-dragoon.env) serving the Quat3rnion ABLITERATED
|
||||
# frank-large is the halogen-flash-server CONTAINER (0.16.2, digest-pinned
|
||||
# through scripts/frank-large.env) serving the Quat3rnion ABLITERATED
|
||||
# Swift1.5 finetune of peonist's Qwen3.8-Flash-Next v2 checkpoint —
|
||||
# qwen38-flash-next-v2-swift15-abliterated.hgn (66,687,678,464 B, ~62 GiB
|
||||
# resident) + the REQUIRED n-gram lookup table + the vision tower —
|
||||
# MTP spec decode (HALOGEN_SPEC_ADAPT=0), 2 slots x 200192, image input —
|
||||
# on the Strix Halo iGPU. It stays a ROOT unit because the serve script
|
||||
# drives `podman run` directly (the frank-archon pattern): relink_unit
|
||||
# points /etc/systemd/system/frank-dragoon.service at the repo file, boot
|
||||
# drives `podman run` directly (the frank-primary pattern): relink_unit
|
||||
# points /etc/systemd/system/frank-large.service at the repo file, boot
|
||||
# persistence is that unit's [Install], and LimitMEMLOCK=infinity is
|
||||
# fleet parity. 2026-10-06 cutover from the halogen 27B-dense shape —
|
||||
# rollback = revert the swap commit's trio; the 27B payload stays on
|
||||
# disk (~/models/halogen-qwen3.8-27b).
|
||||
#
|
||||
# The gates mirror serve-frank-dragoon.sh's predicates EXACTLY (the
|
||||
# The gates mirror serve-frank-large.sh's predicates EXACTLY (the
|
||||
# 2026-10-01 crash-loop lesson): image digest in dank's podman storage,
|
||||
# each .hgn payload (checkpoint, n-gram table, vision tower) at the exact
|
||||
# byte count, the tokenizer directory holding every named file as a
|
||||
@@ -1098,19 +1114,24 @@ deploy_anubis() {
|
||||
# artifact would leave a restart-looping unit and a dark endpoint. The
|
||||
# port gate is the same as every cutover since 2026-09-16: nothing
|
||||
# starts until 7770 is released.
|
||||
LLM_DRAGOON_IMAGE="${LLM_DRAGOON_IMAGE:-ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a}"
|
||||
LLM_DRAGOON_CKPT="${LLM_DRAGOON_CKPT:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-v2-swift15-abliterated.hgn}"
|
||||
LLM_DRAGOON_CKPT_BYTES="${LLM_DRAGOON_CKPT_BYTES:-66687678464}"
|
||||
LLM_DRAGOON_NGRAM="${LLM_DRAGOON_NGRAM:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-ngram.hgn}"
|
||||
LLM_DRAGOON_NGRAM_BYTES="${LLM_DRAGOON_NGRAM_BYTES:-51200246144}"
|
||||
LLM_DRAGOON_VISION="${LLM_DRAGOON_VISION:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-vision.hgn}"
|
||||
LLM_DRAGOON_VISION_BYTES="${LLM_DRAGOON_VISION_BYTES:-897916416}"
|
||||
LLM_DRAGOON_TOK_DIR="${LLM_DRAGOON_TOK_DIR:-/home/dank/models/halogen-flashnext-v2/tokenizer}"
|
||||
LLM_DRAGOON_TOK_FILES="${LLM_DRAGOON_TOK_FILES:-tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json}"
|
||||
LLM_LARGE_IMAGE="${LLM_LARGE_IMAGE:-ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a}"
|
||||
LLM_LARGE_CKPT="${LLM_LARGE_CKPT:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-v2-swift15-abliterated.hgn}"
|
||||
LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-66687678464}"
|
||||
LLM_LARGE_NGRAM="${LLM_LARGE_NGRAM:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-ngram.hgn}"
|
||||
LLM_LARGE_NGRAM_BYTES="${LLM_LARGE_NGRAM_BYTES:-51200246144}"
|
||||
LLM_LARGE_VISION="${LLM_LARGE_VISION:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-vision.hgn}"
|
||||
LLM_LARGE_VISION_BYTES="${LLM_LARGE_VISION_BYTES:-897916416}"
|
||||
LLM_LARGE_TOK_DIR="${LLM_LARGE_TOK_DIR:-/home/dank/models/halogen-flashnext-v2/tokenizer}"
|
||||
LLM_LARGE_TOK_FILES="${LLM_LARGE_TOK_FILES:-tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json}"
|
||||
|
||||
retire_halogen_dragoon_quadlet() {
|
||||
retire_unit_file "$quadlet_unit_dir/frank-dragoon.container" \
|
||||
"$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet" frank-dragoon.service
|
||||
retire_halogen_large_quadlet() {
|
||||
local trash="$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet"
|
||||
systemctl --user stop frank-large.service 2>/dev/null || true
|
||||
if [[ -e "$quadlet_unit_dir/frank-large.container" ]]; then
|
||||
mkdir -p "$trash"
|
||||
mv -- "$quadlet_unit_dir/frank-large.container" "$trash/frank-large.container"
|
||||
echo "retired quadlet frank-large.container -> $trash"
|
||||
fi
|
||||
systemctl --user daemon-reload
|
||||
}
|
||||
|
||||
@@ -1132,159 +1153,187 @@ wait_port_free() {
|
||||
ss -tan "sport = :$port" >&2 || true
|
||||
return 1
|
||||
}
|
||||
# assert_unit_owns_port SERVICE PORT — the false-green gate every LLM
|
||||
# endpoint shares. A health answer proves the PORT replied, never WHO
|
||||
# replied: on 2026-09-26 a leftover probe held :7770, satisfied the curl,
|
||||
# and the deploy reported green over a stopped unit. Green requires the
|
||||
# unit's own MainPID to be alive behind the answer.
|
||||
assert_unit_owns_port() {
|
||||
local service="$1" port="$2" main_pid
|
||||
main_pid="$(run_privileged systemctl show -p MainPID --value "$service.service")"
|
||||
if [[ -z "$main_pid" || "$main_pid" == "0" ]]; then
|
||||
echo "FAIL: :$port answered but $service.service has no MainPID — a stray process holds the port" >&2
|
||||
|
||||
deploy_llm_large() {
|
||||
# Refuse rather than restart-loop; every gate below has a twin in
|
||||
# serve-frank-large.sh and they must agree (2026-10-01 lesson).
|
||||
# Capture podman output before grepping: `podman images | grep -q`
|
||||
# races under pipefail (SIGPIPE 141 becomes "no match", 2026-09-30).
|
||||
if ! grep -qF "${LLM_LARGE_IMAGE#*@}" <<<"$(podman images --format '{{.RepoDigests}}' 2>/dev/null)"; then
|
||||
echo "ERROR: halogen image absent: $LLM_LARGE_IMAGE" >&2
|
||||
echo "frank-large: pull it: podman pull ${LLM_LARGE_IMAGE%@*}" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$service: health answered by unit MainPID $main_pid"
|
||||
}
|
||||
if [[ ! -f "$LLM_LARGE_CKPT" ]]; then
|
||||
echo "ERROR: halogen checkpoint absent at $LLM_LARGE_CKPT" >&2
|
||||
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$(stat -c %s "$LLM_LARGE_CKPT")" != "$LLM_LARGE_CKPT_BYTES" ]]; then
|
||||
echo "ERROR: halogen checkpoint is not $LLM_LARGE_CKPT_BYTES bytes (partial download?)" >&2
|
||||
echo "frank-large: re-check: bash scripts/download-frank-large-flashnext.sh --verify-only" >&2
|
||||
return 1
|
||||
fi
|
||||
for gate in "$LLM_LARGE_NGRAM:$LLM_LARGE_NGRAM_BYTES:n-gram table" \
|
||||
"$LLM_LARGE_VISION:$LLM_LARGE_VISION_BYTES:vision tower"; do
|
||||
IFS=: read -r gpath gbytes gname <<<"$gate"
|
||||
if [[ ! -f "$gpath" ]]; then
|
||||
echo "ERROR: halogen $gname absent at $gpath" >&2
|
||||
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ "$(stat -c %s "$gpath")" != "$gbytes" ]]; then
|
||||
echo "ERROR: halogen $gname is not $gbytes bytes (partial download?)" >&2
|
||||
echo "frank-large: re-check: bash scripts/download-frank-large-flashnext.sh --verify-only" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
if [[ -L "$LLM_LARGE_TOK_DIR" || ! -d "$LLM_LARGE_TOK_DIR" ]]; then
|
||||
echo "ERROR: tokenizer dir missing or a symlink at $LLM_LARGE_TOK_DIR" >&2
|
||||
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
return 1
|
||||
fi
|
||||
for tf in $LLM_LARGE_TOK_FILES; do
|
||||
if [[ ! -f "$LLM_LARGE_TOK_DIR/$tf" || -L "$LLM_LARGE_TOK_DIR/$tf" ]]; then
|
||||
echo "ERROR: tokenizer file missing or a symlink: $LLM_LARGE_TOK_DIR/$tf" >&2
|
||||
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# start_root_unit — the tail every LLM root unit deploys: relink the unit
|
||||
# file from the checkout into /etc/systemd/system, (re)enable, restart,
|
||||
# wait for /health, and prove the answer came from the unit's own process.
|
||||
# The three endpoints differ in gates, ports, and budgets, never in this.
|
||||
start_root_unit() {
|
||||
local service="$1" port="$2" health_limit="$3"
|
||||
relink_unit "$service"
|
||||
run_privileged systemctl daemon-reload
|
||||
run_privileged systemctl enable "$service.service"
|
||||
run_privileged systemctl restart "$service.service"
|
||||
wait_for_health "$service" "http://127.0.0.1:${port}/health" "$health_limit" || return 1
|
||||
assert_unit_owns_port "$service" "$port"
|
||||
}
|
||||
|
||||
deploy_llm_dragoon() {
|
||||
# Refuse rather than restart-loop; every gate here is the SAME library
|
||||
# call the serve script makes, so the two twins cannot drift (the
|
||||
# 2026-10-01 crash-loop lesson).
|
||||
# Capture podman output before grepping is moot: require_image is one
|
||||
# command, no pipe (the 2026-09-30 SIGPIPE lesson).
|
||||
export GATE_PREFIX=frank-dragoon
|
||||
require_image "$LLM_DRAGOON_IMAGE" \
|
||||
"pull it: podman pull docker://${LLM_DRAGOON_IMAGE%@*}" || return 1
|
||||
require_bytes "$LLM_DRAGOON_CKPT" checkpoint "$LLM_DRAGOON_CKPT_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh; re-check: --verify-only" || return 1
|
||||
require_bytes "$LLM_DRAGOON_NGRAM" n-gram "$LLM_DRAGOON_NGRAM_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh; re-check: --verify-only" || return 1
|
||||
require_bytes "$LLM_DRAGOON_VISION" vision "$LLM_DRAGOON_VISION_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh; re-check: --verify-only" || return 1
|
||||
# shellcheck disable=SC2086
|
||||
require_dir_files "$LLM_DRAGOON_TOK_DIR" tokenizer \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh" \
|
||||
$LLM_DRAGOON_TOK_FILES || return 1
|
||||
|
||||
retire_halogen_dragoon_quadlet
|
||||
retire_halogen_large_quadlet
|
||||
# The predecessor on a re-deploy is THIS unit: stop it (ExecStop stops
|
||||
# the container) before waiting on the port. The stray sweep is keyed
|
||||
# on the FULL container name: it appears in the argv of a manual
|
||||
# `podman run --name frank-dragoon-halogen` / `podman wait`, while a
|
||||
# pattern naming only the runtime would ALSO match this deployer's own
|
||||
# command line — a pkill whose pattern hits its own argv takes the
|
||||
# running deploy down with the target. The full name can never match
|
||||
# frank-zealot's llama-server either.
|
||||
run_privileged systemctl stop frank-dragoon.service 2>/dev/null || true
|
||||
pkill -f 'frank-dragoon-halogen' 2>/dev/null || true
|
||||
podman rm -f frank-dragoon frank-dragoon-halogen >/dev/null 2>&1 || true
|
||||
# on the FULL container name, which appears in the argv of a manual
|
||||
# `podman run --name frank-large-halogen` / `podman wait` but NOT in
|
||||
# this deployer's own command line (`bash deploy-remote.sh
|
||||
# frank-large`) — so it cannot shoot itself in the foot (the fleet
|
||||
# pkill law), and it can never match frank-secondary's llama-server.
|
||||
run_privileged systemctl stop frank-large.service 2>/dev/null || true
|
||||
pkill -f 'frank-large-halogen' 2>/dev/null || true
|
||||
podman rm -f frank-large frank-large-halogen >/dev/null 2>&1 || true
|
||||
# Nothing may start until the predecessor has released the port
|
||||
# (2026-09-16: "Deactivated successfully" was not "port free").
|
||||
wait_port_free 7770 || return 1
|
||||
start_root_unit frank-dragoon 7770 1200
|
||||
relink_unit frank-large
|
||||
run_privileged systemctl daemon-reload
|
||||
run_privileged systemctl enable frank-large.service
|
||||
run_privileged systemctl restart frank-large.service
|
||||
wait_for_health frank-large http://127.0.0.1:7770/health 1200
|
||||
}
|
||||
|
||||
# deploy_llm_zealot — frank-zealot is llama.cpp (master build, CUDA sm_86) on
|
||||
# deploy_llm_secondary — frank-secondary is llama.cpp (master build, CUDA sm_86) on
|
||||
# the RTX 3090 as a ROOT unit: the runtime is a native binary tree under
|
||||
# ~/GitPackages/llama-master/build-cuda, so there is no container image and
|
||||
# no generated unit to avoid. relink_unit points
|
||||
# /etc/systemd/system/frank-zealot.service at the repo file and boot
|
||||
# /etc/systemd/system/frank-secondary.service at the repo file and boot
|
||||
# persistence is that unit's [Install] section.
|
||||
#
|
||||
# It is a SEPARATE endpoint on port 7771; frank-dragoon keeps 7770. The 3090 is
|
||||
# not shared: frank-dragoon's engine runs on the gfx1151 iGPU.
|
||||
LLM_ZEALOT_MODEL_FILE="${LLM_ZEALOT_MODEL_FILE:-/home/dank/models/Qwen35-9B-HauhauCS-Q6K/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q6_K.gguf}"
|
||||
LLM_ZEALOT_RUNTIME="${LLM_ZEALOT_RUNTIME:-/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
|
||||
# It is a SEPARATE endpoint on port 7771; frank-large keeps 7770. The 3090 is
|
||||
# not shared: frank-large's engine runs on the gfx1151 iGPU.
|
||||
LLM_SECONDARY_MODEL_FILE="${LLM_SECONDARY_MODEL_FILE:-/home/dank/models/Qwen35-9B-HauhauCS-Q6K/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-Q6_K.gguf}"
|
||||
LLM_SECONDARY_RUNTIME="${LLM_SECONDARY_RUNTIME:-/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
|
||||
# The projector IS the vision capability here: the model is natively
|
||||
# multimodal and the Q6_K file carries no vision tensors — the publisher ships
|
||||
# its own BF16 projector in the same repo, so a deploy that starts without
|
||||
# this file silently ships a text-only endpoint. Checked unconditionally, like
|
||||
# frank-dragoon's.
|
||||
LLM_ZEALOT_MMPROJ="${LLM_ZEALOT_MMPROJ:-/home/dank/models/Qwen35-9B-HauhauCS-Q6K/mmproj-Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-BF16.gguf}"
|
||||
# frank-large's.
|
||||
LLM_SECONDARY_MMPROJ="${LLM_SECONDARY_MMPROJ:-/home/dank/models/Qwen35-9B-HauhauCS-Q6K/mmproj-Qwen3.5-9B-Uncensored-HauhauCS-Aggressive-BF16.gguf}"
|
||||
|
||||
deploy_llm_zealot() {
|
||||
deploy_llm_secondary() {
|
||||
# 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 log to say why. Same library calls
|
||||
# as serve-frank-zealot.sh, so the deployer's twin refuses identically.
|
||||
export GATE_PREFIX=frank-zealot
|
||||
require_exec "$LLM_ZEALOT_RUNTIME" runtime \
|
||||
"build it: cmake -B build-cuda -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=86" \
|
||||
|| return 1
|
||||
require_file "$LLM_ZEALOT_MODEL_FILE" gguf \
|
||||
"fetch it: bash scripts/download-frank-zealot.sh" || return 1
|
||||
require_file "$LLM_ZEALOT_MMPROJ" mmproj \
|
||||
"fetch it: bash scripts/download-frank-zealot.sh" || return 1
|
||||
|
||||
start_root_unit frank-zealot 7771 900
|
||||
# endpoint dark with nothing in the log to say why.
|
||||
if [[ ! -x "$LLM_SECONDARY_RUNTIME" ]]; then
|
||||
echo "ERROR: llama.cpp runtime absent at $LLM_SECONDARY_RUNTIME" >&2
|
||||
echo "frank-secondary: build it (cmake -B build-cuda -DGGML_CUDA=ON" >&2
|
||||
echo "frank-secondary: -DCMAKE_CUDA_ARCHITECTURES=86 -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13" >&2
|
||||
echo "frank-secondary: && cmake --build build-cuda --target llama-server) first." >&2
|
||||
return 1
|
||||
fi
|
||||
for f in "$LLM_SECONDARY_MODEL_FILE" "$LLM_SECONDARY_MMPROJ"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "ERROR: frank-secondary payload file absent at $f" >&2
|
||||
echo "frank-secondary: run scripts/download-frank-secondary.sh first;" >&2
|
||||
echo "frank-secondary: refusing to start a unit that cannot load." >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
relink_unit frank-secondary
|
||||
run_privileged systemctl daemon-reload
|
||||
run_privileged systemctl enable frank-secondary.service
|
||||
run_privileged systemctl restart frank-secondary.service
|
||||
wait_for_health frank-secondary http://127.0.0.1:7771/health 900 || return 1
|
||||
# False-green hardening (frank-large's lesson): the port answering
|
||||
# proves nothing about WHO answers — the unit must be active with a
|
||||
# live MainPID, or a stray SGLang holdover is masquerading as deployed.
|
||||
local main_pid
|
||||
main_pid="$(run_privileged systemctl show -p MainPID --value frank-secondary.service)"
|
||||
if [[ -z "$main_pid" || "$main_pid" == "0" ]]; then
|
||||
echo "FAIL: :7771 answered but frank-secondary.service has no MainPID — a stray process holds the port" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "frank-secondary: health answered by unit MainPID $main_pid"
|
||||
}
|
||||
|
||||
# deploy_llm_archon — frank-archon is the radiance engine (StillDeadcode,
|
||||
# deploy_llm_primary — frank-primary is the radiance engine (StillDeadcode,
|
||||
# digest-pinned stilldeadcode/radiance image) serving the OFFICIAL
|
||||
# Qwen/Qwen3.8-Flash-Next-FP8 at native block-FP8 precision from the
|
||||
# rad-convert container qwen4exp-fp8-native.rad, on BOTH R9700s (TP=2) as a
|
||||
# ROOT unit whose serve script drives `podman run` directly (the engine is
|
||||
# the image entrypoint; the image lives in dank's rootless podman storage).
|
||||
# 2026-10-05 cutover from FreeToken (pre-cutover tag
|
||||
# frank-archon-freetoken-pr587-20261005); before that, the tcclaviger vLLM
|
||||
# fork (tag frank-archon-vllm-shapeG-20261003). Same port and alias; the
|
||||
# frank-primary-freetoken-pr587-20261005); before that, the tcclaviger vLLM
|
||||
# fork (tag frank-primary-vllm-shapeG-20261003). Same port and alias; the
|
||||
# harness models.yml id is unchanged.
|
||||
#
|
||||
# Gates: the image must exist under its digest ref in dank's rootless
|
||||
# storage (FRANK_ARCHON_IMAGE), the .rad container file must exist at the
|
||||
# env's size floor (FRANK_ARCHON_MODEL_FILE >= FRANK_ARCHON_RAD_MIN_MIB —
|
||||
# storage (FRANK_PRIMARY_IMAGE), the .rad container file must exist at the
|
||||
# env's size floor (FRANK_PRIMARY_MODEL_FILE >= FRANK_PRIMARY_RAD_MIN_MIB —
|
||||
# a truncated convert is a guaranteed long-timeout boot), and the unit env
|
||||
# must be complete. Per-file sha256 verification is the fetch script's job
|
||||
# at download time and rad-convert/rad-info's job at build time, never
|
||||
# re-hashed here.
|
||||
LLM_ARCHON_CONTAINER=frank-archon
|
||||
LLM_ARCHON_LEGACY_CONTAINERS="frank-archon-qwen frank-archon-r9v qwen-fp8 glm-probe"
|
||||
LLM_PRIMARY_CONTAINER=frank-primary
|
||||
LLM_PRIMARY_LEGACY_CONTAINERS="frank-primary-qwen frank-primary-r9v qwen-fp8 glm-probe"
|
||||
|
||||
deploy_llm_archon() {
|
||||
deploy_llm_primary() {
|
||||
# Refuse rather than restart-loop: a unit whose image or payload is
|
||||
# missing never becomes healthy, and a green deploy would leave the
|
||||
# endpoint dark with nothing in the log to say why. The gates are the
|
||||
# shared library's, called with the values the unit's own env file
|
||||
# declares — so serve-frank-archon.sh and this handler cannot disagree,
|
||||
# and the payload path lives in exactly one file.
|
||||
export GATE_PREFIX=frank-archon
|
||||
local env_file="${LLM_ARCHON_ENV_FILE:-$REMOTE_DIR/scripts/frank-archon.env}"
|
||||
local image model_dir model_file rad_min
|
||||
# endpoint dark with nothing in the log to say why.
|
||||
local env_file="/home/dank/serverAppConfigs/scripts/frank-primary.env"
|
||||
local image model_dir model_file rad_min mib
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
echo "frank-archon: env: missing at $env_file" >&2
|
||||
echo " hint: deploy the repo checkout first ($REMOTE_DIR)" >&2
|
||||
echo "ERROR: frank-primary env file absent: $env_file" >&2
|
||||
return 1
|
||||
fi
|
||||
image="$(sed -n 's/^FRANK_ARCHON_IMAGE=//p' "$env_file" | tail -1)"
|
||||
model_dir="$(sed -n 's/^FRANK_ARCHON_MODEL_DIR=//p' "$env_file" | tail -1)"
|
||||
model_file="$(sed -n 's/^FRANK_ARCHON_MODEL_FILE=//p' "$env_file" | tail -1)"
|
||||
rad_min="$(sed -n 's/^FRANK_ARCHON_RAD_MIN_MIB=//p' "$env_file" | tail -1)"
|
||||
image="$(sed -n 's/^FRANK_PRIMARY_IMAGE=//p' "$env_file" | tail -1)"
|
||||
model_dir="$(sed -n 's/^FRANK_PRIMARY_MODEL_DIR=//p' "$env_file" | tail -1)"
|
||||
model_file="$(sed -n 's/^FRANK_PRIMARY_MODEL_FILE=//p' "$env_file" | tail -1)"
|
||||
rad_min="$(sed -n 's/^FRANK_PRIMARY_RAD_MIN_MIB=//p' "$env_file" | tail -1)"
|
||||
if [[ -z "$image" || -z "$model_dir" || -z "$model_file" || -z "$rad_min" ]]; then
|
||||
echo "frank-archon: env: missing FRANK_ARCHON_IMAGE/MODEL_DIR/MODEL_FILE/RAD_MIN_MIB" >&2
|
||||
echo " hint: scripts/frank-archon.env" >&2
|
||||
echo "ERROR: frank-primary.env missing FRANK_PRIMARY_IMAGE/MODEL_DIR/MODEL_FILE/RAD_MIN_MIB" >&2
|
||||
return 1
|
||||
fi
|
||||
# The image gate is the digest ref itself: a name@sha256 ref exists in
|
||||
# storage only if THAT manifest was pulled. One command — no
|
||||
# pipefail/grep race (the 2026-09-30 lesson).
|
||||
if ! podman image exists "$image"; then
|
||||
echo "ERROR: runtime image absent from storage: $image" >&2
|
||||
echo "frank-primary: podman pull docker://stilldeadcode/radiance:latest" >&2
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "$model_dir/$model_file" ]]; then
|
||||
echo "ERROR: .rad container missing: $model_dir/$model_file" >&2
|
||||
echo "frank-primary: run scripts/build-frank-primary-rad.sh" >&2
|
||||
return 1
|
||||
fi
|
||||
mib=$(( $(stat -c %s "$model_dir/$model_file") / 1048576 ))
|
||||
if [[ "$mib" -lt "$rad_min" ]]; then
|
||||
echo "ERROR: .rad container too small: $model_dir/$model_file is ${mib} MiB, want >= $rad_min" >&2
|
||||
echo "frank-primary: rebuild it (scripts/build-frank-primary-rad.sh --force)" >&2
|
||||
return 1
|
||||
fi
|
||||
require_image "$image" \
|
||||
"pull it: podman pull docker://stilldeadcode/radiance:latest (as dank)" \
|
||||
|| return 1
|
||||
require_min_mib "$model_dir/$model_file" ".rad" "$rad_min" \
|
||||
"convert it: scripts/convert-swift15-w4dn8.sh (rad-info is the record)" \
|
||||
|| return 1
|
||||
|
||||
# A stale container with the same name makes `podman run --name` fail at
|
||||
# start; remove any leftover from a failed previous attempt.
|
||||
@@ -1294,24 +1343,50 @@ deploy_llm_archon() {
|
||||
# The legacy identities (the retired R9V container, the ad hoc
|
||||
# experiment container qwen-fp8, the retired glm-probe) hold the same
|
||||
# GPUs and die here too.
|
||||
run_privileged systemctl stop frank-archon.service 2>/dev/null || true
|
||||
run_privileged systemctl stop frank-primary.service 2>/dev/null || true
|
||||
local ctr
|
||||
for ctr in $LLM_ARCHON_LEGACY_CONTAINERS "$LLM_ARCHON_CONTAINER"; do
|
||||
for ctr in $LLM_PRIMARY_LEGACY_CONTAINERS "$LLM_PRIMARY_CONTAINER"; do
|
||||
podman rm -f "$ctr" >/dev/null 2>&1 || true
|
||||
done
|
||||
# Nothing starts until the port is released: the lucebox cutover
|
||||
# (2026-09-26) and the frank-dragoon cutover (2026-09-16) both failed
|
||||
# (2026-09-26) and the frank-large cutover (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: .rad load + PLE pin + expert host-pool upload (the
|
||||
# publisher's compose healthcheck allows a 15-minute start_period) —
|
||||
# the 900-iteration budget is 1800s, parity with every predecessor on
|
||||
# this port. The HTTP listener binds AFTER the full load (main.cpp:
|
||||
# engine.start() then engine.serve()), so during load the port REFUSES
|
||||
# connections — a bare curl is honest here, unlike FreeToken's
|
||||
# 200-while-loading body. (start_root_unit's MainPID check still
|
||||
# guards the stray-port case.)
|
||||
start_root_unit frank-archon 7770 900
|
||||
# publisher's compose healthcheck allows a 15-minute start_period).
|
||||
# Parity with every predecessor on this port. The HTTP listener binds
|
||||
# AFTER the full load (main.cpp: engine.start() then engine.serve()),
|
||||
# so during load the port REFUSES connections — a bare curl is honest
|
||||
# here, unlike FreeToken's 200-while-loading body. (The MainPID check
|
||||
# below still guards the stray-port case.)
|
||||
local i ok=0
|
||||
for ((i=1; i<=900; i++)); do
|
||||
if curl -fsS http://127.0.0.1:7770/health 2>/dev/null >/dev/null; then
|
||||
echo "frank-primary engine healthy after approximately $((i * 2))s"
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "$ok" != 1 ]]; then
|
||||
echo "FAIL: frank-primary /health never answered 200 in 1800s" >&2
|
||||
return 1
|
||||
fi
|
||||
# False-green hardening (the 2026-09-26 lesson): a bare curl against the
|
||||
# port cannot tell WHO answers — a stray probe holding :7770 satisfies
|
||||
# it while the unit is stopped. The deploy is only green if the unit
|
||||
# itself is active with a live MainPID behind the health answer.
|
||||
local main_pid
|
||||
main_pid="$(run_privileged systemctl show -p MainPID --value frank-primary.service)"
|
||||
if [[ -z "$main_pid" || "$main_pid" == "0" ]]; then
|
||||
echo "FAIL: :7770 answered but frank-primary.service has no MainPID — a stray process holds the port" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "frank-primary: health answered by unit MainPID $main_pid"
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
@@ -1498,9 +1573,9 @@ deploy_service() {
|
||||
anubis) deploy_anubis ;;
|
||||
seerr) deploy_seerr ;;
|
||||
marreta) deploy_marreta ;;
|
||||
frank-dragoon) deploy_llm_dragoon ;;
|
||||
frank-zealot) deploy_llm_zealot ;;
|
||||
frank-archon) deploy_llm_archon ;;
|
||||
frank-large) deploy_llm_large ;;
|
||||
frank-secondary) deploy_llm_secondary ;;
|
||||
frank-primary) deploy_llm_primary ;;
|
||||
hermes|hermes-dashboard|hermes-webui) deploy_hermes ;;
|
||||
romm|romm-mariadb|romm-redis|mariadb|redis) deploy_romm_stack "$service" ;;
|
||||
jellyfin) deploy_jellyfin ;;
|
||||
@@ -1511,12 +1586,7 @@ deploy_service() {
|
||||
tautulli) deploy_tautulli ;;
|
||||
prometheus|grafana|loki|alloy|node-exporter|alertmanager) deploy_observability "$service" ;;
|
||||
comfyui) deploy_comfyui ;;
|
||||
*)
|
||||
# Every deployable service on every target has a handler above
|
||||
# (inventory = allow-list, so nothing unreachable arrives).
|
||||
echo "no handler for service: $service" >&2
|
||||
exit 2
|
||||
;;
|
||||
*) deploy_compose_service "$service" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
+26
-8
@@ -35,7 +35,7 @@ Options:
|
||||
Examples:
|
||||
./scripts/deploy.sh dankclaw
|
||||
./scripts/deploy.sh dankclaw --service searxng
|
||||
./scripts/deploy.sh dankserver --service frank-archon
|
||||
./scripts/deploy.sh dankserver --service frank-primary
|
||||
./scripts/deploy.sh dankclaw --dry-run
|
||||
EOF
|
||||
}
|
||||
@@ -131,13 +131,31 @@ else
|
||||
fi
|
||||
|
||||
valid_service() {
|
||||
# The inventory is the only allow-list (see scripts/service-map.py);
|
||||
# service-map's "allows" exit code is the answer. The former literal
|
||||
# case list here drifted from reality (it still listed the *arr stack
|
||||
# under dankserver, which moved to frankbook-pro, and a
|
||||
# "searxng-network" name that is a generated systemd unit, not a
|
||||
# deployable service).
|
||||
python3 "$REPO_ROOT/scripts/service-map.py" allows --host "$TARGET" --service "$1" >/dev/null
|
||||
local service="$1"
|
||||
case "$TARGET:$service" in
|
||||
dankclaw:caddy|dankclaw:gluetun|dankclaw:searxng-network|dankclaw:searxng-valkey|\
|
||||
dankclaw:searxng|dankclaw:marreta|dankclaw:pihole|dankclaw:open-webui|dankclaw:gitea-postgres|\
|
||||
dankclaw:gitea-server|dankclaw:homepage|\
|
||||
dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\
|
||||
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
||||
dankclaw:alertmanager|\
|
||||
dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
||||
dankclaw:frank-large|dankclaw:frank-secondary|\
|
||||
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
|
||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
|
||||
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
||||
dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\
|
||||
dankserver:frank-primary|\
|
||||
frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\
|
||||
frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\
|
||||
frankbook-pro:readarr)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
for service in "${EXPLICIT_SERVICES[@]}"; do
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""derive-protected-experts.py — rewrite the recipe's protected-expert section
|
||||
from a calibration recording. RUNS ON DANKSERVER (venv: numpy).
|
||||
|
||||
The selection rule (docs/MOE-W4.md "Protected experts", restated in the recipe):
|
||||
The law (docs/MOE-W4.md "Protected experts", restated in the recipe):
|
||||
an expert whose share of its layer's down-input energy is at least 3% AND
|
||||
at least three times its share of the routing is kept bf16; the count must
|
||||
be EVEN on each rank's half of the layer (the four-bit GEMM takes the rest
|
||||
|
||||
+8
-13
@@ -1,18 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# download-frank-dragoon-flashnext.sh — fetch frank-dragoon's flash-server v2
|
||||
# download-frank-large-flashnext.sh — fetch frank-large's flash-server v2
|
||||
# payload: the Quat3rnion abliterated Swift1.5 checkpoint of peonist's v2
|
||||
# .hgn, the REQUIRED lookup table and the optional vision tower from
|
||||
# peonist-ai/halogen-qwen3.8-flash-next (both at the revisions the model
|
||||
# card pins), and the flat tokenizer the two repos share. All public, no
|
||||
# token needed — and a secret would never be passed via argv anyway: the
|
||||
# command line is world-readable through /proc, so any token placed there
|
||||
# leaks to every process-listing tool on the host.
|
||||
# token — and per the fleet secret law, none would ever touch argv.
|
||||
#
|
||||
# The fetch rules, uniform across every sibling script: curl -f so an
|
||||
# access error fails loudly instead of writing the error body as a
|
||||
# "successful" tiny artifact; the exact byte count from the Hub tree API;
|
||||
# LFS sha256 verification; --verify-only to re-check an existing payload
|
||||
# without downloading.
|
||||
# The fetch law, same as every sibling script: curl -f (an access error
|
||||
# must not become a "successful" 153-byte artifact), exact byte count from
|
||||
# the Hub tree API, LFS sha256 verification, --verify-only to re-check an
|
||||
# existing payload without downloading.
|
||||
#
|
||||
# File manifest (verified against the Hub tree API on 2026-10-06):
|
||||
# qwen38-flash-next-v2-swift15-abliterated.hgn 66,687,678,464 B sha256
|
||||
@@ -29,12 +26,10 @@
|
||||
# tokenizer/* @e053f488 — byte-identical to the 27B-era tokenizer
|
||||
# already on disk (tokenizer.json LFS
|
||||
# 0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3);
|
||||
# fetched fresh, never symlinked: a snapshot link points into the
|
||||
# host-side cache directory, which does not exist inside the
|
||||
# container mount, so the link dangles exactly where it is consumed.
|
||||
# fetched fresh rather than symlinked, per the no-symlink law.
|
||||
set -euo pipefail
|
||||
|
||||
DEST="${FRANK_DRAGOON_MODEL_DIR:-$HOME/models/halogen-flashnext-v2}"
|
||||
DEST="${FRANK_LARGE_MODEL_DIR:-$HOME/models/halogen-flashnext-v2}"
|
||||
ABLIT="https://huggingface.co/Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated/resolve/main"
|
||||
PEONIST="https://huggingface.co/peonist-ai/halogen-qwen3.8-flash-next/resolve"
|
||||
NGRAM_REV="5cc17cea1a10b502b4a5db41d8d2b1a0f22dba26"
|
||||
+8
-10
@@ -1,18 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# download-frank-dragoon-halogen.sh — fetch frank-dragoon's halogen payload:
|
||||
# download-frank-large-halogen.sh — fetch frank-large's halogen payload:
|
||||
# the qwen3.8-27b .hgn checkpoint and the export tokenizer, from the
|
||||
# PUBLIC peonist-ai/halogen-qwen3.8-27b repo (no token — and a token
|
||||
# would never travel in argv: command lines are world-readable through
|
||||
# /proc, so a secret placed there leaks to any process-listing tool).
|
||||
# PUBLIC peonist-ai/halogen-qwen3.8-27b repo (no token — and per the fleet
|
||||
# secret law, none would ever touch argv).
|
||||
#
|
||||
# The fetch rules, uniform across every sibling script: curl -f so an
|
||||
# access error fails loudly instead of writing the error body as a
|
||||
# "successful" tiny artifact; the exact byte count from the model card;
|
||||
# LFS sha256 verification; --verify-only to re-check an existing payload
|
||||
# without downloading.
|
||||
# The fetch law, same as every sibling script: curl -f (an access error
|
||||
# must not become a "successful" 153-byte artifact), exact byte count from
|
||||
# the model card, LFS sha256 verification, --verify-only to re-check an
|
||||
# existing payload without downloading.
|
||||
set -euo pipefail
|
||||
|
||||
DEST="${FRANK_DRAGOON_MODEL_DIR:-$HOME/models/halogen-qwen3.8-27b}"
|
||||
DEST="${FRANK_LARGE_MODEL_DIR:-$HOME/models/halogen-qwen3.8-27b}"
|
||||
BASE="https://huggingface.co/peonist-ai/halogen-qwen3.8-27b/resolve/main"
|
||||
|
||||
FILES=(qwen3.8-27b-p1w4d-d2.hgn
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download kingjones777/Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP-GGUF for
|
||||
# frank-dragoon (Dankclaw iGPU). Public repo — no auth header, and never a token
|
||||
# frank-large (Dankclaw iGPU). Public repo — no auth header, and never a token
|
||||
# on the command line (it is world-readable in /proc/<pid>/cmdline).
|
||||
#
|
||||
# The payload is the repo's Q6 AGENT rung:
|
||||
@@ -10,7 +10,7 @@
|
||||
# names for MTP serving ("Take Q6 AGENT (114) if you want protected
|
||||
# heads"). Like every ROCmFPX file it loads on nothing but the fork.
|
||||
#
|
||||
# The other two files frank-dragoon needs are NOT re-downloaded:
|
||||
# The other two files frank-large needs are NOT re-downloaded:
|
||||
# * mtp-Qwen3.8-27B-Q4_0.gguf — the uncensored repo redistributes the
|
||||
# ALIGNED repo's draft head byte for byte (identical Hub LFS oid
|
||||
# 051a1764…), so it is hardlinked out of the aligned payload dir when that
|
||||
@@ -25,7 +25,7 @@
|
||||
# byte count and the Hub LFS sha256. Idempotent: complete files are skipped.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-dragoon-kingjones-uncensored.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-large-kingjones-uncensored.sh [DEST_DIR] [--verify-only]
|
||||
set -euo pipefail
|
||||
|
||||
REPO="kingjones777/Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP-GGUF"
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download kingjones777/Qwen3.8-27B-ROCmFP4-STRIX-MTP-GGUF for frank-dragoon
|
||||
# Download kingjones777/Qwen3.8-27B-ROCmFP4-STRIX-MTP-GGUF for frank-large
|
||||
# (Dankclaw iGPU). Public repo — no auth header, and never a token on the
|
||||
# command line (it is world-readable in /proc/<pid>/cmdline).
|
||||
#
|
||||
@@ -24,7 +24,7 @@
|
||||
# come from the Hub LFS oids (api/models/<repo>/tree/main).
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-dragoon-kingjones.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-large-kingjones.sh [DEST_DIR] [--verify-only]
|
||||
set -euo pipefail
|
||||
|
||||
REPO="kingjones777/Qwen3.8-27B-ROCmFP4-STRIX-MTP-GGUF"
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download and assemble jcbtc/Qwen3.8-Flash-CIRU-STRIX-Orca (the frank-dragoon
|
||||
# Download and assemble jcbtc/Qwen3.8-Flash-CIRU-STRIX-Orca (the frank-large
|
||||
# model) on Dankclaw.
|
||||
#
|
||||
# bash scripts/download-frank-dragoon-orca.sh
|
||||
# bash scripts/download-frank-dragoon-orca.sh --verify-only
|
||||
# bash scripts/download-frank-large-orca.sh
|
||||
# bash scripts/download-frank-large-orca.sh --verify-only
|
||||
#
|
||||
# The package is 136 GB of Hub-sized transport parts (HF caps new uploads at
|
||||
# 50 GB, so the 79 GB main GGUF, the 52 GB PLE payload and the 4 GB MTP head
|
||||
@@ -105,4 +105,4 @@ fi
|
||||
echo "verifying assembled files against SHA256SUMS"
|
||||
(cd "$MODEL_DIR" && sha256sum -c SHA256SUMS)
|
||||
(cd "$MODEL_DIR/vision" && sha256sum -c SHA256SUMS)
|
||||
echo "frank-dragoon payload complete: $MODEL_DIR"
|
||||
echo "frank-large payload complete: $MODEL_DIR"
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the Q4_K_XL frank-dragoon payload on Dankclaw:
|
||||
# Download the Q4_K_XL frank-large payload on Dankclaw:
|
||||
# unsloth/Qwen3.8-27B-GGUF -> Qwen3.8-27B-UD-Q4_K_XL.gguf
|
||||
#
|
||||
# bash scripts/download-frank-dragoon-q4.sh
|
||||
# bash scripts/download-frank-dragoon-q4.sh --verify-only
|
||||
# bash scripts/download-frank-large-q4.sh
|
||||
# bash scripts/download-frank-large-q4.sh --verify-only
|
||||
#
|
||||
# Integrity: the file is checked against its declared byte count and sha256
|
||||
# (the LFS oid from the Hub tree API, 2026-10-01). NO AUTH HEADER IS USED:
|
||||
@@ -21,12 +21,12 @@
|
||||
# x 2 (K,V) x 2 B), so 4 sessions x 163840 ctx = 640,000 tokens = 39.06 GiB.
|
||||
#
|
||||
# The speculative drafter is UNCHANGED and still fetched by its own script
|
||||
# (scripts/download-frank-dragoon.sh, z-lab's Hub repo). That name must not
|
||||
# appear here at all — the frank-dragoon seam greps THIS file for it to prove
|
||||
# (scripts/download-frank-large.sh, z-lab's Hub repo). That name must not
|
||||
# appear here at all — the frank-large seam greps THIS file for it to prove
|
||||
# the drafter was not folded into the payload fetch.
|
||||
#
|
||||
# NOTE ON MTP: this payload has no baked-in MTP head (the Hub repo ships one
|
||||
# separately as MTP/mtp-Qwen3.8-27B-Q4_0.gguf). frank-dragoon does not need it —
|
||||
# separately as MTP/mtp-Qwen3.8-27B-Q4_0.gguf). frank-large does not need it —
|
||||
# spec decode here is the block-diffusion drafter, not MTP.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the Swift-1.5 frank-dragoon candidate on Dankclaw:
|
||||
# Download the Swift-1.5 frank-large candidate on Dankclaw:
|
||||
# ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF
|
||||
# Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-UD-Q8_K_XL.gguf
|
||||
#
|
||||
# bash scripts/download-frank-dragoon-swift.sh
|
||||
# bash scripts/download-frank-dragoon-swift.sh --verify-only
|
||||
# bash scripts/download-frank-large-swift.sh
|
||||
# bash scripts/download-frank-large-swift.sh --verify-only
|
||||
#
|
||||
# Integrity: the file is checked against its declared byte count and sha256
|
||||
# (the LFS oid from the Hub, 2026-09-28). NO AUTH HEADER IS USED: the repo is
|
||||
@@ -15,9 +15,9 @@
|
||||
# Layout after a successful run (MODEL_DIR):
|
||||
# Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-UD-Q8_K_XL.gguf 31457990272
|
||||
#
|
||||
# This is the CANDIDATE payload, not the deployed one. frank-dragoon still
|
||||
# serves scripts/download-frank-dragoon.sh's output; do not repoint
|
||||
# frank-dragoon.env at MODEL_DIR until the gufo load test passes.
|
||||
# This is the CANDIDATE payload, not the deployed one. frank-large still
|
||||
# serves scripts/download-frank-large.sh's output; do not repoint
|
||||
# frank-large.env at MODEL_DIR until the gufo load test passes.
|
||||
set -euo pipefail
|
||||
|
||||
MODEL_DIR="/home/dank/models/Swift-1.5-Qwen3.8-27B-Uncensored-MTP-UD-Q8_K_XL"
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download unsloth/Qwen3.8-Flash-Next-GGUF:UD-IQ4_XS (the
|
||||
# frank-dragoon model) on Dankclaw.
|
||||
# frank-large model) on Dankclaw.
|
||||
#
|
||||
# bash scripts/download-frank-dragoon-unsloth.sh
|
||||
# bash scripts/download-frank-dragoon-unsloth.sh --verify-only
|
||||
# bash scripts/download-frank-large-unsloth.sh
|
||||
# bash scripts/download-frank-large-unsloth.sh --verify-only
|
||||
#
|
||||
# The UD-IQ4_XS GGUF ships as three llama.cpp split parts (HF caps uploads at
|
||||
# 50 GB). The parts ARE the canonical payload: the serve script points -m at
|
||||
@@ -112,4 +112,4 @@ if [[ "$mode" == "download" ]]; then
|
||||
[[ -e "$MODEL_DIR/vision/mmproj-F16.gguf" ]] || mv "$MODEL_DIR/mmproj-F16.gguf" "$MODEL_DIR/vision/"
|
||||
fi
|
||||
|
||||
echo "frank-dragoon payload complete: $MODEL_DIR"
|
||||
echo "frank-large payload complete: $MODEL_DIR"
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the frank-dragoon payload on Dankclaw:
|
||||
# Download the frank-large payload on Dankclaw:
|
||||
# - unsloth/Qwen3.8-27B-GGUF:UD-Q8_K_XL (the 27B dense main GGUF)
|
||||
# - z-lab/Qwen3.8-27B-DFlash2-GGUF:Q4_K_M (the DFlash2 drafter)
|
||||
#
|
||||
# bash scripts/download-frank-dragoon.sh
|
||||
# bash scripts/download-frank-dragoon.sh --verify-only
|
||||
# bash scripts/download-frank-large.sh
|
||||
# bash scripts/download-frank-large.sh --verify-only
|
||||
#
|
||||
# Integrity: every file is checked against its declared byte count and sha256
|
||||
# (the values below are the LFS oids from the Hub, 2026-09-21). An HF token
|
||||
@@ -86,4 +86,4 @@ if [[ "$mode" == "download" ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
echo "frank-dragoon payload complete: $MODEL_DIR"
|
||||
echo "frank-large payload complete: $MODEL_DIR"
|
||||
+8
-9
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# download-frank-archon-swift15.sh — payload fetch/verify for the planned
|
||||
# frank-archon swap to the Swift 1.5 finetune: ukisai/Swift1.5-Qwen3.8-Flash-Next
|
||||
# download-frank-primary-swift15.sh — payload fetch/verify for the planned
|
||||
# frank-primary swap to the Swift 1.5 finetune: ukisai/Swift1.5-Qwen3.8-Flash-Next
|
||||
# (BF16 source, 149 files, 360,037,355,038 bytes = 335.3 GiB: 131 safetensors
|
||||
# shards + weight-map index + tokenizer + vision preprocessing configs + chat
|
||||
# template + licenses). Key set verified identical to the official
|
||||
@@ -8,17 +8,16 @@
|
||||
# apply verbatim; the checkpoint ships BF16 with NO quantization_config.
|
||||
# Repo is UNGATED — no token needed (never put one on the command line).
|
||||
#
|
||||
# bash scripts/download-frank-archon-swift15.sh --fetch resume/complete
|
||||
# bash scripts/download-frank-archon-swift15.sh verify only
|
||||
# bash scripts/download-frank-primary-swift15.sh --fetch resume/complete
|
||||
# bash scripts/download-frank-primary-swift15.sh verify only
|
||||
#
|
||||
# Fetch uses the vendored scripts/hfget.sh (StillDeadcode's Apache-2.0
|
||||
# listing/resume downloader, curl+jq only): per-file size checks against
|
||||
# the Hub listing, `-C -` resume, pickle-format duplicates skipped.
|
||||
# Verify (default) is three independent agreements: every listed file
|
||||
# present with the listed size (catches truncation), the shard count
|
||||
# matching the weight-map index (catches omissions), and every shard's
|
||||
# sha256 equal to the Hub's LFS oid (catches substituted content).
|
||||
# Verify needs only the public API.
|
||||
# Verify (default) is the AGENTS.md law in three parts: every listed file
|
||||
# present with the listed size, the shard count matches the weight-map
|
||||
# index, and every shard's sha256 equals the Hub's LFS oid. Verify needs
|
||||
# only the public API.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="ukisai/Swift1.5-Qwen3.8-Flash-Next"
|
||||
@@ -1,22 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# download-frank-archon.sh — payload fetch/verify for frank-archon:
|
||||
# download-frank-primary.sh — payload fetch/verify for frank-primary:
|
||||
# the OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint (131 safetensors
|
||||
# shards + weight-map index + tokenizer + vision preprocessing configs,
|
||||
# ~172.8 GB). 2026-10-05: replaces the orcarouter uncensored payload of the
|
||||
# FreeToken era (that script lived here; its repo was gated and needed
|
||||
# HF_TOKEN — this one is UNGATED and needs no token at all).
|
||||
#
|
||||
# bash scripts/download-frank-archon.sh --fetch resume/complete
|
||||
# bash scripts/download-frank-archon.sh verify only
|
||||
# bash scripts/download-frank-primary.sh --fetch resume/complete
|
||||
# bash scripts/download-frank-primary.sh verify only
|
||||
#
|
||||
# Fetch uses the vendored scripts/hfget.sh (StillDeadcode's Apache-2.0
|
||||
# listing/resume downloader, curl+jq only): per-file size checks against
|
||||
# the Hub listing, `-C -` resume, pickle-format duplicates skipped.
|
||||
# Verify (default) is three independent agreements: every listed file
|
||||
# present with the listed size (catches truncation), the shard count
|
||||
# matching the weight-map index (catches omissions), and every shard's
|
||||
# sha256 equal to the Hub's LFS oid (catches substituted content).
|
||||
# Verify needs only the public API.
|
||||
# Verify (default) is the AGENTS.md law in three parts: every listed file
|
||||
# present with the listed size, the shard count matches the weight-map
|
||||
# index, and every shard's sha256 equals the Hub's LFS oid. Verify needs
|
||||
# only the public API.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="Qwen/Qwen3.8-Flash-Next-FP8"
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download the frank-zealot payload for llama.cpp on the RTX 3090:
|
||||
# Download the frank-secondary payload for llama.cpp on the RTX 3090:
|
||||
# HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive at **Q6_K** plus the
|
||||
# PUBLISHER's OWN vision projector (the BF16 mmproj — the model is natively
|
||||
# multimodal, so this file is the endpoint's eyes). Public repo — no auth
|
||||
@@ -21,7 +21,7 @@
|
||||
# replaced), Swift15-UncIQ3XXS/, MiMo-Q6K/, MiMo-Q5KM/, Ornith-Q8/.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/download-frank-zealot.sh [DEST_DIR] [--verify-only]
|
||||
# scripts/download-frank-secondary.sh [DEST_DIR] [--verify-only]
|
||||
set -euo pipefail
|
||||
|
||||
DEST=""
|
||||
@@ -1,230 +0,0 @@
|
||||
# frank-archon.env — radiance (StillDeadcode, Apache-2.0) serving
|
||||
# ukisai/Swift1.5-Qwen3.8-Flash-Next converted to GPTQ w4 (MODEL_FILE below,
|
||||
# built by scripts/convert-swift15-w4dn8.sh) on BOTH R9700s (TP=2),
|
||||
# dankserver :7770.
|
||||
#
|
||||
# SHAPE (2026-10-11): 2 decode slots x 200,000 tokens with an UNQUANTIZED
|
||||
# (bf16) KV cache — the pool is sized for the pair, and the weights slab pays
|
||||
# for it (see the KV block). The fp8-native payload this file was written for
|
||||
# is the rollback target and keeps its own trio under the fp8-native runbook.
|
||||
#
|
||||
# 2026-10-05 cutover from FreeToken (PR #587 ROCm, one card, experts in
|
||||
# pinned host RAM; git tag frank-archon-freetoken-pr587-20261005 is the
|
||||
# pre-cutover state — its unit/env/serve trio and Dockerfile are all in the
|
||||
# tag and rebuildable). Before that: tcclaviger vLLM fork (tag
|
||||
# frank-archon-vllm-shapeG-20261003).
|
||||
#
|
||||
# WHY RADIANCE: the engine is C++/HIP with libr4d kernels built for
|
||||
# gfx1201, drives `podman run` directly from the digest-pinned
|
||||
# stilldeadcode/radiance image (287 MiB; host needs only the amdgpu driver),
|
||||
# and its qwen4exp plugin reads the modelopt FP8 encoding NATIVELY
|
||||
# (`X.weight_scale_inv` is the scale plane — checkpoint.h). The PLE n-gram
|
||||
# table (51B params, 47.7 GiB) is served from pinned host RAM via io_uring
|
||||
# reads — Frank's call: "Native FP8. Experts from ram. Pin the PLE to ram."
|
||||
# The 4-bit-codebook publisher shape (qwen3.8-next-flash-fp8-iq4r-moe.rad,
|
||||
# --host-pool-mib 12288) remains the documented fallback if the native
|
||||
# container proves unbootable.
|
||||
#
|
||||
# PAYLOAD: /home/dank/models/rad/qwen4exp-fp8-native.rad — a rad-convert
|
||||
# NO-RECIPE pure copy of the official 131-shard checkpoint (every weight
|
||||
# keeps its block-FP8 encoding; ~161 GiB). The convert runs against a
|
||||
# staging dir whose config.json drops `quantization_config`: the descriptor
|
||||
# string is only the plugin SELECTION key (spec §2.4 — exact match, no
|
||||
# fallback), and no shipped plugin claims (qwen4exp, "fp8"); the shipped
|
||||
# qwen4exp plugin claims (qwen4exp, "") which is what recipe-built
|
||||
# containers carry. Per-weight encodings are data, not the descriptor —
|
||||
# removing the key changes nothing about what the weights hold. Built by
|
||||
# scripts/build-frank-archon-rad.sh (staging + convert + rad-info).
|
||||
#
|
||||
# SERVING FACTS (docs/GUIDE.md, docs/DOCKER.md, compose/flashnext.yaml):
|
||||
# - The HTTP listener binds AFTER the full load (main.cpp: engine.start()
|
||||
# then engine.serve()) — during load the port refuses connections, so a
|
||||
# bare curl /health gate is honest. /health == /ping: 200 JSON while
|
||||
# serving, 503 only while stopping (core/server/admin.cpp:385).
|
||||
# - Reasoning: --reasoning-format defaults to auto, emitting thinking in
|
||||
# `reasoning_content` (config.cpp). Tool calls: the qwen3.8 dialect, from
|
||||
# the chat format. Both need no parser flags.
|
||||
# - Sampling: the OpenAI endpoint accepts temperature, top_p, top_k, min_p,
|
||||
# typical_p, presence_penalty, frequency_penalty, repetition_penalty,
|
||||
# repeat_last_n, chat_template_kwargs, reasoning_effort,
|
||||
# enable_thinking, preserve_thinking (core/server/oai.cpp:194-205) —
|
||||
# models.yml's extraBody set is fully inside it. Unknown params are
|
||||
# refused (400), which is why nothing unverified rides the wire.
|
||||
# - MTP: the official checkpoint carries the mtp.* head (3,101 tensors,
|
||||
# mtp_num_hidden_layers=1) and the plugin probes draft depth 3 — the
|
||||
# publisher's throughput peak for this model. Stated explicitly with
|
||||
# --num-speculative-tokens 3.
|
||||
# - The fused TP all-reduce is declared over max-num-seqs x
|
||||
# (1 + --num-speculative-tokens) rows; 2 x 3 = 6 rows, under the 25-row
|
||||
# exactness bound of the wht6 wire.
|
||||
# Engine pin: Codeberg tag v1.0.13 (commit 53282fa), Docker Hub tag 1.0.13,
|
||||
# index digest below. REJECTED UPGRADE: 1.2.0 (sha256:665775ed1d90de6dbb
|
||||
# 25558e07fc60d20328f1346f96f3c6e00845eb763f01fa, tag 1.2.0, commit
|
||||
# e95d397) was benched 2026-10-06 on this exact shape (fp8-native rad,
|
||||
# vram pair, spec 2, seqs 3, 43.6k greedy probe): decode 29.21 t/s and
|
||||
# draft acceptance 0.7123 vs the 1.0.13 control's 30.08 and 0.7549.
|
||||
# Acceptance on the greedy harness is deterministic (the control
|
||||
# reproduced the recorded 0.755 exactly), so the drop is a real numerics
|
||||
# change — the 1.2.0 MoE weight-rotation change (92b8aaf) is the named
|
||||
# suspect; decode loss tracks the acceptance ratio (0.943 vs 0.945).
|
||||
# The vram-pair translation itself is exact (1.2.0 boot ledger took
|
||||
# vram_weights 25.46 GiB / vram_kv 3.46 GiB per card byte-identical to
|
||||
# the 1.0.13 ledger at EVC 0.82). Re-test 1.2.0+ when the rotation change
|
||||
# is untangled; 1.2.0 also REMOVED --expert-vs-cache-ratio in favour of
|
||||
# the --vram-weights-mib / --vram-kv-mib pair (core/config.cpp) — this
|
||||
# file already uses the pair, which BOTH pins accept (rollback symmetry).
|
||||
FRANK_ARCHON_IMAGE=stilldeadcode/radiance@sha256:4c3f484564bb2872784e46b0a59cbd71becc9c1e746e301a3596ffae253c322e
|
||||
# The .rad container FILE (mounted under /models) and the state dir for the
|
||||
# prefix-cache disk tier (mounted rw at /data). The model dir holds ONLY
|
||||
# .rad files; the raw HF download and its staging dir live beside it and
|
||||
# are never mounted.
|
||||
FRANK_ARCHON_MODEL_DIR=/home/dank/models/rad
|
||||
FRANK_ARCHON_MODEL_FILE=qwen4exp-swift15-w4dn8-i8-hc8m.rad
|
||||
FRANK_ARCHON_STATE_DIR=/home/dank/radiance-state
|
||||
# The state dir's tune/ subdirectory holds rad-tune's kernel-selection
|
||||
# cache (the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune
|
||||
# at boot; $RADIANCE_HOME defaults to /opt/radiance, in the image, so the
|
||||
# cache must ride a host mount — serve script mounts it, see the runbook's
|
||||
# "Tuning levers").
|
||||
# Container identity is the SERVICE name (Frank's call, 2026-10-05): one
|
||||
# name across systemctl and podman ps. The runtime changes; the name does
|
||||
# not. The legacy list retires every predecessor identity that could hold
|
||||
# the GPUs or the port.
|
||||
FRANK_ARCHON_CONTAINER=frank-archon
|
||||
FRANK_ARCHON_LEGACY_CONTAINERS="frank-archon-qwen frank-archon-r9v qwen-fp8 glm-probe"
|
||||
# Served model id — Protoss name (wire rename 2026-10-09). The container,
|
||||
# unit, scripts and this file keep the frank-archon name above; only the
|
||||
# alias on the wire moved. Radiance validates the requested model name, so
|
||||
# every client (harness, bench scripts, probes) MUST send this value.
|
||||
FRANK_ARCHON_SERVED_NAME=frank-archon
|
||||
FRANK_ARCHON_PORT=7770
|
||||
# Frank's chat-template call: froggeric's fixed Qwen3.8 template (the
|
||||
# repo's root chat_template.jinja, vendored here; the checkpoint's own
|
||||
# template is baked into the .rad by rad-convert and this overrides it).
|
||||
# Radiance derives the reasoning/tool-call reply format from it.
|
||||
FRANK_ARCHON_TEMPLATE=/home/dank/serverAppConfigs/scripts/qwen38-froggeric-v22.5.jinja
|
||||
|
||||
# --- engine shape (the boot ledger arbitrates every number below) ---------
|
||||
# Tensor parallel: BOTH R9700s. The board swap (2026-09-20, ProArt B850
|
||||
# x8/x8 on root ports) made collectives legal on this host; radiance's TP
|
||||
# wire is libr4d's own, not RCCL.
|
||||
FRANK_ARCHON_TP=2
|
||||
# Lossy WHT 6-bit wire for large all-reduces (publisher's production choice:
|
||||
# +5.6-6.3% prefill, ~-1.3% 8-stream decode at TP2). min-kb stays the
|
||||
# engine default.
|
||||
FRANK_ARCHON_TP_WIRE=wht6
|
||||
# Decode slots: TWO, so the served window is a CAPACITY CONTRACT — two
|
||||
# concurrent conversations of --max-model-len fit the KV pool (see the KV
|
||||
# block below). Slot-count limits observed on other runtimes do NOT
|
||||
# transfer: a "this slot count cannot boot" refusal is usually that
|
||||
# runtime's compiler specializing shapes (e.g. a ConstraintViolationError
|
||||
# from shape specialization on query_start_loc), not a property of the
|
||||
# engine now running. This engine boots seqs 2, and seqs 2
|
||||
# vs 3 decode measurements land inside each other's noise floor. Side
|
||||
# benefit: the fused TP all-reduce is declared over seqs x (1 + spec) = 6
|
||||
# rows, under the 25-row bound at which the wht6 wire stays EXACT
|
||||
# (single-sequence decode and any step of <= 25 rows is bit-exact; only
|
||||
# busier steps are lossy).
|
||||
FRANK_ARCHON_MAX_SEQS=2
|
||||
# Served window. Native is 262,144; 200,000 is the publisher's production
|
||||
# value (compose) and what models.yml's contextWindow must quote — a longer
|
||||
# prompt is refused by the engine, never truncated. The engine treats
|
||||
# `kv group tokens >= max-model-len` as a HARD limit ("ONE request longer
|
||||
# than N tokens is accepted by the API and then refused as too large to ever
|
||||
# fit — this is a hard limit, not a backing ratio"), so the pool is sized
|
||||
# for seqs x maxlen, not for one sequence.
|
||||
FRANK_ARCHON_MAX_MODEL_LEN=200000
|
||||
FRANK_ARCHON_PLACEMENT=expert_tiered
|
||||
# Pinned host pool for the expert rows that do not fit VRAM. vram_weights and
|
||||
# this pool serve the per-rank resident set TOGETHER: at the fp8-KV pair
|
||||
# (26071/3543) the ledger showed the VRAM slab filled to 259 KiB of slack,
|
||||
# i.e. the pair was exactly sized. Cutting vram-weights by 3.8 GiB to pay for
|
||||
# the bf16 KV pool SPILLS ~3.8 GiB/rank here, so the pool moves with it —
|
||||
# under-sizing it is a boot failure (36864 refused the boot in the fp8 era).
|
||||
# The boot ledger's coverage line remains the referee, not this arithmetic.
|
||||
FRANK_ARCHON_HOST_POOL_MIB=44928
|
||||
# VRAM left unclaimed per card (publisher default 96 MiB).
|
||||
FRANK_ARCHON_GPU_HEADROOM_MIB=96
|
||||
# VRAM split of claimed memory per card, MiB (0 = derive from the card).
|
||||
# THE CEILING, measured on this host 2026-10-11: card 31,856 MiB, a boot
|
||||
# consumes 1,710 MiB OUTSIDE both budgets (driver context + activation arena
|
||||
# 990 + hardware queues 100 + headroom 96 + step staging), and the fp8 pair
|
||||
# settled at 1,379 MiB free — so `vram_weights + vram_kv <= 29,120 MiB` keeps
|
||||
# >= 1 GiB free. The pair below is exactly that sum.
|
||||
# THE PRICE, and it is paid twice: on this engine idle KV VRAM is ALSO the
|
||||
# expert cache's spillover (`mover: 2275 slots can be lent out of the KV
|
||||
# cache (2.69 GiB)`, `arena: lends 740 MiB while steps are <= 256 tokens`),
|
||||
# so a bigger KV pool steals weight residency from the slab AND from the
|
||||
# lendable cache. Priors on this card/model: stealing 2.14 GiB of weights for
|
||||
# a bigger pool (MAX_MODEL_LEN 262144) cost 2.5% decode; the EVC ladder's
|
||||
# other direction (0.82 -> 0.84, +2.5 GiB weights) cost 8.9%. Expect -3..-8%
|
||||
# vs the fp8 control (136 t/s median at 44.5k depth, acceptance 0.7246,
|
||||
# ~190 t/s on a live 512-token probe); re-tune from a boot ledger + probe,
|
||||
# never from that estimate.
|
||||
FRANK_ARCHON_VRAM_WEIGHTS_MIB=22208
|
||||
FRANK_ARCHON_VRAM_KV_MIB=6912
|
||||
# KV cache UNQUANTIZED (bf16). This engine's flag takes ONLY bf16|fp8 — there
|
||||
# is ONE kv_dtype for the whole cache, so "exact K, quantized V" (llama.cpp's
|
||||
# --cache-type-k/--cache-type-v pair, which is what frank-zealot runs) is
|
||||
# UNSERVABLE here: the rung below bf16 is fp8 on both halves.
|
||||
# Measured per-rank cost, read out of the boot ledger's group table (fp8 boot
|
||||
# 2026-10-08, bf16 boot 2026-10-11):
|
||||
# kv_attn 13 layers, 4-token blocks, page 26.00 KiB (fp8) -> 52.00 KiB
|
||||
# kv_qsa_bkey 13 layers, 4-token blocks, page 3.25 KiB at BOTH dtypes —
|
||||
# the QSA block key is an index, not attention state, and does
|
||||
# NOT follow --kv-cache-dtype
|
||||
# => 7.3125 KiB/token fp8, 13.8125 KiB/token bf16: bf16 costs 1.89x fp8,
|
||||
# not 2x, precisely because the bkey plane is dtype-fixed.
|
||||
# The dtype-INDEPENDENT terms also live in the pool: kv_gdn_state 54 MiB x 2
|
||||
# copies per sequence (+ conv/tail/ple ~= 110 MiB a sequence) and the prefix
|
||||
# checkpoint region 8 x 55.99 = 448 MiB. So:
|
||||
# pool_mib >= tokens x 13.8125 KiB + 110 x seqs + 448
|
||||
# 6912 -> (6912 - 220 - 448) x 1024 / 13.8125 = 462,946 tokens; the engine
|
||||
# reported 462,752 (0.04% page rounding) — 2 x 200,000 with 62k of slack,
|
||||
# and the ledger says it outright: "backs 2 sequences all at 200000 tokens
|
||||
# at once, which is the worst case".
|
||||
# (the fp8 pool of 3543 gave 386,904 — it never held 2 x 200k, it held 1.93
|
||||
# sessions, and a third long conversation preempted to the host/disk tiers).
|
||||
# Secondary cost: the prefix-cache entry grows with the attention plane
|
||||
# (one entry 58.50 KiB at fp8 -> 110.50 KiB at bf16, measured on the two boot
|
||||
# ledgers' `kv tier transfer` lines), so the host tier below doubles to keep
|
||||
# the same number of parked conversations, out of RAM the host has
|
||||
# (104 GiB available with the service loaded).
|
||||
FRANK_ARCHON_KV_DTYPE=bf16
|
||||
# Spec decode: the checkpoint's own MTP head at the publisher's depth-3
|
||||
# operating point (probe() returns 3 when mtp_num_hidden_layers > 0; the
|
||||
# flag states it so the ledger records the intent).
|
||||
FRANK_ARCHON_SPEC_TOKENS=2
|
||||
FRANK_ARCHON_MAX_BATCHED_TOKENS=2048
|
||||
# Prefix cache tiers (publisher pattern; disk tier sized for 2 slots).
|
||||
# The host tier is sized in ENTRIES, and one entry doubles with the cache
|
||||
# dtype (58.50 KiB at fp8 -> 117 KiB at bf16, per the ledger's `kv tier
|
||||
# transfer` line): 4096 MiB at bf16 parks ~1.4 sessions of 200k where it
|
||||
# parked ~2.9 at fp8. Doubled to keep the capacity, at RAM the host has
|
||||
# (104 GiB available with the service loaded).
|
||||
FRANK_ARCHON_PREFIX_HOST_MIB=8192
|
||||
FRANK_ARCHON_PREFIX_DISK_MIB=65536
|
||||
# The PLE n-gram table (47.7 GiB) serves from pinned host RAM the cards
|
||||
# read. `ram` REFUSES the start if the memlock limit is too small — a
|
||||
# named refusal beats a mid-load allocator failure — and the unit's
|
||||
# LimitMEMLOCK=infinity is the other half of the arrangement.
|
||||
FRANK_ARCHON_NGRAM_PLACEMENT=ram
|
||||
# TTM preflight: the pinned ledger (2 x 44 GiB pools + 47.7 GiB PLE + 8 GiB
|
||||
# prefix host = ~147 GiB) rides TTM's shared pages, capped by ttm's
|
||||
# pages_limit (the current kernel exposes NO pages_max; the cap function
|
||||
# takes the min over the knobs that exist). Below the plan the load dies
|
||||
# mid-pin with an allocator error instead of this named refusal — raise the
|
||||
# knob first (runtime sysfs write; kernel cmdline fallback + one reboot only
|
||||
# if the file is read-only), see the runbook. Cap raised 2026-10-05 to
|
||||
# 52,428,800 pages = 200 GiB, which still clears this floor.
|
||||
FRANK_ARCHON_TTM_MIN_MIB=150000
|
||||
# Escape hatch for boot-ladder experiments. Unset -> no extra argv. Keep
|
||||
# multi-flag values as one string; the serve script expands it unquoted on
|
||||
# purpose.
|
||||
FRANK_ARCHON_EXTRA_ARGS=
|
||||
# The .rad's minimum size in MiB (deployer gate; a truncated convert is a
|
||||
# guaranteed long-timeout boot instead of a fast failure). The converted
|
||||
# container measured 177,109 MiB (1,857,127,997,440 B = 173.0 GiB; the
|
||||
# plan-only estimate 172.76 GiB covers the data planes but not the metadata
|
||||
# tail) - floor sits just under the payload.
|
||||
FRANK_ARCHON_RAD_MIN_MIB=110000
|
||||
@@ -1,11 +1,11 @@
|
||||
# frank-dragoon.env — tunables for the frank-dragoon endpoint on Dankclaw's
|
||||
# frank-large.env — tunables for the frank-large endpoint on Dankclaw's
|
||||
# Strix Halo iGPU (gfx1151), host port 7770.
|
||||
#
|
||||
# RUNTIME (2026-10-06, Frank): halogen-flash-server 0.16.2 — the
|
||||
# closed-source C++/HIP engine (image ghcr.io/peonist-ai/halogen-flash-server,
|
||||
# digest-pinned below) serving the Quat3rnion ABLITERATED Swift1.5 finetune
|
||||
# of peonist's Qwen3.8-Flash-Next v2 checkpoint, as a podman container under
|
||||
# a root unit, driven by scripts/serve-frank-dragoon.sh. This CUTS OVER the
|
||||
# a root unit, driven by scripts/serve-frank-large.sh. This CUTS OVER the
|
||||
# 2026-10-05 halogen 27B-dense shape (censored base, DFlash2, 31.7 t/s):
|
||||
# rollback = revert the swap commit's trio; the 27B payload stays on disk at
|
||||
# /home/dank/models/halogen-qwen3.8-27b/.
|
||||
@@ -36,24 +36,23 @@
|
||||
# target). Worst-case request: prompt + budget <= 217088 − 4096 + 3 %
|
||||
# est-error ≈ 219k <= 262144 — the 45k gap between harness window and
|
||||
# engine ceiling absorbs the estimate error with ~7x margin (robust to a
|
||||
# 10 % under-count). Compaction rides on frank-zealot: input ≈
|
||||
# 10 % under-count). Compaction rides on frank-secondary: input ≈
|
||||
# threshold − keepRecent(20k) ≈ 170k measured, inside its 200,704 slot.
|
||||
# HALOGEN_KV_POOL_POSITIONS=524288 = KV_SLOTS(2) x CTX — the explicit
|
||||
# form of the engine's default, so the seam test can pin the product.
|
||||
# KV costs ~28 KiB/position (FLAGS.md) => 524,288 x 28 KiB = 14.0 GiB.
|
||||
# HALOGEN_KV_SLOTS=2 restores the w4b-era shape on this box: the MTP
|
||||
# drafter is NOT slot-locked on this engine. A "slots > 1 disables the
|
||||
# drafter" restriction is a property of the engine generation that
|
||||
# imposed it, not of speculation itself — batching and speculation
|
||||
# coexist here, and the card's own quickstart ships 2 slots WITH the
|
||||
# head — so the second slot is concurrent capacity, not a speculation tax.
|
||||
# drafter is NOT slot-locked on this engine (batching and speculation
|
||||
# coexist — DFlash-era slots>1 law does NOT carry over; the card's own
|
||||
# quickstart ships 2 slots WITH the head), so the second slot is
|
||||
# concurrent capacity, not a speculation tax.
|
||||
# Memory ledger against the box's 121.2 GiB unified (the n-gram table is
|
||||
# mmap'd page cache beside the checkpoint, not a pinned charge — the
|
||||
# card's own quickstart arithmetic does not bill it):
|
||||
# weights ~62 GiB resident + KV 14.0 + arena (HALOGEN_MAX_TOK=8192 =>
|
||||
# ~8 GiB per FLAGS.md arena formula 3.8+8.4*(MT/16384)) + vision ~2
|
||||
# = ~86 GiB engine-side; HALOGEN_HOST_RESERVE_GIB=20 keeps the host
|
||||
# honest; baseline co-tenancy (frank-zealot, ComfyUI, Caddy/observ)
|
||||
# honest; baseline co-tenancy (frank-secondary, ComfyUI, Caddy/observ)
|
||||
# ~10-15. ~15 GiB margin. The boot ledger is the referee — read it
|
||||
# after deploy and re-size if the engine prints a clamp.
|
||||
# HALOGEN_SPEC_ADAPT=0 is MANDATORY (card): the default adaptive policy
|
||||
@@ -89,51 +88,50 @@
|
||||
# before. Kernel needs CONFIG_HSA_AMD_SVM (CachyOS ships it) and
|
||||
# amdgpu.noretry=0 (check at deploy).
|
||||
|
||||
# Image: digest-pinned, not tag-pinned — a tag can be re-pointed to a
|
||||
# build that was never qualified here, while the digest pins exactly the
|
||||
# build that was tested. 0.16.2 = the version the model card names as its
|
||||
# quickstart pin; the RepoDigest was captured at a pull of that tag.
|
||||
FRANK_DRAGOON_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
|
||||
# Image: digest-pinned (fetch law: tag pulls silently re-point; the digest
|
||||
# is what was qualified). 0.16.2 = the version the model card names as its
|
||||
# quickstart pin; RepoDigest captured at pull on 2026-10-06.
|
||||
FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
|
||||
# Container identity: kept from the 27B era (unit ExecStop names it).
|
||||
FRANK_DRAGOON_CONTAINER=frank-dragoon-halogen
|
||||
FRANK_LARGE_CONTAINER=frank-large-halogen
|
||||
# Old quadlet-era container identity; the serve script clears stale names.
|
||||
FRANK_DRAGOON_LEGACY_CONTAINERS=frank-dragoon
|
||||
FRANK_LARGE_LEGACY_CONTAINERS=frank-large
|
||||
# host:container — the frontend listens on 8731 inside (HALOGEN_API_PORT
|
||||
# default); the engine port (8730) is never published.
|
||||
FRANK_DRAGOON_PUBLISH=7770:8731
|
||||
FRANK_LARGE_PUBLISH=7770:8731
|
||||
|
||||
FRANK_DRAGOON_MODEL_DIR=/home/dank/models/halogen-flashnext-v2
|
||||
FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-flashnext-v2
|
||||
# The checkpoint: Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated
|
||||
# qwen38-flash-next-v2-swift15-abliterated.hgn — compacted r2 of the card's
|
||||
# append-only original (byte-identical live tensors, padding dropped:
|
||||
# 66,687,678,464 B vs 89,775,868,032 B). LFS sha256 verified at fetch.
|
||||
FRANK_DRAGOON_CHECKPOINT=qwen38-flash-next-v2-swift15-abliterated.hgn
|
||||
FRANK_DRAGOON_CHECKPOINT_BYTES=66687678464
|
||||
FRANK_DRAGOON_CHECKPOINT_SHA256=9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037
|
||||
FRANK_LARGE_CHECKPOINT=qwen38-flash-next-v2-swift15-abliterated.hgn
|
||||
FRANK_LARGE_CHECKPOINT_BYTES=66687678464
|
||||
FRANK_LARGE_CHECKPOINT_SHA256=9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037
|
||||
# The n-gram lookup table: REQUIRED (the engine reads it beside the
|
||||
# checkpoint; the card's quickstart names it explicitly). From
|
||||
# peonist-ai/halogen-qwen3.8-flash-next @ 5cc17cea.
|
||||
FRANK_DRAGOON_NGRAM_TABLE=qwen38-flash-next-ngram.hgn
|
||||
FRANK_DRAGOON_NGRAM_BYTES=51200246144
|
||||
FRANK_DRAGOON_NGRAM_SHA256=3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe
|
||||
FRANK_LARGE_NGRAM_TABLE=qwen38-flash-next-ngram.hgn
|
||||
FRANK_LARGE_NGRAM_BYTES=51200246144
|
||||
FRANK_LARGE_NGRAM_SHA256=3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe
|
||||
# The vision tower: OPTIONAL on the engine, ENABLED here (image input).
|
||||
# From peonist-ai/halogen-qwen3.8-flash-next @ e053f488.
|
||||
FRANK_DRAGOON_VISION_TOWER=qwen38-flash-next-vision.hgn
|
||||
FRANK_DRAGOON_VISION_BYTES=897916416
|
||||
FRANK_DRAGOON_VISION_SHA256=d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10
|
||||
# The tokenizer is a FLAT DIRECTORY, $FRANK_DRAGOON_MODEL_DIR/tokenizer,
|
||||
FRANK_LARGE_VISION_TOWER=qwen38-flash-next-vision.hgn
|
||||
FRANK_LARGE_VISION_BYTES=897916416
|
||||
FRANK_LARGE_VISION_SHA256=d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10
|
||||
# The tokenizer is a FLAT DIRECTORY, $FRANK_LARGE_MODEL_DIR/tokenizer,
|
||||
# mounted whole at /tokenizer. The two repos ship the same one; fetched
|
||||
# fresh from peonist @ e053f488 (tokenizer.json is LFS, byte-identical to
|
||||
# the 27B-era copy; the rest are plain git blobs — size-checked only). The
|
||||
# serve gate and the deployer refuse a missing file or ANY symlink: an HF
|
||||
# cache snapshot symlinks each file into blobs/, and those links dangle
|
||||
# inside the container mount.
|
||||
FRANK_DRAGOON_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json"
|
||||
FRANK_LARGE_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json"
|
||||
|
||||
# --- engine flags (docs/FLAGS.md of the flash-server repo; anything unset
|
||||
# stays at the image default, which is what was qualified) ------------------
|
||||
# Served model id — Protoss name (wire rename 2026-10-09); the service, unit,
|
||||
# scripts and this file keep the frank-dragoon name. OpenWebUI api_configs
|
||||
# scripts and this file keep the frank-large name. OpenWebUI api_configs
|
||||
# model_ids for the :7770 backend MUST name this value.
|
||||
HALOGEN_MODEL_ID=frank-dragoon
|
||||
HALOGEN_CTX=262144
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=halogen-flash-server container (Quat3rnion abliterated Swift1.5 of Qwen3.8-Flash-Next v2, MTP spec decode, 2 slots x 200192, vision ON), root systemd unit (dankclaw, frank-dragoon, Strix Halo iGPU)
|
||||
Description=halogen-flash-server container (Quat3rnion abliterated Swift1.5 of Qwen3.8-Flash-Next v2, MTP spec decode, 2 slots x 200192, vision ON), root systemd unit (dankclaw, frank-large, Strix Halo iGPU)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
@@ -11,11 +11,11 @@ Group=dank
|
||||
# RLIMIT smaller than the pinned set cannot fail the load. Parity with the
|
||||
# other LLM units on this fleet.
|
||||
LimitMEMLOCK=infinity
|
||||
Environment="FRANK_DRAGOON_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-dragoon.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-dragoon.sh
|
||||
Environment="FRANK_LARGE_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-large.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-large.sh
|
||||
# systemd stop alone cannot stop the container child; stop it explicitly
|
||||
# (frank-archon pattern). The engine drains on SIGTERM.
|
||||
ExecStop=/usr/bin/podman stop -t 60 frank-dragoon-halogen
|
||||
# (frank-primary pattern). The engine drains on SIGTERM.
|
||||
ExecStop=/usr/bin/podman stop -t 60 frank-large-halogen
|
||||
TimeoutStartSec=1200
|
||||
TimeoutStopSec=120
|
||||
# always, not on-failure: an engine that exits 0 after an internal fault
|
||||
@@ -23,7 +23,7 @@ TimeoutStopSec=120
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
# The engine pins ~62 GiB of v2 weights and keeps a multi-GiB working set
|
||||
# on a box that also hosts frank-zealot, ComfyUI and the Caddy/observability
|
||||
# on a box that also hosts frank-secondary, ComfyUI and the Caddy/observability
|
||||
# stack; make it a late OOM candidate, not the first.
|
||||
OOMScoreAdjust=-500
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure per-block / per-expert expert-routing activity on live frank-archon.
|
||||
"""Measure per-block / per-expert expert-routing activity on live frank-primary.
|
||||
|
||||
Method: AMD IBS op sampling (data addresses, `perf record -e ibs_op// --data`)
|
||||
on the llama-server process while concurrent generations of mixed content run.
|
||||
@@ -11,13 +11,13 @@ Tensor extents are DERIVED from consecutive GGUF header offsets — no quant
|
||||
type-size table (those are easy to get subtly wrong; a wrong per-element size
|
||||
silently under-covers each tensor and dumps tail samples into a junk bucket).
|
||||
|
||||
What this instrument settles: per-BLOCK expert read volume is FLAT across
|
||||
all CPU-resident blocks, because MoE top-k routing fires every block on
|
||||
every token — there are no "most active layers" at `-ot` granularity. The
|
||||
real skew is per-EXPERT: a small fraction of (block,expert) cells carries
|
||||
most of the samples, and tensor-level overrides cannot reach that level.
|
||||
Any fixed number of GPU-resident blocks is therefore equivalent; COUNT is
|
||||
the only lever.
|
||||
Fleet law this instrument established (2026-09-26, 75 s window, 4 mixed
|
||||
streams, 2.7M expert-tensor samples): per-BLOCK read volume is FLAT
|
||||
(0.93x-1.08x of mean across all 40 CPU-resident blocks) because MoE top-k
|
||||
routing fires every block every token — there are no "most active layers" at
|
||||
`-ot` granularity. The real skew is per-EXPERT (top 10% of (block,expert)
|
||||
cells carry ~40% of samples), which tensor-level overrides cannot reach.
|
||||
Any 8 GPU-resident blocks are therefore equivalent; COUNT is the only lever.
|
||||
|
||||
Usage (needs passwordless sudo for perf only; model files must be readable):
|
||||
fp-expert-activity.py <pid> [duration_s] # dump + sample + analyze
|
||||
@@ -0,0 +1,168 @@
|
||||
# frank-primary.env — radiance (StillDeadcode, Apache-2.0) serving the
|
||||
# OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint at NATIVE precision
|
||||
# (modelopt block-FP8 throughout, no requantisation) on BOTH R9700s (TP=2),
|
||||
# dankserver :7770.
|
||||
#
|
||||
# 2026-10-05 cutover from FreeToken (PR #587 ROCm, one card, experts in
|
||||
# pinned host RAM; git tag frank-primary-freetoken-pr587-20261005 is the
|
||||
# pre-cutover state — its unit/env/serve trio and Dockerfile are all in the
|
||||
# tag and rebuildable). Before that: tcclaviger vLLM fork (tag
|
||||
# frank-primary-vllm-shapeG-20261003).
|
||||
#
|
||||
# WHY RADIANCE: the engine is C++/HIP with libr4d kernels built for
|
||||
# gfx1201, drives `podman run` directly from the digest-pinned
|
||||
# stilldeadcode/radiance image (287 MiB; host needs only the amdgpu driver),
|
||||
# and its qwen4exp plugin reads the modelopt FP8 encoding NATIVELY
|
||||
# (`X.weight_scale_inv` is the scale plane — checkpoint.h). The PLE n-gram
|
||||
# table (51B params, 47.7 GiB) is served from pinned host RAM via io_uring
|
||||
# reads — Frank's call: "Native FP8. Experts from ram. Pin the PLE to ram."
|
||||
# The 4-bit-codebook publisher shape (qwen3.8-next-flash-fp8-iq4r-moe.rad,
|
||||
# --host-pool-mib 12288) remains the documented fallback if the native
|
||||
# container proves unbootable.
|
||||
#
|
||||
# PAYLOAD: /home/dank/models/rad/qwen4exp-fp8-native.rad — a rad-convert
|
||||
# NO-RECIPE pure copy of the official 131-shard checkpoint (every weight
|
||||
# keeps its block-FP8 encoding; ~161 GiB). The convert runs against a
|
||||
# staging dir whose config.json drops `quantization_config`: the descriptor
|
||||
# string is only the plugin SELECTION key (spec §2.4 — exact match, no
|
||||
# fallback), and no shipped plugin claims (qwen4exp, "fp8"); the shipped
|
||||
# qwen4exp plugin claims (qwen4exp, "") which is what recipe-built
|
||||
# containers carry. Per-weight encodings are data, not the descriptor —
|
||||
# removing the key changes nothing about what the weights hold. Built by
|
||||
# scripts/build-frank-primary-rad.sh (staging + convert + rad-info).
|
||||
#
|
||||
# SERVING FACTS (docs/GUIDE.md, docs/DOCKER.md, compose/flashnext.yaml):
|
||||
# - The HTTP listener binds AFTER the full load (main.cpp: engine.start()
|
||||
# then engine.serve()) — during load the port refuses connections, so a
|
||||
# bare curl /health gate is honest. /health == /ping: 200 JSON while
|
||||
# serving, 503 only while stopping (core/server/admin.cpp:385).
|
||||
# - Reasoning: --reasoning-format defaults to auto, emitting thinking in
|
||||
# `reasoning_content` (config.cpp). Tool calls: the qwen3.8 dialect, from
|
||||
# the chat format. Both need no parser flags.
|
||||
# - Sampling: the OpenAI endpoint accepts temperature, top_p, top_k, min_p,
|
||||
# typical_p, presence_penalty, frequency_penalty, repetition_penalty,
|
||||
# repeat_last_n, chat_template_kwargs, reasoning_effort,
|
||||
# enable_thinking, preserve_thinking (core/server/oai.cpp:194-205) —
|
||||
# models.yml's extraBody set is fully inside it. Unknown params are
|
||||
# refused (400), which is why nothing unverified rides the wire.
|
||||
# - MTP: the official checkpoint carries the mtp.* head (3,101 tensors,
|
||||
# mtp_num_hidden_layers=1) and the plugin probes draft depth 3 — the
|
||||
# publisher's throughput peak for this model. Stated explicitly with
|
||||
# --num-speculative-tokens 3.
|
||||
# - The fused TP all-reduce is declared over max-num-seqs x
|
||||
# (1 + --num-speculative-tokens) rows; 3 x 3 = 9 rows fits either wire.
|
||||
# Engine pin: Codeberg tag v1.0.13 (commit 53282fa), Docker Hub tag 1.0.13,
|
||||
# index digest below. REJECTED UPGRADE: 1.2.0 (sha256:665775ed1d90de6dbb
|
||||
# 25558e07fc60d20328f1346f96f3c6e00845eb763f01fa, tag 1.2.0, commit
|
||||
# e95d397) was benched 2026-10-06 on this exact shape (fp8-native rad,
|
||||
# vram pair, spec 2, seqs 3, 43.6k greedy probe): decode 29.21 t/s and
|
||||
# draft acceptance 0.7123 vs the 1.0.13 control's 30.08 and 0.7549.
|
||||
# Acceptance on the greedy harness is deterministic (the control
|
||||
# reproduced the recorded 0.755 exactly), so the drop is a real numerics
|
||||
# change — the 1.2.0 MoE weight-rotation change (92b8aaf) is the named
|
||||
# suspect; decode loss tracks the acceptance ratio (0.943 vs 0.945).
|
||||
# The vram-pair translation itself is exact (1.2.0 boot ledger took
|
||||
# vram_weights 25.46 GiB / vram_kv 3.46 GiB per card byte-identical to
|
||||
# the 1.0.13 ledger at EVC 0.82). Re-test 1.2.0+ when the rotation change
|
||||
# is untangled; 1.2.0 also REMOVED --expert-vs-cache-ratio in favour of
|
||||
# the --vram-weights-mib / --vram-kv-mib pair (core/config.cpp) — this
|
||||
# file already uses the pair, which BOTH pins accept (rollback symmetry).
|
||||
FRANK_PRIMARY_IMAGE=stilldeadcode/radiance@sha256:4c3f484564bb2872784e46b0a59cbd71becc9c1e746e301a3596ffae253c322e
|
||||
# The .rad container FILE (mounted under /models) and the state dir for the
|
||||
# prefix-cache disk tier (mounted rw at /data). The model dir holds ONLY
|
||||
# .rad files; the raw HF download and its staging dir live beside it and
|
||||
# are never mounted.
|
||||
FRANK_PRIMARY_MODEL_DIR=/home/dank/models/rad
|
||||
FRANK_PRIMARY_MODEL_FILE=qwen4exp-swift15-w4dn8-i8-hc8m.rad
|
||||
FRANK_PRIMARY_STATE_DIR=/home/dank/radiance-state
|
||||
# The state dir's tune/ subdirectory holds rad-tune's kernel-selection
|
||||
# cache (the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune
|
||||
# at boot; $RADIANCE_HOME defaults to /opt/radiance, in the image, so the
|
||||
# cache must ride a host mount — serve script mounts it, see the runbook's
|
||||
# "Tuning levers").
|
||||
# Container identity is the SERVICE name (Frank's call, 2026-10-05): one
|
||||
# name across systemctl and podman ps. The runtime changes; the name does
|
||||
# not. The legacy list retires every predecessor identity that could hold
|
||||
# the GPUs or the port.
|
||||
FRANK_PRIMARY_CONTAINER=frank-primary
|
||||
FRANK_PRIMARY_LEGACY_CONTAINERS="frank-primary-qwen frank-primary-r9v qwen-fp8 glm-probe"
|
||||
# Served model id — Protoss name (wire rename 2026-10-09). The container,
|
||||
# unit, scripts and this file keep the frank-primary name above; only the
|
||||
# alias on the wire moved. Radiance validates the requested model name, so
|
||||
# every client (harness, bench scripts, probes) MUST send this value.
|
||||
FRANK_PRIMARY_SERVED_NAME=frank-archon
|
||||
FRANK_PRIMARY_PORT=7770
|
||||
# Frank's chat-template call: froggeric's fixed Qwen3.8 template (the
|
||||
# repo's root chat_template.jinja, vendored here; the checkpoint's own
|
||||
# template is baked into the .rad by rad-convert and this overrides it).
|
||||
# Radiance derives the reasoning/tool-call reply format from it.
|
||||
FRANK_PRIMARY_TEMPLATE=/home/dank/serverAppConfigs/scripts/qwen38-froggeric-v22.5.jinja
|
||||
|
||||
# --- engine shape (first-boot conservative; the boot ledger arbitrates) --
|
||||
# Tensor parallel: BOTH R9700s. The board swap (2026-09-20, ProArt B850
|
||||
# x8/x8 on root ports) made collectives legal on this host; radiance's TP
|
||||
# wire is libr4d's own, not RCCL.
|
||||
FRANK_PRIMARY_TP=2
|
||||
# Lossy WHT 6-bit wire for large all-reduces (publisher's production choice:
|
||||
# +5.6-6.3% prefill, ~-1.3% 8-stream decode at TP2). min-kb stays the
|
||||
# engine default.
|
||||
FRANK_PRIMARY_TP_WIRE=wht6
|
||||
# Decode slots. 2 is Frank's first-boot value ("use parallelism = 2 to
|
||||
# start"); raise only after a real-traffic look at slot occupancy and the
|
||||
# KV pool's headroom line. The recurrent-state cost scales with it (54
|
||||
# MiB/copy/card, 2 copies with speculation).
|
||||
FRANK_PRIMARY_MAX_SEQS=3
|
||||
# Served window. Native is 262,144; 200,000 is the publisher's production
|
||||
# value (compose) and what models.yml's contextWindow must quote — a longer
|
||||
# prompt is refused by the engine, never truncated.
|
||||
FRANK_PRIMARY_MAX_MODEL_LEN=200000
|
||||
FRANK_PRIMARY_PLACEMENT=expert_tiered
|
||||
# Pinned host pool for the expert rows that do not fit VRAM. Native FP8
|
||||
# experts are 112.8 GiB total (56.4 GiB per rank slice) vs the 4-bit
|
||||
# publisher shape's ~29 GiB — this pool is the cost of native precision.
|
||||
# The boot ledger prints the pool's coverage ("the pool backs N% of the
|
||||
# worst case ... M full-length sessions"); resize from that line, not from
|
||||
# arithmetic.
|
||||
FRANK_PRIMARY_HOST_POOL_MIB=40960
|
||||
# VRAM left unclaimed per card (publisher default 96 MiB).
|
||||
FRANK_PRIMARY_GPU_HEADROOM_MIB=96
|
||||
# VRAM split of claimed memory per card, MiB (0 = derive from the card).
|
||||
# Engine 1.2.0 REMOVED --expert-vs-cache-ratio; these two knobs replace
|
||||
# it. The values transcribe the live 1.0.13 boot ledger at ratio 0.82:
|
||||
# vram_weights 25.46 GiB, vram_kv 3.46 GiB per card -> 26071 / 3543.
|
||||
# Bracket arms scale the KV term linearly in (1-r) from the 0.82 anchor
|
||||
# with weights fixed: ratio 0.90 -> KV 1968, ratio 0.95 -> KV 984. The
|
||||
# boot ledger arbitrates; re-derive after any payload change.
|
||||
FRANK_PRIMARY_VRAM_WEIGHTS_MIB=26071
|
||||
FRANK_PRIMARY_VRAM_KV_MIB=3543
|
||||
# KV cache in FP8 (7.32 KiB/token at this model's 12 full-attention layers,
|
||||
# 2 KV heads, 256 head_dim, K+V).
|
||||
FRANK_PRIMARY_KV_DTYPE=fp8
|
||||
# Spec decode: the checkpoint's own MTP head at the publisher's depth-3
|
||||
# operating point (probe() returns 3 when mtp_num_hidden_layers > 0; the
|
||||
# flag states it so the ledger records the intent).
|
||||
FRANK_PRIMARY_SPEC_TOKENS=2
|
||||
FRANK_PRIMARY_MAX_BATCHED_TOKENS=2048
|
||||
# Prefix cache tiers (publisher pattern; disk tier sized for 2 slots).
|
||||
FRANK_PRIMARY_PREFIX_HOST_MIB=4096
|
||||
FRANK_PRIMARY_PREFIX_DISK_MIB=65536
|
||||
# Frank's pin law: the PLE n-gram table (47.7 GiB) serves from pinned host
|
||||
# RAM the cards read. `ram` REFUSES the start if the memlock limit is too
|
||||
# small — the unit's LimitMEMLOCK=infinity is the other half of this law.
|
||||
FRANK_PRIMARY_NGRAM_PLACEMENT=ram
|
||||
# TTM preflight: the pinned ledger (2 x 40 GiB pools + 47.7 GiB PLE + 4 GiB
|
||||
# prefix host) is ~132 GiB. ttm's pages_limit/pages_max cap the GPU-mappable
|
||||
# pinned pages; the current cap (32,252,917 pages = ~123 GiB) is BELOW the
|
||||
# plan, so the serve script refuses to start until both are raised. This is
|
||||
# the floor it enforces (in MiB), with ~8 GiB of margin.
|
||||
FRANK_PRIMARY_TTM_MIN_MIB=140000
|
||||
# Escape hatch for boot-ladder experiments. Unset -> no extra argv. Keep
|
||||
# multi-flag values as one string; the serve script expands it unquoted on
|
||||
# purpose.
|
||||
FRANK_PRIMARY_EXTRA_ARGS=
|
||||
# The .rad's minimum size in MiB (deployer gate; a truncated convert is a
|
||||
# guaranteed long-timeout boot instead of a fast failure). The converted
|
||||
# container measured 177,109 MiB (1,857,127,997,440 B = 173.0 GiB; the
|
||||
# plan-only estimate 172.76 GiB covers the data planes but not the metadata
|
||||
# tail) - floor sits just under the payload.
|
||||
FRANK_PRIMARY_RAD_MIN_MIB=110000
|
||||
@@ -1,8 +1,8 @@
|
||||
# frank-archon.service — radiance (StillDeadcode, Apache-2.0) endpoint
|
||||
# frank-primary.service — radiance (StillDeadcode, Apache-2.0) endpoint
|
||||
# (OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE block-FP8 precision, the
|
||||
# rad-convert container qwen4exp-fp8-native.rad) on BOTH R9700s (TP=2,
|
||||
# wht6 wire), dankserver :7770. 2026-10-05 cutover from FreeToken (git tag
|
||||
# frank-archon-freetoken-pr587-20261005 is the pre-cutover state; the
|
||||
# frank-primary-freetoken-pr587-20261005 is the pre-cutover state; the
|
||||
# retired unit/env/serve trio and Dockerfile are in that tag).
|
||||
#
|
||||
# Same port, same served alias, same container identity as the unit name;
|
||||
@@ -19,7 +19,7 @@
|
||||
# unit grants LimitMEMLOCK=infinity to the dank process via PID1; the
|
||||
# engine still runs out of dank's rootless podman storage.
|
||||
[Unit]
|
||||
Description=frank-archon — radiance Qwen3.8-Flash-Next-FP8 (native) on 2x R9700 TP=2
|
||||
Description=frank-primary — radiance Qwen3.8-Flash-Next-FP8 (native) on 2x R9700 TP=2
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
# Never let the start-rate limiter give up after a crash loop (a failed LLM
|
||||
@@ -33,14 +33,14 @@ Group=dank
|
||||
# RLIMIT_MEMLOCK smaller than the pinned set fails the load mid-start.
|
||||
# PID1 grants infinity here — parity with the fleet's other LLM units.
|
||||
LimitMEMLOCK=infinity
|
||||
Environment="FRANK_ARCHON_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-archon.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-archon.sh
|
||||
Environment="FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh
|
||||
# systemd stop: wait alone cannot stop the container child; stop it
|
||||
# explicitly. No graceful drain contract is claimed (the OpenAI front end
|
||||
# holds no queue to drain — in-flight decodes are lost, which a stop
|
||||
# during a bench window accepts; radiance takes SIGTERM on the engine
|
||||
# process and podman reaps the container).
|
||||
ExecStop=/usr/bin/podman stop -t 120 frank-archon
|
||||
ExecStop=/usr/bin/podman stop -t 120 frank-primary
|
||||
# Boot cost: .rad load (weights + PLE upload) + expert host-pool pin. The
|
||||
# publisher's compose healthcheck allows a 15-minute start_period; the
|
||||
# vLLM-era budget (1800 s) carried forward.
|
||||
@@ -1,4 +1,4 @@
|
||||
# frank-zealot — llama.cpp (master, CUDA sm_86) serving
|
||||
# frank-secondary — llama.cpp (master, CUDA sm_86) serving
|
||||
# HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive at Q6_K on the RTX 3090,
|
||||
# :7771.
|
||||
#
|
||||
@@ -16,11 +16,11 @@
|
||||
# custom-quant lock-in like the ROCmFPX era). NO nextn/mtp tensors exist, so
|
||||
# in-file MTP is impossible here; SPEC_TYPE=none is forced by the payload.
|
||||
#
|
||||
# The KV rate follows from the architecture: 8 full layers × 4 KV heads ×
|
||||
# 256 dim = 8,192 elements per token per cache. f16 K + f16 V = 32,768
|
||||
# B/token = 32 KiB/token, so the 401,408-token pool is 12.25 GiB. The 24
|
||||
# DeltaNet layers cost a fixed recurrent state per SLOT (not per token) —
|
||||
# that is why two 200k slots fit where the 26B MoE fit only one.
|
||||
# KV rate law: 8 full layers × 4 KV heads × 256 dim = 8,192 elements per
|
||||
# token per cache. f16 K + f16 V = 32,768 B/token = 32 KiB/token, so the
|
||||
# 401,408-token pool is 12.25 GiB. The 24 DeltaNet layers cost a fixed
|
||||
# recurrent state per SLOT (not per token) — that is why two 200k slots fit
|
||||
# where the 26B MoE fit only one.
|
||||
#
|
||||
# Feasibility comes from the predecessor's MEASURED boots, not a formula:
|
||||
# MiMo-V2.6-Distill-Qwen-9B (identical qwen35 geometry, 7,793,710,624 B) at
|
||||
@@ -38,7 +38,7 @@ MMPROJ_FILE=/home/dank/models/Qwen35-9B-HauhauCS-Q6K/mmproj-Qwen3.5-9B-Uncensore
|
||||
# Served model id — the harness/family name (Protoss rename completed to the
|
||||
# wire 2026-10-09: harness ids + homepage cards 2026-10-08, /v1/models aliases
|
||||
# this day). The SERVICE, unit, scripts and this env file keep the
|
||||
# frank-zealot name; only the alias on the wire changed. Consumers that
|
||||
# frank-secondary name; only the alias on the wire changed. Consumers that
|
||||
# key on the served id: OpenWebUI (model row, access_grant, api_configs
|
||||
# model_ids) and the harness models.yml.
|
||||
ALIAS=frank-zealot
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=llama.cpp Qwen3.5-9B-Uncensored-HauhauCS-Aggressive Q6_K GGUF (qwen35 hybrid, 2x200704 slots, f16 K+V, MTP off, mmproj vision) on the RTX 3090 (dankclaw, frank-secondary)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=dank
|
||||
Group=dank
|
||||
# CUDA pins its staging buffers, so the process needs the memlock budget even
|
||||
# though the weights themselves are copied into VRAM.
|
||||
LimitMEMLOCK=infinity
|
||||
Environment="FRANK_SECONDARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-secondary.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-secondary.sh
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,24 +0,0 @@
|
||||
[Unit]
|
||||
Description=llama.cpp Qwen3.5-9B-Uncensored-HauhauCS-Aggressive Q6_K GGUF (qwen35 hybrid, 2x200704 slots, f16 K+V, MTP off, mmproj vision) on the RTX 3090 (dankclaw, frank-zealot)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=dank
|
||||
Group=dank
|
||||
# CUDA pins its staging buffers, so the process needs the memlock budget even
|
||||
# though the weights themselves are copied into VRAM.
|
||||
LimitMEMLOCK=infinity
|
||||
Environment="FRANK_ZEALOT_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-zealot.env"
|
||||
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-zealot.sh
|
||||
# Restart=always is the right policy for a serving daemon: a process that
|
||||
# exits on its own — exit 0 — is a CLEAN stop to systemd, so on-failure
|
||||
# never fires and the endpoint stays dark (with the unit reporting
|
||||
# success) until the next boot. frank-zealot is the fleet's compaction
|
||||
# hub — every endpoint's compaction dies when it does.
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -24,7 +24,7 @@ KLD_OUT=/home/dank/radiance-state/kld-gate.json
|
||||
: > "$KLD_OUT"
|
||||
cp -f /home/dank/serverAppConfigs/scripts/qwen38-froggeric-v22.5.jinja \
|
||||
/home/dank/models/tpl.jinja 2>/dev/null || true
|
||||
sudo systemctl stop frank-archon >/dev/null 2>&1 || true
|
||||
sudo systemctl stop frank-primary >/dev/null 2>&1 || true
|
||||
podman rm -f "$C" >/dev/null 2>&1 || true
|
||||
|
||||
podman run --rm --name "$C" \
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# llm-gates.sh — the artifact gates every LLM endpoint runs before it starts.
|
||||
#
|
||||
# Why this file exists: every LLM endpoint gates its payload twice — the
|
||||
# serve script at start, the deployer before dispatch. Two hand-written
|
||||
# copies of the same checks drift apart in wording and strictness, and the
|
||||
# drift surfaces as deploy-time disagreement: one side passes, the other
|
||||
# refuses, and the failure lands in production instead of in the gate.
|
||||
# Both sides source this library, so the refusal is one text and one rule.
|
||||
#
|
||||
# Contract (public interface):
|
||||
# require_exec PATH LABEL HINT
|
||||
# require_file PATH LABEL HINT
|
||||
# require_bytes PATH LABEL WANT_BYTES HINT
|
||||
# require_min_mib PATH LABEL MIN_MIB HINT
|
||||
# require_dir_files DIR LABEL HINT FILE...
|
||||
# require_image REF HINT
|
||||
#
|
||||
# Each returns 0 and prints nothing on success. On failure it prints two
|
||||
# lines to stderr — the first naming the endpoint ($GATE_PREFIX) and the
|
||||
# artifact, the second the remedy — and returns 1. It NEVER exits: the serve
|
||||
# scripts run under `set -e` (so a refusal is terminal), while the deployer
|
||||
# wants `|| return 1` so a failed gate reports a red deploy for that one
|
||||
# service and the dispatcher's state marker stays pending.
|
||||
# Lines are labelled by ARTIFACT CLASS, and the class is the whole label
|
||||
# (the path rides in the same line): the class + path is what an operator
|
||||
# acts on, so no wording order and no payload rename can make the two twins
|
||||
# disagree. The deployer's old "halogen vision tower" and the serve script's
|
||||
# "vision-tower" were one gate in two vocabularies that no grep could join.
|
||||
|
||||
# Size checks use equality, not a floor, for `require_bytes`: a fetch that
|
||||
# truncates looks like a success (an HTTP error exit is often swallowed, and
|
||||
# the error body is written in the artifact's place), so "exists and is big
|
||||
# enough" is not proof of the right artifact. The declared byte count is a
|
||||
# contract in both directions — a bigger file is a DIFFERENT artifact.
|
||||
|
||||
# GATE_PREFIX names the endpoint in every refusal (e.g. frank-dragoon). The
|
||||
# caller exports it; shellcheck cannot follow it across the source boundary,
|
||||
# so the read site carries the directive.
|
||||
# shellcheck disable=SC2034
|
||||
: "${GATE_PREFIX:=gate}"
|
||||
|
||||
# Two bash behaviors make the `return 1` at every call site load-bearing:
|
||||
# * a function whose body ends inside an `if` returns the CONDITION's
|
||||
# status (true = 0), so a gate that ends on its own refusal prints the
|
||||
# refusal and reports SUCCESS;
|
||||
# * `set -e` is suspended for any command in a `… || …` chain, so a gate
|
||||
# that runs inside one must not be trusted to abort the caller.
|
||||
# Every gate's refusal therefore carries an explicit `|| return 1`, and the
|
||||
# caller consumes the status (`|| exit 1`, `|| return 1`).
|
||||
_gate_fail() {
|
||||
printf '%s: %s\n' "$GATE_PREFIX" "$1" >&2
|
||||
printf ' hint: %s\n' "$2" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# require_file — PATH must be a regular file. A directory (an unpacked
|
||||
# staging dir wearing the payload's name) and a symlink (a HuggingFace cache
|
||||
# snapshot's entry, which dangles inside the container mount) both refuse.
|
||||
require_file() {
|
||||
local path="$1" label="$2" hint="$3"
|
||||
if [[ -L "$path" || ! -f "$path" ]]; then
|
||||
_gate_fail "$label: missing at $path" "$hint" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# require_exec — PATH must be an executable regular file. The native runtimes
|
||||
# (llama.cpp's llama-server) are binaries the operator builds; a tree that
|
||||
# compiled but lost its +x, or a directory wearing the binary's name, must
|
||||
# refuse the same way a missing one does.
|
||||
require_exec() {
|
||||
local path="$1" label="$2" hint="$3"
|
||||
if [[ -L "$path" || ! -x "$path" ]]; then
|
||||
_gate_fail "$label: missing at $path" "$hint" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# require_bytes — PATH must exist AND be exactly WANT_BYTES bytes.
|
||||
require_bytes() {
|
||||
local path="$1" label="$2" want="$3" hint="$4" got
|
||||
require_file "$path" "$label" "$hint" || return 1
|
||||
got="$(stat -c %s "$path")"
|
||||
if [[ "$got" != "$want" ]]; then
|
||||
_gate_fail "$label: is $got bytes, expected $want" "$hint" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# require_min_mib — PATH must be at least MIN_MIB mebibytes. The floor form
|
||||
# is for build products (a .rad container is a range of legal sizes; a
|
||||
# truncated convert is not).
|
||||
require_min_mib() {
|
||||
local path="$1" label="$2" min="$3" hint="$4" mib
|
||||
require_file "$path" "$label" "$hint" || return 1
|
||||
mib=$(( $(stat -c %s "$path") / 1048576 ))
|
||||
if (( mib < min )); then
|
||||
_gate_fail "$label: is $mib MiB, expected >= $min MiB" "$hint" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# require_dir_files — DIR must be a directory and every named member a
|
||||
# regular file, never a symlink: snapshot layouts link files into place, and
|
||||
# a link dangles inside the container mount even when the directory itself
|
||||
# is real. A symlinked DIRECTORY is refused too — the same layout can link
|
||||
# the directory itself, and the link dangles identically.
|
||||
require_dir_files() {
|
||||
local dir="$1" label="$2" hint="$3"
|
||||
shift 3
|
||||
local f
|
||||
if [[ -L "$dir" || ! -d "$dir" ]]; then
|
||||
_gate_fail "$label: missing at $dir" "$hint" || return 1
|
||||
fi
|
||||
for f in "$@"; do
|
||||
if [[ -L "$dir/$f" || ! -f "$dir/$f" ]]; then
|
||||
_gate_fail "$label: $dir/$f is missing or a symlink" "$hint" || return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# require_image — the image ref must be in podman storage. A name@sha256 ref
|
||||
# exists only if THAT manifest was pulled, so the gate is the ref itself, in
|
||||
# one command: no pipefail/grep race (the 2026-09-30 lesson).
|
||||
require_image() {
|
||||
local ref="$1" hint="$2"
|
||||
if ! podman image exists "$ref"; then
|
||||
_gate_fail "image: $ref is not in podman storage" "$hint" || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
@@ -44,17 +44,17 @@ JSONL_EXTS = (".jsonl", ".json")
|
||||
DOC_DIR = "serverAppConfigs-docs"
|
||||
|
||||
FLEET_CMDS = [
|
||||
"systemctl status frank-archon --no-pager",
|
||||
"journalctl -u frank-archon -n 100 --no-pager",
|
||||
"systemctl status frank-primary --no-pager",
|
||||
"journalctl -u frank-primary -n 100 --no-pager",
|
||||
"podman ps --format '{{.Names}} {{.Status}}'",
|
||||
"podman logs --tail 50 frank-archon",
|
||||
"podman logs --tail 50 frank-primary",
|
||||
"curl -s http://192.168.0.235:7770/health",
|
||||
"curl -s http://192.168.0.235:7770/metrics | grep radiance_",
|
||||
"df -h /home/dank | tail -1",
|
||||
"free -g",
|
||||
"sudo systemctl restart frank-archon",
|
||||
"sudo systemctl restart frank-primary",
|
||||
"cd /home/dank/serverAppConfigs && git pull --ff-only",
|
||||
"./scripts/deploy.sh dankserver --service frank-archon",
|
||||
"./scripts/deploy.sh dankserver --service frank-primary",
|
||||
"./tests/run.sh 2>&1 | tail -5",
|
||||
"jq . /home/dank/models/rad/index.json | head -40",
|
||||
"python3 -c \"import json; print(json.load(open('config.json'))['num_hidden_layers'])\"",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# STATUS: WIP — first of three patches in the "compile the GLM trunk" changelist. The
|
||||
# set does NOT yet boot the engine (see 03 for the next blocker), so it is NOT part of
|
||||
# the deployed frank-archon shape. Nothing in scripts/ mounts these files.
|
||||
# the deployed frank-primary shape. Nothing in scripts/ mounts these files.
|
||||
#
|
||||
# WHY THIS IS THE RIGHT LEVER (measured 2026-09-30, not inferred):
|
||||
#
|
||||
|
||||
@@ -29,11 +29,10 @@
|
||||
# its base SupportsEagleBase (has_own_lm_head, has_own_embed_tokens) -- and
|
||||
# getattr_static()s each one off the instance, breaking on the first miss.
|
||||
# So BOTH wrappers need all three attributes.
|
||||
# * Why not inheritance: adding SupportsEagle3 as a BASE class dies at
|
||||
# class creation with "Cannot create a consistent method resolution
|
||||
# order (MRO) for bases ..." (GLM's wrappers already carry four/five
|
||||
# Protocol-derived bases; the base ordering cannot take another). The
|
||||
# attribute route avoids the MRO entirely; simulation confirms that
|
||||
# * MRO law: INHERITING SupportsEagle3 as a base dies at class creation with
|
||||
# "Cannot create a consistent method resolution order (MRO) for bases ..."
|
||||
# (GLM's wrappers already carry four/five Protocol-derived bases). The
|
||||
# attribute route avoids the MRO entirely. Also proven by simulation:
|
||||
# the four-base ForCausalLM ordering cannot take SupportsEagle3 in ANY
|
||||
# position.
|
||||
# * Method placement: without inheriting, the Protocol's default
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# gpu_worker-kv-cap.patch — fork-local fix for the expert-offload KV cap.
|
||||
#
|
||||
# STATUS: NOT part of the deployed frank-archon shape. It exists because the
|
||||
# STATUS: NOT part of the deployed frank-primary shape. It exists because the
|
||||
# fork's plan under-models this model's KV, and it is the enabler for a future
|
||||
# MTP attempt (the production GLM shape runs without MTP and boots fine on the
|
||||
# plan's own reserve).
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""probe-frank-archon-kv.py — verify the 2 x 200,000 KV contract on a LIVE
|
||||
frank-archon (radiance) service. Run it ON dankserver: it talks to
|
||||
127.0.0.1:7770 and builds its prompts from the repo checkout there.
|
||||
No scratch container, no mask: the service under test is the service.
|
||||
|
||||
ssh dankserver 'python3 /home/dank/serverAppConfigs/scripts/probe-frank-archon-kv.py'
|
||||
|
||||
Three probes, in increasing cost:
|
||||
A decode at depth — one ~51k-token chat completion, 512 tokens, greedy.
|
||||
Reports the engine's own `timings` (prefill, decode, draft acceptance).
|
||||
Method: >=128 generated tokens (below that, first-token latency
|
||||
dominates the decode rate), greedy (sampling noise is unattributable),
|
||||
real text (synthetic text inflates draft acceptance).
|
||||
B the capacity contract — TWO concurrent chats of ~197k prompt tokens
|
||||
(~394k tokens resident) each carrying a unique marker at its head. Both
|
||||
must answer their own marker. This is what a 2 x 200,000 endpoint owes:
|
||||
two full-height conversations resident at once, exact retrieval in both.
|
||||
C prefix-cache restore — probe A's cold prompt again, so the disk tier has
|
||||
to give the blocks back (cache_n > 0, TTFT collapses).
|
||||
|
||||
PROBE PITFALL: radiance routes the thinking block to
|
||||
`reasoning_content`, so a chat request whose budget is spent inside
|
||||
`<think>` returns EMPTY `content` with `finish_reason: stop`. A probe that
|
||||
reads `content` alone scores 0/2 on a healthy server. Every request here
|
||||
sets chat_template_kwargs.enable_thinking=false and reads both fields.
|
||||
|
||||
A/B method for a shape change (no scratch container): write a variant env file
|
||||
on the host, point a transient systemd drop-in at it
|
||||
(`Environment=FRANK_ARCHON_ENV_FILE=...` — drop-ins are read AFTER the unit's
|
||||
own Environment= lines, so they win), restart, probe, delete the drop-in,
|
||||
daemon-reload, restart. `systemctl mask` is not available: the unit file in
|
||||
/etc/systemd/system is a relink symlink into the checkout.
|
||||
"""
|
||||
import json
|
||||
import glob
|
||||
import time
|
||||
import urllib.request
|
||||
import concurrent.futures
|
||||
|
||||
BASE = "http://127.0.0.1:7770"
|
||||
MODEL = "frank-archon"
|
||||
REPO = "/home/dank/serverAppConfigs"
|
||||
GLOBS = [f"{REPO}/scripts/*.sh", f"{REPO}/serverAppConfigs-docs/**/*.md"]
|
||||
|
||||
|
||||
def post(payload, timeout=1800):
|
||||
req = urllib.request.Request(
|
||||
BASE + "/v1/chat/completions",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
t0 = time.time()
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
body = json.loads(r.read().decode())
|
||||
hdr = dict(r.headers.items())
|
||||
return body, time.time() - t0, hdr
|
||||
|
||||
|
||||
def get(path, timeout=60):
|
||||
with urllib.request.urlopen(BASE + path, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def corpus(min_chars):
|
||||
files = []
|
||||
for g in GLOBS:
|
||||
files.extend(sorted(glob.glob(g, recursive=True)))
|
||||
out, total = [], 0
|
||||
for f in files:
|
||||
try:
|
||||
t = open(f, encoding="utf-8", errors="replace").read()
|
||||
except OSError:
|
||||
continue
|
||||
out.append(t)
|
||||
total += len(t)
|
||||
if total > min_chars:
|
||||
break
|
||||
return "".join(text for text in out)
|
||||
|
||||
|
||||
def prompt(text, chars, salt, fact):
|
||||
"""Real repo text, salted so the prefix cache cannot answer, marker planted
|
||||
near the head, question at the tail."""
|
||||
head = f"\n[SALT {salt}] confidential marker: {fact}\n"
|
||||
body = text + text
|
||||
p = (head + body * max(1, chars // len(body)))[:chars]
|
||||
return p + "\n\nQuestion: what is the confidential marker in the " \
|
||||
"SALT block near the top of this document? Answer with the marker " \
|
||||
"alone, no other words.\n"
|
||||
|
||||
|
||||
def chat(text, chars, salt, fact, max_tokens):
|
||||
return {"model": MODEL,
|
||||
"messages": [{"role": "user",
|
||||
"content": prompt(text, chars, salt, fact)}],
|
||||
"max_tokens": max_tokens, "temperature": 0.0,
|
||||
"chat_template_kwargs": {"enable_thinking": False}}
|
||||
|
||||
|
||||
def show(tag, j, wall, expect=None):
|
||||
tim, u = j.get("timings") or {}, j.get("usage", {})
|
||||
ch = (j.get("choices") or [{}])[0]
|
||||
msg = ch.get("message") or {}
|
||||
txt = (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
||||
acc = (tim["draft_n_accepted"] / tim["draft_n"]) if tim.get("draft_n") else None
|
||||
line = (f"[{tag}] prompt={u.get('prompt_tokens')} gen={u.get('completion_tokens')} "
|
||||
f"wall={wall:.1f}s prefill={tim.get('prompt_per_second', 0):.0f} t/s "
|
||||
f"decode={tim.get('predicted_per_second', 0):.1f} t/s "
|
||||
f"draft={tim.get('draft_n')}/{tim.get('draft_n_accepted')} "
|
||||
f"accept={acc if acc is None else round(acc, 4)} "
|
||||
f"cache_n={tim.get('cache_n')} finish={ch.get('finish_reason')}")
|
||||
if expect:
|
||||
hit = expect.split("-")[0] in txt.lower()
|
||||
line += f" expected={expect} answered={txt[:80]!r} HIT={hit}"
|
||||
print(line)
|
||||
|
||||
|
||||
def main():
|
||||
print("health:", json.dumps(get("/health")))
|
||||
print("models:", [m.get("id") for m in get("/v1/models").get("data", [])])
|
||||
deep = corpus(160_000)
|
||||
wide = corpus(1_100_000)
|
||||
|
||||
facts = {"B1": "cobalt-harbor-13", "B2": "quartz-meadow-42"}
|
||||
bodies = {s: chat(wide, 646_000, s, f, 24) for s, f in facts.items()}
|
||||
print(f"[B] {len(bodies['B1']['messages'][0]['content']):,} chars per "
|
||||
f"request, x2 concurrent")
|
||||
|
||||
# A + C first (cheap), B last (the expensive pair).
|
||||
for tag, salt in (("A-cold", "A1-9137"), ):
|
||||
j, wall, _ = post(chat(deep, 140_000, salt, "zephyr-lantern-7", 512))
|
||||
show(tag, j, wall, "zephyr-lantern-7")
|
||||
t0 = time.time()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
|
||||
futs = {s: ex.submit(post, b) for s, b in bodies.items()}
|
||||
for s, fut in futs.items():
|
||||
try:
|
||||
j, wall, _ = fut.result()
|
||||
show(f"B-{s}", j, wall, facts[s])
|
||||
except Exception as e: # a preemption/timeout lands here
|
||||
print(f"[B-{s}] FAILED after {time.time() - t0:.1f}s: {e!r}")
|
||||
print(f"[B] total {time.time() - t0:.1f}s")
|
||||
j, wall, _ = post(chat(deep, 140_000, "A1-9137", "zephyr-lantern-7", 512))
|
||||
show("C-prefix-hit", j, wall, "zephyr-lantern-7")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,12 +1,10 @@
|
||||
# qwen4exp-swift15-w4dn8-i8-hc8m.recipe -- the Swift 1.5 finetune of Qwen3.8-Flash-Next
|
||||
# (ukisai/Swift1.5-Qwen3.8-Flash-Next, BF16 source, 335.3 GiB, key set identical to the official
|
||||
# base) as a radiance container for frank-archon: EVERY routed expert -- gate_up AND down --
|
||||
# through GPTQ into the w4nl64a8h table. The converter declares ONE
|
||||
# encoding per expert table (qwen4exp_fp8 rad_arch_declare): the served
|
||||
# kernel covers the whole expert set, and the only legal mixed layout is a
|
||||
# WHOLE expert kept plain bf16 beside it. That rules out the original
|
||||
# split tier (down block-fp8 + gate_up w4), which asks one table for two
|
||||
# encodings.
|
||||
# base) as a radiance container for frank-primary: EVERY routed expert -- gate_up AND down --
|
||||
# through GPTQ into the w4nl64a8h table. The plugin's one-table-one-encoding law
|
||||
# ("one table of experts takes one encoding, and only a whole expert kept plain bf16 is served
|
||||
# beside it", qwen4exp_fp8 rad_arch_declare) forbids the original split tier (down block-fp8 +
|
||||
# gate_up w4): the served kernel is w4nl64a8h for the whole expert set, bf16 experts beside it.
|
||||
# The rest is qwen4exp-w5dn64-i8-hc8m.recipe verbatim: int8 trunk linears, block-fp8 hc mixers
|
||||
# and MTP fc, the fixed-scale n-gram table, the 2-bit draft head.
|
||||
#
|
||||
@@ -27,14 +25,11 @@
|
||||
|
||||
# The protected experts, cast to bf16 ahead of both expert rules. THE NAMES BELOW ARE THE BASE
|
||||
# CHECKPOINT'S (from its 1M-token recording, qwen4exp-w5dn64-i8-hc8m.recipe) -- a warm start only.
|
||||
# The Swift 1.5 finetune's routing distribution is not assumed equal: the
|
||||
# derivation step rewrites this section from Swift's OWN calibration
|
||||
# recording before this recipe is ever run. The selection rule is the base
|
||||
# checkpoint's: an expert whose share of its layer's down-input energy is
|
||||
# at least 3% AND at least three times its share of the routing; an even
|
||||
# count on each rank's half of the layer (the four-bit GEMM takes the rest
|
||||
# as two equal tables by parity), padded with that half's next most
|
||||
# energetic expert.
|
||||
# The Swift 1.5 finetune's routing distribution is not assumed equal: the derivation step rewrites
|
||||
# this section from Swift's OWN calibration recording before this recipe is ever run, by the base's
|
||||
# law -- an expert whose share of its layer's down-input energy is at least 3% and at least three
|
||||
# times its share of the routing, an even count on each rank's half of the layer (the four-bit GEMM
|
||||
# takes the rest as two equal tables by parity) by adding that half's next most energetic expert.
|
||||
# PROTECTED-BEGIN (rewritten from Swift's OWN grams by derive-protected-experts.py)
|
||||
blk.1.ffn_*_exps.271.weight cast dtype=bf16
|
||||
blk.1.ffn_*_exps.355.weight cast dtype=bf16
|
||||
|
||||
@@ -30,9 +30,9 @@ KLD_REF_DIR=/home/dank/corpora/kld-ref
|
||||
CALIB_DIR=/home/dank/corpora/calib
|
||||
KLD_CORPUS=/home/dank/corpora/out/kld.jsonl
|
||||
|
||||
restore() { sudo systemctl start frank-archon >/dev/null 2>&1 || true; }
|
||||
restore() { sudo systemctl start frank-primary >/dev/null 2>&1 || true; }
|
||||
trap restore EXIT
|
||||
sudo systemctl stop frank-archon >/dev/null 2>&1 || true
|
||||
sudo systemctl stop frank-primary >/dev/null 2>&1 || true
|
||||
podman rm -f "$C" >/dev/null 2>&1 || true
|
||||
|
||||
ARGS=(--model /models/rad/qwen4exp-swift15-bf16.rad
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# serve-frank-archon.sh — frank-archon endpoint: radiance (StillDeadcode,
|
||||
# Apache-2.0) serving the OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint at
|
||||
# NATIVE block-FP8 precision from a rad-convert container
|
||||
# (/home/dank/models/rad/qwen4exp-fp8-native.rad), on BOTH R9700s (TP=2,
|
||||
# wht6 wire), dankserver :7770. 2026-10-05 cutover from FreeToken (PR #587,
|
||||
# one card; git tag frank-archon-freetoken-pr587-20261005 is the
|
||||
# pre-cutover state).
|
||||
#
|
||||
# The run line below IS the engine argv: the image's entrypoint is the
|
||||
# engine and its tools sit beside it on PATH. Unlike every predecessor
|
||||
# there is no per-boot VRAM planner to second-guess: the boot ledger in the
|
||||
# log states every budget before anything is allocated (weights vs KV
|
||||
# split, the expert host pool's coverage of the worst case, the PLE pin,
|
||||
# the P2P answer per pair) — read it after every resize.
|
||||
# Deployed shape (2026-10-11): 2 decode slots x 200,000 tokens, bf16
|
||||
# (unquantized) KV in a 6,912 MiB pool, the weights slab cut to 22,208 MiB to
|
||||
# pay for it, 44 GiB/rank pinned expert pool, MTP depth 2, prefix cache host
|
||||
# 8 GiB + disk 64 GiB, PLE pinned in RAM.
|
||||
#
|
||||
# The root unit (scripts/frank-archon.service, User=dank,
|
||||
# LimitMEMLOCK=infinity) grants the pin budget: 2 x 44 GiB expert pools +
|
||||
# the 47.7 GiB PLE table + 8 GiB prefix host tier — ~147 GiB of pinned
|
||||
# pages, which is ALSO why this script refuses to start when ttm's
|
||||
# pages_limit/pages_max cap is under FRANK_ARCHON_TTM_MIN_MIB (see below).
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# The artifact gates are the shared library: the serve script and the
|
||||
# deployer gate the same payload, and two hand-written copies of the same
|
||||
# checks drift apart until one side passes what the other refuses. One
|
||||
# library, one refusal text, one rule.
|
||||
export GATE_PREFIX=frank-archon
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/llm-gates.sh"
|
||||
ENV_FILE="${FRANK_ARCHON_ENV_FILE:-$SCRIPT_DIR/frank-archon.env}"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "ERROR: frank-archon env file absent: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${FRANK_ARCHON_IMAGE:?FRANK_ARCHON_IMAGE must name the radiance image}"
|
||||
: "${FRANK_ARCHON_MODEL_DIR:?FRANK_ARCHON_MODEL_DIR must name the .rad dir}"
|
||||
: "${FRANK_ARCHON_MODEL_FILE:?FRANK_ARCHON_MODEL_FILE must name the .rad file}"
|
||||
: "${FRANK_ARCHON_STATE_DIR:?FRANK_ARCHON_STATE_DIR must name the /data state dir}"
|
||||
: "${FRANK_ARCHON_CONTAINER:?FRANK_ARCHON_CONTAINER must name the container}"
|
||||
: "${FRANK_ARCHON_SERVED_NAME:?FRANK_ARCHON_SERVED_NAME must name the alias}"
|
||||
: "${FRANK_ARCHON_PORT:?FRANK_ARCHON_PORT must be the port}"
|
||||
: "${FRANK_ARCHON_TEMPLATE:?FRANK_ARCHON_TEMPLATE must name the chat template}"
|
||||
: "${FRANK_ARCHON_TP:?FRANK_ARCHON_TP must be set}"
|
||||
: "${FRANK_ARCHON_TP_WIRE:?FRANK_ARCHON_TP_WIRE must be set}"
|
||||
: "${FRANK_ARCHON_MAX_SEQS:?FRANK_ARCHON_MAX_SEQS must be set}"
|
||||
: "${FRANK_ARCHON_MAX_MODEL_LEN:?FRANK_ARCHON_MAX_MODEL_LEN must be set}"
|
||||
: "${FRANK_ARCHON_PLACEMENT:?FRANK_ARCHON_PLACEMENT must be set}"
|
||||
: "${FRANK_ARCHON_HOST_POOL_MIB:?FRANK_ARCHON_HOST_POOL_MIB must be set}"
|
||||
: "${FRANK_ARCHON_GPU_HEADROOM_MIB:?FRANK_ARCHON_GPU_HEADROOM_MIB must be set}"
|
||||
: "${FRANK_ARCHON_VRAM_WEIGHTS_MIB:?FRANK_ARCHON_VRAM_WEIGHTS_MIB must be set}"
|
||||
: "${FRANK_ARCHON_VRAM_KV_MIB:?FRANK_ARCHON_VRAM_KV_MIB must be set}"
|
||||
: "${FRANK_ARCHON_KV_DTYPE:?FRANK_ARCHON_KV_DTYPE must be set}"
|
||||
: "${FRANK_ARCHON_SPEC_TOKENS:?FRANK_ARCHON_SPEC_TOKENS must be set}"
|
||||
: "${FRANK_ARCHON_MAX_BATCHED_TOKENS:?FRANK_ARCHON_MAX_BATCHED_TOKENS must be set}"
|
||||
: "${FRANK_ARCHON_PREFIX_HOST_MIB:?FRANK_ARCHON_PREFIX_HOST_MIB must be set}"
|
||||
: "${FRANK_ARCHON_PREFIX_DISK_MIB:?FRANK_ARCHON_PREFIX_DISK_MIB must be set}"
|
||||
: "${FRANK_ARCHON_NGRAM_PLACEMENT:?FRANK_ARCHON_NGRAM_PLACEMENT must be set}"
|
||||
: "${FRANK_ARCHON_TTM_MIN_MIB:?FRANK_ARCHON_TTM_MIN_MIB must be set}"
|
||||
: "${FRANK_ARCHON_RAD_MIN_MIB:?FRANK_ARCHON_RAD_MIN_MIB must be set}"
|
||||
|
||||
# The image gate is the digest-pinned ref itself: a name@sha256 ref exists
|
||||
# in storage only if THAT manifest was pulled — a retagged look-alike or a
|
||||
# newer latest does not satisfy it. One command, no pipefail/grep race (the
|
||||
# 2026-09-30 lesson). Runs as dank: the unit's process uses dank's ROOTLESS
|
||||
# podman storage (root unit, User=dank — the fleet's frank-archon shape).
|
||||
require_image "$FRANK_ARCHON_IMAGE" \
|
||||
"podman pull docker://stilldeadcode/radiance:latest (as dank)" || exit 1
|
||||
|
||||
# The payload gate: the .rad container file, sized. A truncated convert
|
||||
# would otherwise buy a guaranteed long-timeout boot instead of this fast
|
||||
# failure. The staging dir / raw checkpoint are NOT this script's concern
|
||||
# (the convert script built the file; its rad-info output is the record).
|
||||
require_min_mib "$FRANK_ARCHON_MODEL_DIR/$FRANK_ARCHON_MODEL_FILE" \
|
||||
container "$FRANK_ARCHON_RAD_MIN_MIB" \
|
||||
"build it: scripts/convert-swift15-w4dn8.sh (rad-info is the record)" || exit 1
|
||||
|
||||
require_file "$FRANK_ARCHON_TEMPLATE" template \
|
||||
"the chat template is vendored: scripts/qwen38-froggeric-v22.5.jinja" || exit 1
|
||||
|
||||
mkdir -p "$FRANK_ARCHON_STATE_DIR/kvcache/frank-archon"
|
||||
# The engine's create is NOT recursive: with only the state dir mounted, its
|
||||
# "cannot create /data/kvcache/frank-archon: No such file or directory"
|
||||
# left the SSD prefix tier off (measured 2026-10-06 boot ledger). Pre-create
|
||||
# the full path on the host so the tier opens on first boot.
|
||||
|
||||
mkdir -p "$FRANK_ARCHON_STATE_DIR/tune"
|
||||
# rad-tune's kernel-selection cache (see the runbook's "Tuning levers"):
|
||||
# the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune at boot;
|
||||
# $RADIANCE_HOME defaults to /opt/radiance, which lives in the image, so
|
||||
# the cache must ride a host mount or it dies with the --rm container
|
||||
# that wrote it. The dir is pre-created for the same reason as kvcache
|
||||
# (the engine's create is not recursive).
|
||||
|
||||
# GPU access is mandatory; the node paths are knobs so the seam can drive
|
||||
# this gate on a box without AMD GPUs (production defaults).
|
||||
DEV_KFD="${FRANK_ARCHON_DEV_KFD:-/dev/kfd}"
|
||||
DEV_DRI="${FRANK_ARCHON_DEV_DRI:-/dev/dri}"
|
||||
if [ ! -e "$DEV_KFD" ] || [ ! -d "$DEV_DRI" ]; then
|
||||
echo "ERROR: GPU device nodes absent ($DEV_KFD, $DEV_DRI)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TTM preflight: the pinned ledger (~2 x 40 GiB pools + 47.7 GiB PLE + 4 GiB
|
||||
# prefix host = ~132 GiB) rides TTM's shared pages, capped by ttm's
|
||||
# pages_limit (the current kernel exposes NO pages_max; the cap function
|
||||
# takes the min over the knobs that exist). Below FRANK_ARCHON_TTM_MIN_MIB
|
||||
# the load dies mid-pin with an allocator error instead of this named
|
||||
# refusal — raise the knob first (runtime sysfs write; kernel cmdline
|
||||
# fallback + one reboot only if the file is read-only), see the runbook.
|
||||
# RAISED 2026-10-05 pre-cutover to 52,428,800 pages = 200 GiB.
|
||||
ttm_cap_mib() {
|
||||
# min over the knobs THIS kernel exposes — the current kernel has
|
||||
# pages_limit only (no pages_max); a knob that is absent is skipped,
|
||||
# not a zero (a zero would refuse a healthy cap).
|
||||
local v cur="" dir
|
||||
dir="${FRANK_ARCHON_TTM_SYSFS:-/sys/module/ttm/parameters}"
|
||||
for p in "$dir/pages_limit" "$dir/pages_max"; do
|
||||
[ -r "$p" ] || continue
|
||||
v="$(cat "$p" 2>/dev/null)" || continue
|
||||
[ -n "$v" ] || continue
|
||||
v=$(( v * 4 / 1024 ))
|
||||
if [ "$v" -lt "${cur:-999999999}" ]; then cur=$v; fi
|
||||
done
|
||||
echo "${cur:-0}"
|
||||
}
|
||||
TTM_CAP="$(ttm_cap_mib)"
|
||||
if [ "$TTM_CAP" -lt "$FRANK_ARCHON_TTM_MIN_MIB" ]; then
|
||||
echo "ERROR: ttm pages cap is ${TTM_CAP} MiB, the pinned ledger needs >= $FRANK_ARCHON_TTM_MIN_MIB" >&2
|
||||
echo "frank-archon: raise /sys/module/ttm/parameters/pages_limit (and pages_max if the kernel exposes one) to >= $FRANK_ARCHON_TTM_MIN_MIB in 4 KiB pages" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A crashed/killed unit can leave the container name registered (systemd
|
||||
# SIGKILLs conmon before cleanup runs). Clear OUR name plus the legacy
|
||||
# identities (the FreeToken-era frank-archon-qwen, the retired R9V
|
||||
# container and the ad hoc experiment containers all hold this GPU); all
|
||||
# idempotent.
|
||||
for CTR in $FRANK_ARCHON_LEGACY_CONTAINERS "$FRANK_ARCHON_CONTAINER"; do
|
||||
podman rm -f "$CTR" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
LOG="${FRANK_ARCHON_LOG:-/home/dank/logs/frank_archon.log}"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
|
||||
# Publisher-vetted container contract (docs/DOCKER.md, compose/common.yaml):
|
||||
# --network host (the server binds the host's addresses directly, no NAT),
|
||||
# --init (the engine releases the cards on SIGTERM), seccomp=unconfined
|
||||
# (Docker/podman's default profile refuses io_uring, which the n-gram
|
||||
# table's reads go through, and the NUMA binding calls the ROCm runtime
|
||||
# makes for host memory), memlock unbounded (the pinned ledger — legal
|
||||
# because the root unit grants LimitMEMLOCK=infinity to this dank process),
|
||||
# /dev/kfd + /dev/dri (render/video groups are moot for a root container).
|
||||
# The template rides a single-file mount; the .rad dir is ro; /data holds
|
||||
# the prefix-cache disk tier.
|
||||
podman run -d --name "$FRANK_ARCHON_CONTAINER" \
|
||||
--network host \
|
||||
--init \
|
||||
--security-opt seccomp=unconfined \
|
||||
--ulimit memlock=-1:-1 \
|
||||
--device /dev/kfd --device /dev/dri \
|
||||
-v "$FRANK_ARCHON_MODEL_DIR":/models:ro \
|
||||
-v "$FRANK_ARCHON_STATE_DIR":/data \
|
||||
-v "$FRANK_ARCHON_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
||||
"$FRANK_ARCHON_IMAGE" \
|
||||
--model "/models/$FRANK_ARCHON_MODEL_FILE" \
|
||||
--host :: --port "$FRANK_ARCHON_PORT" \
|
||||
--served-model-name "$FRANK_ARCHON_SERVED_NAME" \
|
||||
--override-chat-template /templates/template.jinja \
|
||||
--tp "$FRANK_ARCHON_TP" --tp-wire "$FRANK_ARCHON_TP_WIRE" \
|
||||
--max-num-seqs "$FRANK_ARCHON_MAX_SEQS" \
|
||||
--max-model-len "$FRANK_ARCHON_MAX_MODEL_LEN" \
|
||||
--placement "$FRANK_ARCHON_PLACEMENT" \
|
||||
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
||||
--gpu-headroom-mib "$FRANK_ARCHON_GPU_HEADROOM_MIB" \
|
||||
--vram-weights-mib "$FRANK_ARCHON_VRAM_WEIGHTS_MIB" \
|
||||
--vram-kv-mib "$FRANK_ARCHON_VRAM_KV_MIB" \
|
||||
--kv-cache-dtype "$FRANK_ARCHON_KV_DTYPE" \
|
||||
--prefix-cache-host-mib "$FRANK_ARCHON_PREFIX_HOST_MIB" \
|
||||
--prefix-cache-dir /data/kvcache/frank-archon \
|
||||
--prefix-cache-disk-mib "$FRANK_ARCHON_PREFIX_DISK_MIB" \
|
||||
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
||||
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
||||
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
||||
${FRANK_ARCHON_EXTRA_ARGS:+$FRANK_ARCHON_EXTRA_ARGS} \
|
||||
> "$LOG" 2>&1
|
||||
|
||||
# From here on, this script's lifetime IS the container's: wait returns the
|
||||
# container's exit code, so a crash is a nonzero ExecStart and the unit's
|
||||
# Restart=always is real. (systemd stop goes through ExecStop in the unit —
|
||||
# podman wait alone cannot stop the container child.)
|
||||
exec podman wait "$FRANK_ARCHON_CONTAINER"
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# serve-frank-dragoon.sh — frank-dragoon endpoint: halogen-flash-server 0.16.2
|
||||
# serve-frank-large.sh — frank-large endpoint: halogen-flash-server 0.16.2
|
||||
# (closed-source engine, image ghcr.io/peonist-ai/halogen-flash-server,
|
||||
# digest-pinned in frank-dragoon.env) serving the Quat3rnion abliterated
|
||||
# digest-pinned in frank-large.env) serving the Quat3rnion abliterated
|
||||
# Swift1.5 finetune of peonist's Qwen3.8-Flash-Next v2 checkpoint on
|
||||
# Dankclaw's Strix Halo iGPU, host :7770. 2026-10-06 cutover from the
|
||||
# halogen 27B-dense shape — revert the swap commit's trio to roll back
|
||||
# (the 27B payload stays on disk).
|
||||
#
|
||||
# The engine is driven by `podman run` DIRECTLY (the fleet's frank-archon
|
||||
# The engine is driven by `podman run` DIRECTLY (the fleet's frank-primary
|
||||
# pattern): the image starts engine + OpenAI frontend in one container.
|
||||
# The frontend listens on 8731 inside (HALOGEN_API_PORT default) and is
|
||||
# published as host 7770; the engine's own token port (8730, no auth)
|
||||
@@ -26,20 +26,13 @@
|
||||
# Why containers instead of a bare binary: the payload is .hgn and only
|
||||
# the flash-server line reads it; it ships as a container; the root unit
|
||||
# keeps LimitMEMLOCK=infinity for parity and to outlive the login session,
|
||||
# exactly like frank-archon's.
|
||||
# exactly like frank-primary's.
|
||||
set -euo pipefail
|
||||
|
||||
# The artifact gates are the shared library — the serve script and the
|
||||
# deployer refuse the same missing/truncated payload with the same words
|
||||
# (the 2026-10-01 crash-loop was the two hand-written twins disagreeing).
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export GATE_PREFIX=frank-dragoon
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/llm-gates.sh"
|
||||
|
||||
ENV_FILE="${FRANK_DRAGOON_ENV_FILE:-$SCRIPT_DIR/frank-dragoon.env}"
|
||||
ENV_FILE="${FRANK_LARGE_ENV_FILE:-$SCRIPT_DIR/frank-large.env}"
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "frank-dragoon: env file not found: $ENV_FILE" >&2
|
||||
echo "frank-large: env file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
@@ -47,65 +40,81 @@ set -a
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${FRANK_DRAGOON_IMAGE:?FRANK_DRAGOON_IMAGE must name the digest-pinned image}"
|
||||
: "${FRANK_DRAGOON_CONTAINER:?FRANK_DRAGOON_CONTAINER must name the container}"
|
||||
: "${FRANK_DRAGOON_PUBLISH:?FRANK_DRAGOON_PUBLISH must be host:container}"
|
||||
: "${FRANK_DRAGOON_MODEL_DIR:?FRANK_DRAGOON_MODEL_DIR must name the payload dir}"
|
||||
: "${FRANK_DRAGOON_CHECKPOINT:?FRANK_DRAGOON_CHECKPOINT must name the .hgn inside it}"
|
||||
: "${FRANK_DRAGOON_CHECKPOINT_BYTES:?FRANK_DRAGOON_CHECKPOINT_BYTES must pin the size}"
|
||||
: "${FRANK_DRAGOON_NGRAM_TABLE:?FRANK_DRAGOON_NGRAM_TABLE must name the lookup table}"
|
||||
: "${FRANK_DRAGOON_NGRAM_BYTES:?FRANK_DRAGOON_NGRAM_BYTES must pin the table size}"
|
||||
: "${FRANK_DRAGOON_VISION_TOWER:?FRANK_DRAGOON_VISION_TOWER must name the tower}"
|
||||
: "${FRANK_DRAGOON_VISION_BYTES:?FRANK_DRAGOON_VISION_BYTES must pin the tower size}"
|
||||
: "${FRANK_DRAGOON_TOKENIZER_FILES:?FRANK_DRAGOON_TOKENIZER_FILES must list the tokenizer dir contents}"
|
||||
: "${FRANK_LARGE_IMAGE:?FRANK_LARGE_IMAGE must name the digest-pinned image}"
|
||||
: "${FRANK_LARGE_CONTAINER:?FRANK_LARGE_CONTAINER must name the container}"
|
||||
: "${FRANK_LARGE_PUBLISH:?FRANK_LARGE_PUBLISH must be host:container}"
|
||||
: "${FRANK_LARGE_MODEL_DIR:?FRANK_LARGE_MODEL_DIR must name the payload dir}"
|
||||
: "${FRANK_LARGE_CHECKPOINT:?FRANK_LARGE_CHECKPOINT must name the .hgn inside it}"
|
||||
: "${FRANK_LARGE_CHECKPOINT_BYTES:?FRANK_LARGE_CHECKPOINT_BYTES must pin the size}"
|
||||
: "${FRANK_LARGE_NGRAM_TABLE:?FRANK_LARGE_NGRAM_TABLE must name the lookup table}"
|
||||
: "${FRANK_LARGE_NGRAM_BYTES:?FRANK_LARGE_NGRAM_BYTES must pin the table size}"
|
||||
: "${FRANK_LARGE_VISION_TOWER:?FRANK_LARGE_VISION_TOWER must name the tower}"
|
||||
: "${FRANK_LARGE_VISION_BYTES:?FRANK_LARGE_VISION_BYTES must pin the tower size}"
|
||||
: "${FRANK_LARGE_TOKENIZER_FILES:?FRANK_LARGE_TOKENIZER_FILES must list the tokenizer dir contents}"
|
||||
: "${HALOGEN_MODEL_ID:?HALOGEN_MODEL_ID must be the served alias (frank-dragoon)}"
|
||||
: "${HALOGEN_CTX:?HALOGEN_CTX must be set (per-slot window)}"
|
||||
: "${HALOGEN_KV_POOL_POSITIONS:?HALOGEN_KV_POOL_POSITIONS must be set}"
|
||||
: "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set}"
|
||||
: "${HALOGEN_SPEC_ADAPT:?HALOGEN_SPEC_ADAPT must be set (0 = head always on)}"
|
||||
|
||||
# The image gate is the digest-pinned ref itself: `podman image exists` takes
|
||||
# a name@sha256 ref in one command, so a retagged look-alike or a newer
|
||||
# latest cannot satisfy it and there is no pipefail/SIGPIPE race with grep
|
||||
# (the 2026-09-30 deploy refusal). Runs as dank: the unit's process uses
|
||||
# dank's rootless storage.
|
||||
require_image "$FRANK_DRAGOON_IMAGE" \
|
||||
"pull it: podman pull docker://$FRANK_DRAGOON_IMAGE" || exit 1
|
||||
# Capture podman's output BEFORE grepping: under `set -o pipefail` the
|
||||
# `podman images | grep -q` pattern races (grep exits on first match,
|
||||
# podman dies SIGPIPE 141, pipefail turns the 141 into "no match" — the
|
||||
# 2026-09-30 deploy refusal).
|
||||
DIGESTS="$(podman images --format '{{.RepoDigests}}')"
|
||||
if ! grep -qF "${FRANK_LARGE_IMAGE#*@}" <<<"$DIGESTS"; then
|
||||
echo "frank-large: image $FRANK_LARGE_IMAGE not in podman storage;" >&2
|
||||
echo " pull it: podman pull ${FRANK_LARGE_IMAGE%@*}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Size-exact gates on all three payload files: a truncated download can
|
||||
# exit 0, and a partial .hgn loads far enough to look alive, so existence
|
||||
# proves nothing. The byte count published by the Hub tree API is the
|
||||
# contract: any other size is a different (broken) artifact.
|
||||
require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_CHECKPOINT" \
|
||||
checkpoint "$FRANK_DRAGOON_CHECKPOINT_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1
|
||||
require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_NGRAM_TABLE" \
|
||||
n-gram "$FRANK_DRAGOON_NGRAM_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1
|
||||
require_bytes "$FRANK_DRAGOON_MODEL_DIR/$FRANK_DRAGOON_VISION_TOWER" \
|
||||
vision "$FRANK_DRAGOON_VISION_BYTES" \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh" || exit 1
|
||||
# Size-exact gates on all three payload files: a truncated curl exits 0 on
|
||||
# some failures and a partial .hgn loads far enough to look alive, so the
|
||||
# byte counts from the Hub tree API are the contract (fleet fetch law).
|
||||
ck() {
|
||||
# ck <env-name> <path> <expected-bytes>
|
||||
if [ ! -f "$2" ]; then
|
||||
echo "frank-large: $1 $2 missing;" >&2
|
||||
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
local sz
|
||||
sz="$(stat -c %s "$2")"
|
||||
if [ "$sz" != "$3" ]; then
|
||||
echo "frank-large: $1 is $sz bytes, expected $3 (partial download?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
ck checkpoint "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_CHECKPOINT" "$FRANK_LARGE_CHECKPOINT_BYTES"
|
||||
ck n-gram-table "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_NGRAM_TABLE" "$FRANK_LARGE_NGRAM_BYTES"
|
||||
ck vision-tower "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_VISION_TOWER" "$FRANK_LARGE_VISION_BYTES"
|
||||
|
||||
# The payload ships the tokenizer as a FLAT DIRECTORY. A HuggingFace cache
|
||||
# snapshot symlinks each file into a sibling blobs/, and those symlinks
|
||||
# dangle inside the container mount — so the directory must be real and
|
||||
# every named file must be a regular file, not a link.
|
||||
# The tokenizer file list is word-separated ON PURPOSE (one argument per
|
||||
# file), so the unquoted expansion below is the interface, not a slip.
|
||||
# shellcheck disable=SC2086
|
||||
require_dir_files "$FRANK_DRAGOON_MODEL_DIR/tokenizer" tokenizer \
|
||||
"fetch it: bash scripts/download-frank-dragoon-flashnext.sh" \
|
||||
$FRANK_DRAGOON_TOKENIZER_FILES || exit 1
|
||||
TOK_DIR="$FRANK_LARGE_MODEL_DIR/tokenizer"
|
||||
if [ -L "$TOK_DIR" ] || [ ! -d "$TOK_DIR" ]; then
|
||||
echo "frank-large: tokenizer dir $TOK_DIR missing or a symlink (HF cache snapshot?);" >&2
|
||||
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
for TF in $FRANK_LARGE_TOKENIZER_FILES; do
|
||||
if [ ! -f "$TOK_DIR/$TF" ] || [ -L "$TOK_DIR/$TF" ]; then
|
||||
echo "frank-large: tokenizer file $TOK_DIR/$TF missing or a symlink;" >&2
|
||||
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# GPU access is mandatory but the node paths are variables so the seam can
|
||||
# drive the gate on a box without an AMD GPU.
|
||||
DEV_KFD="${FRANK_DRAGOON_DEV_KFD:-/dev/kfd}"
|
||||
DEV_DRI="${FRANK_DRAGOON_DEV_DRI:-/dev/dri}"
|
||||
DEV_KFD="${FRANK_LARGE_DEV_KFD:-/dev/kfd}"
|
||||
DEV_DRI="${FRANK_LARGE_DEV_DRI:-/dev/dri}"
|
||||
if [ ! -e "$DEV_KFD" ] || [ ! -d "$DEV_DRI" ]; then
|
||||
echo "frank-dragoon: $DEV_KFD or $DEV_DRI missing — no gfx1151 to serve on" >&2
|
||||
echo "frank-large: $DEV_KFD or $DEV_DRI missing — no gfx1151 to serve on" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Host-RAM preflight — the engine's pin gate, checked BEFORE podman run.
|
||||
# The engine refuses to pin when MemAvailable is below the checkpoint plus
|
||||
# a 16 GiB floor: pinned pages cannot be swapped, and under the floor the
|
||||
@@ -116,15 +125,15 @@ fi
|
||||
# self-resolved in ~2 min when the dying 27B engine freed RAM. Check here and
|
||||
# WAIT for the RAM to clear, so a transient dip boots once instead of
|
||||
# crash-looping; if it never clears, refuse loudly.
|
||||
MEMINFO="${FRANK_DRAGOON_MEMINFO:-/proc/meminfo}"
|
||||
PIN_KB=$(( FRANK_DRAGOON_CHECKPOINT_BYTES / 1024 ))
|
||||
MEMINFO="${FRANK_LARGE_MEMINFO:-/proc/meminfo}"
|
||||
PIN_KB=$(( FRANK_LARGE_CHECKPOINT_BYTES / 1024 ))
|
||||
NEED_KB=$(( PIN_KB + 16 * 1048576 ))
|
||||
ram_avail_kb() { awk '/^MemAvailable:/{print $2; exit}' "$MEMINFO"; }
|
||||
MEMAVAIL_KB="$(ram_avail_kb)" || MEMAVAIL_KB=""
|
||||
if [ -n "$MEMAVAIL_KB" ] && [ "$MEMAVAIL_KB" -lt "$NEED_KB" ]; then
|
||||
WAIT_S="${FRANK_DRAGOON_RAM_WAIT:-600}"
|
||||
WAIT_S="${FRANK_LARGE_RAM_WAIT:-600}"
|
||||
GIB() { awk -v kb="$1" 'BEGIN{printf "%.2f", kb/1048576}'; }
|
||||
echo "frank-dragoon: host RAM below the pin gate — MemAvailable $(GIB "$MEMAVAIL_KB") GiB < needed $(GIB "$NEED_KB") GiB ($(GIB "$PIN_KB") GiB checkpoint + 16.00 GiB floor); waiting up to ${WAIT_S}s for host RAM to clear" >&2
|
||||
echo "frank-large: host RAM below the pin gate — MemAvailable $(GIB "$MEMAVAIL_KB") GiB < needed $(GIB "$NEED_KB") GiB ($(GIB "$PIN_KB") GiB checkpoint + 16.00 GiB floor); waiting up to ${WAIT_S}s for host RAM to clear" >&2
|
||||
deadline=$(( $(date +%s) + WAIT_S ))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
sleep 5
|
||||
@@ -134,16 +143,16 @@ if [ -n "$MEMAVAIL_KB" ] && [ "$MEMAVAIL_KB" -lt "$NEED_KB" ]; then
|
||||
fi
|
||||
done
|
||||
if [ -z "$MEMAVAIL_KB" ] || [ "$MEMAVAIL_KB" -lt "$NEED_KB" ]; then
|
||||
echo "frank-dragoon: MemAvailable $(GIB "${MEMAVAIL_KB:-0}") GiB after ${WAIT_S}s; refusing to start (the engine would refuse the pin)" >&2
|
||||
echo "frank-large: MemAvailable $(GIB "${MEMAVAIL_KB:-0}") GiB after ${WAIT_S}s; refusing to start (the engine would refuse the pin)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "frank-dragoon: MemAvailable cleared to $(GIB "$MEMAVAIL_KB") GiB; starting" >&2
|
||||
echo "frank-large: MemAvailable cleared to $(GIB "$MEMAVAIL_KB") GiB; starting" >&2
|
||||
fi
|
||||
for CTR in ${FRANK_DRAGOON_LEGACY_CONTAINERS:-} "$FRANK_DRAGOON_CONTAINER"; do
|
||||
for CTR in ${FRANK_LARGE_LEGACY_CONTAINERS:-} "$FRANK_LARGE_CONTAINER"; do
|
||||
podman rm -f "$CTR" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
LOG="${FRANK_DRAGOON_LOG:-/home/dank/logs/frank_dragoon.log}"
|
||||
LOG="${FRANK_LARGE_LOG:-/home/dank/logs/frank_large.log}"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
# --ipc=host for the udev-granted supplementary group, seccomp unconfined
|
||||
# for the ROCm ioctl surface, and unbounded memlock (the unit's
|
||||
@@ -154,17 +163,17 @@ mkdir -p "$(dirname "$LOG")"
|
||||
# and both sibling serve scripts pass no nofile either. No comment lines
|
||||
# inside the backslash chain — a mid-chain # terminates the continuation
|
||||
# (the 2026-10-05 exit-127 crash loop on the sibling serve script).
|
||||
podman run --name "$FRANK_DRAGOON_CONTAINER" \
|
||||
--publish "$FRANK_DRAGOON_PUBLISH" \
|
||||
podman run --name "$FRANK_LARGE_CONTAINER" \
|
||||
--publish "$FRANK_LARGE_PUBLISH" \
|
||||
--ipc=host \
|
||||
--ulimit memlock=-1:-1 \
|
||||
--device "$DEV_KFD" --device "$DEV_DRI" \
|
||||
--group-add keep-groups \
|
||||
--security-opt seccomp=unconfined \
|
||||
-e HALOGEN_MODEL_ID="$HALOGEN_MODEL_ID" \
|
||||
-e HALOGEN_CHECKPOINT="/models/$FRANK_DRAGOON_CHECKPOINT" \
|
||||
-e HALOGEN_NGRAM_TABLE="/models/$FRANK_DRAGOON_NGRAM_TABLE" \
|
||||
-e HALOGEN_VISION_TOWER="/models/$FRANK_DRAGOON_VISION_TOWER" \
|
||||
-e HALOGEN_CHECKPOINT="/models/$FRANK_LARGE_CHECKPOINT" \
|
||||
-e HALOGEN_NGRAM_TABLE="/models/$FRANK_LARGE_NGRAM_TABLE" \
|
||||
-e HALOGEN_VISION_TOWER="/models/$FRANK_LARGE_VISION_TOWER" \
|
||||
-e HALOGEN_TOKENIZER=/tokenizer \
|
||||
-e HALOGEN_CTX="$HALOGEN_CTX" \
|
||||
-e HALOGEN_KV_POOL_POSITIONS="$HALOGEN_KV_POOL_POSITIONS" \
|
||||
@@ -178,9 +187,9 @@ podman run --name "$FRANK_DRAGOON_CONTAINER" \
|
||||
-e HALOGEN_TOP_P="${HALOGEN_TOP_P:-0.95}" \
|
||||
-e HALOGEN_TOP_K="${HALOGEN_TOP_K:-20}" \
|
||||
${HALOGEN_WEIGHTS_LOCK:+-e HALOGEN_WEIGHTS_LOCK="$HALOGEN_WEIGHTS_LOCK"} \
|
||||
-v "$FRANK_DRAGOON_MODEL_DIR:/models:ro" \
|
||||
-v "$FRANK_DRAGOON_MODEL_DIR/tokenizer:/tokenizer:ro" \
|
||||
"$FRANK_DRAGOON_IMAGE" \
|
||||
-v "$FRANK_LARGE_MODEL_DIR:/models:ro" \
|
||||
-v "$FRANK_LARGE_MODEL_DIR/tokenizer:/tokenizer:ro" \
|
||||
"$FRANK_LARGE_IMAGE" \
|
||||
> "$LOG" 2>&1
|
||||
|
||||
# FOREGROUND `podman run`: the engine's stdout/stderr are this script's
|
||||
Executable
+206
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env bash
|
||||
# serve-frank-primary.sh — frank-primary endpoint: radiance (StillDeadcode,
|
||||
# Apache-2.0) serving the OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint at
|
||||
# NATIVE block-FP8 precision from a rad-convert container
|
||||
# (/home/dank/models/rad/qwen4exp-fp8-native.rad), on BOTH R9700s (TP=2,
|
||||
# wht6 wire), dankserver :7770. 2026-10-05 cutover from FreeToken (PR #587,
|
||||
# one card; git tag frank-primary-freetoken-pr587-20261005 is the
|
||||
# pre-cutover state).
|
||||
#
|
||||
# The run line below IS the engine argv: the image's entrypoint is the
|
||||
# engine and its tools sit beside it on PATH. Unlike every predecessor
|
||||
# there is no per-boot VRAM planner to second-guess: the boot ledger in the
|
||||
# log states every budget before anything is allocated (weights vs KV
|
||||
# split, the expert host pool's coverage of the worst case, the PLE pin,
|
||||
# the P2P answer per pair) — read it after every resize. First-boot shape:
|
||||
# 2 decode slots (Frank's call), 40 GiB/rank pinned expert pool, fp8 KV,
|
||||
# MTP depth 3, prefix cache host 4 GiB + disk 64 GiB, PLE pinned in RAM.
|
||||
#
|
||||
# The root unit (scripts/frank-primary.service, User=dank,
|
||||
# LimitMEMLOCK=infinity) grants the pin budget: 2 x 40 GiB expert pools +
|
||||
# the 47.7 GiB PLE table + 4 GiB prefix host tier — ~132 GiB of pinned
|
||||
# pages, which is ALSO why this script refuses to start when ttm's
|
||||
# pages_limit/pages_max cap is under FRANK_PRIMARY_TTM_MIN_MIB (see 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
|
||||
echo "ERROR: frank-primary env file absent: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
: "${FRANK_PRIMARY_IMAGE:?FRANK_PRIMARY_IMAGE must name the radiance image}"
|
||||
: "${FRANK_PRIMARY_MODEL_DIR:?FRANK_PRIMARY_MODEL_DIR must name the .rad dir}"
|
||||
: "${FRANK_PRIMARY_MODEL_FILE:?FRANK_PRIMARY_MODEL_FILE must name the .rad file}"
|
||||
: "${FRANK_PRIMARY_STATE_DIR:?FRANK_PRIMARY_STATE_DIR must name the /data state dir}"
|
||||
: "${FRANK_PRIMARY_CONTAINER:?FRANK_PRIMARY_CONTAINER must name the container}"
|
||||
: "${FRANK_PRIMARY_SERVED_NAME:?FRANK_PRIMARY_SERVED_NAME must name the alias}"
|
||||
: "${FRANK_PRIMARY_PORT:?FRANK_PRIMARY_PORT must be the port}"
|
||||
: "${FRANK_PRIMARY_TEMPLATE:?FRANK_PRIMARY_TEMPLATE must name the chat template}"
|
||||
: "${FRANK_PRIMARY_TP:?FRANK_PRIMARY_TP must be set}"
|
||||
: "${FRANK_PRIMARY_TP_WIRE:?FRANK_PRIMARY_TP_WIRE must be set}"
|
||||
: "${FRANK_PRIMARY_MAX_SEQS:?FRANK_PRIMARY_MAX_SEQS must be set}"
|
||||
: "${FRANK_PRIMARY_MAX_MODEL_LEN:?FRANK_PRIMARY_MAX_MODEL_LEN must be set}"
|
||||
: "${FRANK_PRIMARY_PLACEMENT:?FRANK_PRIMARY_PLACEMENT must be set}"
|
||||
: "${FRANK_PRIMARY_HOST_POOL_MIB:?FRANK_PRIMARY_HOST_POOL_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_GPU_HEADROOM_MIB:?FRANK_PRIMARY_GPU_HEADROOM_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_VRAM_WEIGHTS_MIB:?FRANK_PRIMARY_VRAM_WEIGHTS_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_VRAM_KV_MIB:?FRANK_PRIMARY_VRAM_KV_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_KV_DTYPE:?FRANK_PRIMARY_KV_DTYPE must be set}"
|
||||
: "${FRANK_PRIMARY_SPEC_TOKENS:?FRANK_PRIMARY_SPEC_TOKENS must be set}"
|
||||
: "${FRANK_PRIMARY_MAX_BATCHED_TOKENS:?FRANK_PRIMARY_MAX_BATCHED_TOKENS must be set}"
|
||||
: "${FRANK_PRIMARY_PREFIX_HOST_MIB:?FRANK_PRIMARY_PREFIX_HOST_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_PREFIX_DISK_MIB:?FRANK_PRIMARY_PREFIX_DISK_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_NGRAM_PLACEMENT:?FRANK_PRIMARY_NGRAM_PLACEMENT must be set}"
|
||||
: "${FRANK_PRIMARY_TTM_MIN_MIB:?FRANK_PRIMARY_TTM_MIN_MIB must be set}"
|
||||
: "${FRANK_PRIMARY_RAD_MIN_MIB:?FRANK_PRIMARY_RAD_MIN_MIB must be set}"
|
||||
|
||||
# The image gate is the digest-pinned ref itself: a name@sha256 ref exists
|
||||
# in storage only if THAT manifest was pulled — a retagged look-alike or a
|
||||
# newer latest does not satisfy it. `podman image exists` takes a
|
||||
# name@digest ref in one command — no pipefail/grep race (the 2026-09-30
|
||||
# lesson). Runs as dank: the unit's process uses dank's ROOTLESS podman
|
||||
# storage (root unit, User=dank — the fleet's frank-primary shape).
|
||||
if ! podman image exists "$FRANK_PRIMARY_IMAGE"; then
|
||||
echo "ERROR: radiance image absent: $FRANK_PRIMARY_IMAGE" >&2
|
||||
echo "frank-primary: podman pull docker://stilldeadcode/radiance:latest (as dank)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The payload gate: the .rad container file, sized. A truncated convert
|
||||
# would otherwise buy a guaranteed long-timeout boot instead of this fast
|
||||
# failure. The staging dir / raw checkpoint are NOT this script's concern
|
||||
# (scripts/build-frank-primary-rad.sh built the file; its rad-info output
|
||||
# is the record).
|
||||
RAD="$FRANK_PRIMARY_MODEL_DIR/$FRANK_PRIMARY_MODEL_FILE"
|
||||
if [ ! -f "$RAD" ]; then
|
||||
echo "ERROR: .rad container missing: $RAD" >&2
|
||||
echo "frank-primary: run scripts/build-frank-primary-rad.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
RAD_MIB=$(( $(stat -c %s "$RAD") / 1048576 ))
|
||||
if [ "$RAD_MIB" -lt "$FRANK_PRIMARY_RAD_MIN_MIB" ]; then
|
||||
echo "ERROR: .rad container too small: $RAD is ${RAD_MIB} MiB, want >= $FRANK_PRIMARY_RAD_MIN_MIB" >&2
|
||||
echo "frank-primary: the convert was truncated — rebuild it" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$FRANK_PRIMARY_TEMPLATE" ]; then
|
||||
echo "ERROR: chat template missing: $FRANK_PRIMARY_TEMPLATE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$FRANK_PRIMARY_STATE_DIR/kvcache/frank-primary"
|
||||
# The engine's create is NOT recursive: with only the state dir mounted, its
|
||||
# "cannot create /data/kvcache/frank-primary: No such file or directory"
|
||||
# left the SSD prefix tier off (measured 2026-10-06 boot ledger). Pre-create
|
||||
# the full path on the host so the tier opens on first boot.
|
||||
|
||||
mkdir -p "$FRANK_PRIMARY_STATE_DIR/tune"
|
||||
# rad-tune's kernel-selection cache (see the runbook's "Tuning levers"):
|
||||
# the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune at boot;
|
||||
# $RADIANCE_HOME defaults to /opt/radiance, which lives in the image, so
|
||||
# the cache must ride a host mount or it dies with the --rm container
|
||||
# that wrote it. The dir is pre-created for the same reason as kvcache
|
||||
# (the engine's create is not recursive).
|
||||
|
||||
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
|
||||
echo "ERROR: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TTM preflight: the pinned ledger (~2 x 40 GiB pools + 47.7 GiB PLE + 4 GiB
|
||||
# prefix host = ~132 GiB) rides TTM's shared pages, capped by ttm's
|
||||
# pages_limit (the current kernel exposes NO pages_max; the cap function
|
||||
# takes the min over the knobs that exist). Below FRANK_PRIMARY_TTM_MIN_MIB
|
||||
# the load dies mid-pin with an allocator error instead of this named
|
||||
# refusal — raise the knob first (runtime sysfs write; kernel cmdline
|
||||
# fallback + one reboot only if the file is read-only), see the runbook.
|
||||
# RAISED 2026-10-05 pre-cutover to 52,428,800 pages = 200 GiB.
|
||||
ttm_cap_mib() {
|
||||
# min over the knobs THIS kernel exposes — the current kernel has
|
||||
# pages_limit only (no pages_max); a knob that is absent is skipped,
|
||||
# not a zero (a zero would refuse a healthy cap).
|
||||
local v cur=""
|
||||
for p in /sys/module/ttm/parameters/pages_limit /sys/module/ttm/parameters/pages_max; do
|
||||
[ -r "$p" ] || continue
|
||||
v="$(cat "$p" 2>/dev/null)" || continue
|
||||
[ -n "$v" ] || continue
|
||||
v=$(( v * 4 / 1024 ))
|
||||
if [ "$v" -lt "${cur:-999999999}" ]; then cur=$v; fi
|
||||
done
|
||||
echo "${cur:-0}"
|
||||
}
|
||||
TTM_CAP="$(ttm_cap_mib)"
|
||||
if [ "$TTM_CAP" -lt "$FRANK_PRIMARY_TTM_MIN_MIB" ]; then
|
||||
echo "ERROR: ttm pages cap is ${TTM_CAP} MiB, the pinned ledger needs >= $FRANK_PRIMARY_TTM_MIN_MIB" >&2
|
||||
echo "frank-primary: raise /sys/module/ttm/parameters/pages_limit (and pages_max if the kernel exposes one) to >= $FRANK_PRIMARY_TTM_MIN_MIB in 4 KiB pages" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A crashed/killed unit can leave the container name registered (systemd
|
||||
# SIGKILLs conmon before cleanup runs). Clear OUR name plus the legacy
|
||||
# identities (the FreeToken-era frank-primary-qwen, the retired R9V
|
||||
# container and the ad hoc experiment containers all hold this GPU); all
|
||||
# idempotent.
|
||||
for CTR in $FRANK_PRIMARY_LEGACY_CONTAINERS "$FRANK_PRIMARY_CONTAINER"; do
|
||||
podman rm -f "$CTR" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
LOG=/home/dank/logs/frank_primary.log
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
|
||||
# Publisher-vetted container contract (docs/DOCKER.md, compose/common.yaml):
|
||||
# --network host (the server binds the host's addresses directly, no NAT),
|
||||
# --init (the engine releases the cards on SIGTERM), seccomp=unconfined
|
||||
# (Docker/podman's default profile refuses io_uring, which the n-gram
|
||||
# table's reads go through, and the NUMA binding calls the ROCm runtime
|
||||
# makes for host memory), memlock unbounded (the pinned ledger — legal
|
||||
# because the root unit grants LimitMEMLOCK=infinity to this dank process),
|
||||
# /dev/kfd + /dev/dri (render/video groups are moot for a root container).
|
||||
# The template rides a single-file mount; the .rad dir is ro; /data holds
|
||||
# the prefix-cache disk tier.
|
||||
podman run -d --name "$FRANK_PRIMARY_CONTAINER" \
|
||||
--network host \
|
||||
--init \
|
||||
--security-opt seccomp=unconfined \
|
||||
--ulimit memlock=-1:-1 \
|
||||
--device /dev/kfd --device /dev/dri \
|
||||
-v "$FRANK_PRIMARY_MODEL_DIR":/models:ro \
|
||||
-v "$FRANK_PRIMARY_STATE_DIR":/data \
|
||||
-v "$FRANK_PRIMARY_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||
-v "$FRANK_PRIMARY_TEMPLATE":/templates/template.jinja:ro \
|
||||
"$FRANK_PRIMARY_IMAGE" \
|
||||
--model "/models/$FRANK_PRIMARY_MODEL_FILE" \
|
||||
--host :: --port "$FRANK_PRIMARY_PORT" \
|
||||
--served-model-name "$FRANK_PRIMARY_SERVED_NAME" \
|
||||
--override-chat-template /templates/template.jinja \
|
||||
--tp "$FRANK_PRIMARY_TP" --tp-wire "$FRANK_PRIMARY_TP_WIRE" \
|
||||
--max-num-seqs "$FRANK_PRIMARY_MAX_SEQS" \
|
||||
--max-model-len "$FRANK_PRIMARY_MAX_MODEL_LEN" \
|
||||
--placement "$FRANK_PRIMARY_PLACEMENT" \
|
||||
--host-pool-mib "$FRANK_PRIMARY_HOST_POOL_MIB" \
|
||||
--gpu-headroom-mib "$FRANK_PRIMARY_GPU_HEADROOM_MIB" \
|
||||
--vram-weights-mib "$FRANK_PRIMARY_VRAM_WEIGHTS_MIB" \
|
||||
--vram-kv-mib "$FRANK_PRIMARY_VRAM_KV_MIB" \
|
||||
--kv-cache-dtype "$FRANK_PRIMARY_KV_DTYPE" \
|
||||
--prefix-cache-host-mib "$FRANK_PRIMARY_PREFIX_HOST_MIB" \
|
||||
--prefix-cache-dir /data/kvcache/frank-primary \
|
||||
--prefix-cache-disk-mib "$FRANK_PRIMARY_PREFIX_DISK_MIB" \
|
||||
--ngram-placement "$FRANK_PRIMARY_NGRAM_PLACEMENT" \
|
||||
--num-speculative-tokens "$FRANK_PRIMARY_SPEC_TOKENS" \
|
||||
--max-num-batched-tokens "$FRANK_PRIMARY_MAX_BATCHED_TOKENS" \
|
||||
${FRANK_PRIMARY_EXTRA_ARGS:+$FRANK_PRIMARY_EXTRA_ARGS} \
|
||||
> "$LOG" 2>&1
|
||||
|
||||
# From here on, this script's lifetime IS the container's: wait returns the
|
||||
# container's exit code, so a crash is a nonzero ExecStart and the unit's
|
||||
# Restart=always is real. (systemd stop goes through ExecStop in the unit —
|
||||
# podman wait alone cannot stop the container child.)
|
||||
exec podman wait "$FRANK_PRIMARY_CONTAINER"
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Serve HauhauCS/Qwen3.5-9B-Uncensored-HauhauCS-Aggressive (Q6_K) as
|
||||
# frank-zealot on Dankclaw's RTX 3090 via llama.cpp (master build, CUDA sm_86).
|
||||
# frank-secondary on Dankclaw's RTX 3090 via llama.cpp (master build, CUDA sm_86).
|
||||
#
|
||||
# 2026-10-07 (Frank): Qwen3.5-9B swap at TWO 200,704-token conversations
|
||||
# (--ctx-size 401408 --parallel 2) with EXACT f16 KV — the 9B rung frees the
|
||||
@@ -24,19 +24,11 @@
|
||||
# Sampling: llama.cpp takes it per request (the fleet harness sends the
|
||||
# publisher recipe from models.yml: thinking temp 0.6 / top_p 0.95 / top_k 20).
|
||||
#
|
||||
# Tunables come from frank-zealot.env unless FRANK_ZEALOT_ENV_FILE overrides.
|
||||
# Tunables come from frank-secondary.env unless FRANK_SECONDARY_ENV_FILE overrides.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# The artifact gates are the shared library (scripts/lib/llm-gates.sh): the
|
||||
# serve script and the deployer gate the same payload, and two hand-written
|
||||
# copies of the same checks drift apart until one side passes what the
|
||||
# other refuses.
|
||||
export GATE_PREFIX=frank-zealot
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/lib/llm-gates.sh"
|
||||
ENV_FILE="${FRANK_ZEALOT_ENV_FILE:-$SCRIPT_DIR/frank-zealot.env}"
|
||||
ENV_FILE="${FRANK_SECONDARY_ENV_FILE:-$SCRIPT_DIR/frank-secondary.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
@@ -70,9 +62,9 @@ fi
|
||||
# FP8 class; no true fp8 KV dtype exists in this build).
|
||||
: "${CACHE_TYPE_K:=f16}"
|
||||
: "${CACHE_TYPE_V:=f16}"
|
||||
# Speculative decoding is OFF: this Q6_K file carries NO nextn/mtp tensors,
|
||||
# so there is no in-file head — a speculative head is a property of the
|
||||
# model file, not of the flags; no flag creates tensors that are absent.
|
||||
# Speculative decoding is OFF: this Q6_K file carries NO nextn/mtp tensors, so
|
||||
# there is no in-file head (the law: MTP is a property of the file, not of the
|
||||
# flags). SPEC_TYPE=none strips BOTH speculative flags (flags move as a set);
|
||||
# naming a drafter requires SPEC_DRAFT_FILE alongside SPEC_TYPE — the Gemma4
|
||||
# assistant on disk is arch gemma4-assistant and cannot drafter-match qwen35.
|
||||
: "${SPEC_TYPE:=none}"
|
||||
@@ -89,18 +81,13 @@ fi
|
||||
|
||||
# Fail loudly rather than half-loading: a missing file is a download that
|
||||
# has not finished, and Restart=always would turn it into a crash loop that
|
||||
# reads like a runtime fault. The refusal text is the shared gate library's,
|
||||
# so the deployer's twin refuses identically.
|
||||
require_file "$LLAMA_BIN" runtime \
|
||||
"build it: scripts/build-llama-master-cuda.sh" || exit 1
|
||||
require_file "$MODEL_FILE" gguf \
|
||||
"fetch it: bash scripts/download-frank-zealot.sh" || exit 1
|
||||
if [[ -n "$MMPROJ_FILE" ]]; then
|
||||
require_file "$MMPROJ_FILE" mmproj \
|
||||
"fetch it: bash scripts/download-frank-zealot.sh" || exit 1
|
||||
fi
|
||||
# reads like a runtime fault.
|
||||
for path in "$LLAMA_BIN" "$MODEL_FILE"; do
|
||||
[[ -e "$path" ]] || { echo "frank-secondary: missing $path" >&2; exit 1; }
|
||||
done
|
||||
EXTRA_ARGS=()
|
||||
if [[ -n "$MMPROJ_FILE" ]]; then
|
||||
[[ -e "$MMPROJ_FILE" ]] || { echo "frank-secondary: missing $MMPROJ_FILE" >&2; exit 1; }
|
||||
EXTRA_ARGS+=(--mmproj "$MMPROJ_FILE")
|
||||
fi
|
||||
|
||||
@@ -110,7 +97,7 @@ fi
|
||||
if [[ -n "$SPEC_TYPE" && "$SPEC_TYPE" != "none" ]]; then
|
||||
EXTRA_ARGS+=(--spec-type "$SPEC_TYPE" --spec-draft-n-max "$SPEC_DRAFT_N_MAX")
|
||||
if [[ -n "$SPEC_DRAFT_FILE" ]]; then
|
||||
[[ -e "$SPEC_DRAFT_FILE" ]] || { echo "frank-zealot: missing $SPEC_DRAFT_FILE" >&2; exit 1; }
|
||||
[[ -e "$SPEC_DRAFT_FILE" ]] || { echo "frank-secondary: missing $SPEC_DRAFT_FILE" >&2; exit 1; }
|
||||
EXTRA_ARGS+=(--spec-draft-model "$SPEC_DRAFT_FILE")
|
||||
fi
|
||||
fi
|
||||
@@ -121,7 +108,7 @@ nvidia-modprobe -u -c=0 2>/dev/null || true
|
||||
|
||||
# Flash attention stays pinned on: it is REQUIRED for quantized V (q8_0),
|
||||
# and the build carries GGML_CUDA_FA_ALL_QUANTS=ON (since 2026-09-17), so
|
||||
# the q8_0 KV contract in frank-zealot.env is a real path — without this
|
||||
# the q8_0 KV contract in frank-secondary.env is a real path — without this
|
||||
# flag or that build option the engine silently falls back and the
|
||||
# contract becomes a lie.
|
||||
ARGS=(
|
||||
@@ -1,750 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""service-map.py — the only reader of the service inventory.
|
||||
|
||||
Four lists used to agree by discipline (the planner's path rules, the
|
||||
deployer's allow-list, the changed-test mapper, and the scrape/probe/DNS
|
||||
facts written by hand in three config files). They now all read
|
||||
`shared/services.yml` through this tool:
|
||||
|
||||
hosts the deploy targets
|
||||
services [--host H] deployables a target accepts, canonical order
|
||||
allows --host H --service S exit 0 when H accepts S (silent)
|
||||
plan --host H PATH... services selected by changed paths, order-safe
|
||||
seams [--host H] PATH... the Bats seams a changed path must re-run
|
||||
owner --host H PATH the service that owns a file (empty: none)
|
||||
units --host H --service S "<unit> <unit_dir>" lines the handler installs
|
||||
stack --host H --service S the deployable + members one handler deploys
|
||||
get --service S [--host H] F one field as JSON
|
||||
render pihole-hosts the Pi-hole addn-hosts file
|
||||
check [NAME...] integrity + drift gates (exit 1 on drift)
|
||||
|
||||
Two rules the engine enforces structurally:
|
||||
|
||||
* A path selects only services that RUN on the planned target. Path
|
||||
matching that is host-blind (match first, filter later) lets a file on
|
||||
one host plan a service on another; selection is scoped to the host the
|
||||
path belongs to.
|
||||
* The netns family is DERIVED, never declared: a unit file that says
|
||||
`Network=gluetun.container` rides that tunnel, so a gluetun change selects
|
||||
every sharer. The unit files are the source of that coupling.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_INVENTORY = REPO_ROOT / "shared" / "services.yml"
|
||||
|
||||
REQUIRED_KEYS = ("name", "hosts", "kind", "tests")
|
||||
KINDS = {"quadlet", "root-unit", "vm-quadlet", "host"}
|
||||
UNIT_SUFFIXES = (".container", ".network")
|
||||
|
||||
# Repo material that is never a deployed runtime's state.
|
||||
NON_RUNTIME_ROOTS = {"docs", "report", "old", ".githooks", ".obsidian", ".github",
|
||||
"tests", "serverAppConfigs-docs"}
|
||||
NON_RUNTIME_NAMES = {".env.example"}
|
||||
NON_RUNTIME_PATHS = {"README.md", "AGENTS.md", ".gitignore"}
|
||||
|
||||
|
||||
def is_trash(path: str) -> bool:
|
||||
return ".AGENTTRASH" in PurePosixPath(path).parts
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# inventory
|
||||
# --------------------------------------------------------------------------
|
||||
class Inventory:
|
||||
def __init__(self, doc: dict[str, Any], origin: str) -> None:
|
||||
self.origin = origin
|
||||
self.hosts: dict[str, dict[str, Any]] = doc["hosts"]
|
||||
self.services: list[dict[str, Any]] = doc["services"]
|
||||
self.unowned: dict[str, str] = doc.get("unowned", {})
|
||||
for index, svc in enumerate(self.services):
|
||||
svc["_index"] = index
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path = DEFAULT_INVENTORY) -> "Inventory":
|
||||
doc = yaml.safe_load(Path(path).read_text())
|
||||
if not isinstance(doc, dict) or "services" not in doc:
|
||||
raise SystemExit(f"{path}: not a service inventory document")
|
||||
return cls(doc, str(path))
|
||||
|
||||
def on(self, host: str) -> list[dict[str, Any]]:
|
||||
return [s for s in self.services if host in s["hosts"]]
|
||||
|
||||
def named(self, host: str, name: str) -> list[dict[str, Any]]:
|
||||
return [s for s in self.services if s["name"] == name and host in s["hosts"]]
|
||||
|
||||
def any_named(self, name: str) -> list[dict[str, Any]]:
|
||||
return [s for s in self.services if s["name"] == name]
|
||||
|
||||
@property
|
||||
def order(self) -> list[str]:
|
||||
seen: list[str] = []
|
||||
for svc in self.services:
|
||||
if svc["name"] not in seen:
|
||||
seen.append(svc["name"])
|
||||
return seen
|
||||
|
||||
def edge_ip(self) -> str:
|
||||
"""The single address every DNS name resolves at (Caddy's edge)."""
|
||||
for host in self.hosts.values():
|
||||
if "dns_edge" in host:
|
||||
return str(host["dns_edge"])
|
||||
raise SystemExit(f"{self.origin}: no host declares dns_edge")
|
||||
|
||||
def service(self, name: str) -> dict | None:
|
||||
for svc in self.services:
|
||||
if svc["name"] == name:
|
||||
return svc
|
||||
return None
|
||||
|
||||
def unit_dirs(self, svc: dict[str, Any], host: str | None = None) -> list[str]:
|
||||
"""The repo dirs holding this service's unit files, for one host."""
|
||||
dirs = svc.get("unit_dir")
|
||||
if not dirs:
|
||||
return []
|
||||
if isinstance(dirs, dict):
|
||||
if host is not None:
|
||||
return [dirs[host]] if host in dirs else []
|
||||
return [dirs[h] for h in svc["hosts"] if h in dirs]
|
||||
return [dirs]
|
||||
|
||||
def owns_unit(self, svc: dict[str, Any], host: str, unit: str, directory: str) -> bool:
|
||||
return unit in svc.get("units", []) and directory in self.unit_dirs(svc, host)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# repo facts
|
||||
# --------------------------------------------------------------------------
|
||||
@lru_cache(maxsize=1)
|
||||
def tracked_files() -> tuple[str, ...]:
|
||||
# Tracked + untracked (a check that only saw HEAD would ignore the very
|
||||
# files a work-in-progress commit is adding).
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(REPO_ROOT), "ls-files", "--cached", "--others",
|
||||
"--exclude-standard"],
|
||||
text=True, stdout=subprocess.PIPE, check=True)
|
||||
return tuple(line for line in out.stdout.splitlines() if line)
|
||||
|
||||
|
||||
def _network_values(container: Path) -> list[str]:
|
||||
"""Every `Network=` value a quadlet unit declares (comma lists included)."""
|
||||
values: list[str] = []
|
||||
try:
|
||||
text = container.read_text()
|
||||
except OSError:
|
||||
return values
|
||||
for line in text.splitlines():
|
||||
if line.startswith("Network="):
|
||||
values.extend(v.strip() for v in line[len("Network="):].split(",") if v.strip())
|
||||
return values
|
||||
|
||||
|
||||
class Graph:
|
||||
"""The unit-file coupling, read out of the tree: netns + netns-net users.
|
||||
|
||||
Quadlet installs every user unit flat, so `Network=gluetun.container`
|
||||
names a unit by name, not by path; the same host is the scope, which is
|
||||
also the deployment scope.
|
||||
"""
|
||||
|
||||
def __init__(self, hosts: list[str]) -> None:
|
||||
self.units: dict[tuple[str, str], str] = {} # (host, unit) -> path
|
||||
self.networks: dict[tuple[str, str], str] = {} # (host, net) -> path
|
||||
self.netns_children: dict[tuple[str, str], set[tuple[str, str]]] = {}
|
||||
self.network_users: dict[tuple[str, str], set[tuple[str, str]]] = {}
|
||||
for path in tracked_files():
|
||||
parts = PurePosixPath(path).parts
|
||||
if not parts or parts[0] not in hosts or is_trash(path):
|
||||
continue
|
||||
host, directory, name = parts[0], str(PurePosixPath(*parts[1:-1])), parts[-1]
|
||||
if name.endswith(".container"):
|
||||
self.units[(host, name[: -len(".container")])] = path
|
||||
elif name.endswith(".network"):
|
||||
self.networks[(host, name[: -len(".network")])] = path
|
||||
for (host, unit), path in self.units.items():
|
||||
for value in _network_values(REPO_ROOT / path):
|
||||
if value.endswith(".container"):
|
||||
parent = (host, value[: -len(".container")])
|
||||
self.netns_children.setdefault(parent, set()).add((host, unit))
|
||||
elif value.endswith(".network"):
|
||||
net = (host, value[: -len(".network")])
|
||||
self.network_users.setdefault(net, set()).add((host, unit))
|
||||
|
||||
def rides(self, start: list[tuple[str, str]]) -> set[tuple[str, str]]:
|
||||
"""The transitive netns closure: a unit, everything riding it, and
|
||||
everything riding those (a netns inside a netns)."""
|
||||
found: set[tuple[str, str]] = set()
|
||||
frontier = list(start)
|
||||
while frontier:
|
||||
node = frontier.pop()
|
||||
if node in found:
|
||||
continue
|
||||
found.add(node)
|
||||
frontier.extend(self.netns_children.get(node, ()))
|
||||
return found
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def graph_for(hosts: tuple[str, ...]) -> Graph:
|
||||
return Graph(list(hosts))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# selection
|
||||
# --------------------------------------------------------------------------
|
||||
def _owner_index(inv: Inventory) -> dict[tuple[str, str, int]]:
|
||||
"""(host, unit) -> service index, for unit-file ownership.
|
||||
|
||||
A quadlet unit name is unique per host (``check units`` enforces one
|
||||
claim), so the coupling closure can resolve a riding unit that lives in
|
||||
a DIFFERENT directory than the changed file — gluetun's netns children
|
||||
are the point.
|
||||
"""
|
||||
index: dict[tuple[str, str], int] = {}
|
||||
for svc in inv.services:
|
||||
for host in svc["hosts"]:
|
||||
for unit in svc.get("units", []):
|
||||
key = (host, unit)
|
||||
if key in index and index[key] != svc["_index"]:
|
||||
raise SystemExit(
|
||||
f"{inv.origin}: unit {unit!r} on {host} claimed by "
|
||||
f"{inv.services[index[key]]['name']} and {svc['name']}"
|
||||
)
|
||||
index[key] = svc["_index"]
|
||||
return index
|
||||
|
||||
|
||||
def select(inv: Inventory, host: str, paths: list[str]) -> list[str]:
|
||||
"""Canonical-order service names the changed paths select for one host."""
|
||||
if host not in inv.hosts:
|
||||
raise SystemExit(f"{inv.origin}: no host {host!r}")
|
||||
graph = graph_for(tuple(inv.hosts))
|
||||
owners = _owner_index(inv)
|
||||
hits: set[int] = set()
|
||||
|
||||
def add(index: int) -> None:
|
||||
hits.add(index)
|
||||
|
||||
for raw in paths:
|
||||
path = raw.replace("\\", "/")
|
||||
parts = PurePosixPath(path).parts
|
||||
if not parts or is_trash(path) or path in NON_RUNTIME_PATHS:
|
||||
continue
|
||||
if PurePosixPath(path).name in NON_RUNTIME_NAMES:
|
||||
continue
|
||||
head = parts[0]
|
||||
if head in NON_RUNTIME_ROOTS:
|
||||
continue
|
||||
# A host's directory is the scope: a frankbook-pro file can never
|
||||
# select a dankserver service.
|
||||
scoped_host = head if head in inv.hosts else None
|
||||
if scoped_host is not None and scoped_host != host:
|
||||
continue
|
||||
|
||||
name = parts[-1]
|
||||
directory = str(PurePosixPath(*parts[1:-1])) if len(parts) > 1 else ""
|
||||
|
||||
if scoped_host and name.endswith(UNIT_SUFFIXES):
|
||||
stem = name.rsplit(".", 1)[0]
|
||||
key = (scoped_host, stem)
|
||||
if name.endswith(".network") and key in graph.networks:
|
||||
users = graph.network_users.get(key, set())
|
||||
for unit in graph.rides(sorted(users)):
|
||||
index = owners.get(unit)
|
||||
if index is not None:
|
||||
add(index)
|
||||
elif key in graph.units:
|
||||
for unit in graph.rides([key]):
|
||||
index = owners.get(unit)
|
||||
if index is not None:
|
||||
add(index)
|
||||
|
||||
for index in list(hits):
|
||||
for dependent in inv.services[index].get("dependents", []):
|
||||
for svc in inv.named(host, dependent):
|
||||
add(svc["_index"])
|
||||
|
||||
for svc in inv.on(host):
|
||||
if path in svc.get("files", []):
|
||||
add(svc["_index"])
|
||||
if path in svc.get("retires", []):
|
||||
add(svc["_index"])
|
||||
for directory_rule in svc.get("dirs", []):
|
||||
if path == directory_rule or path.startswith(directory_rule + "/"):
|
||||
add(svc["_index"])
|
||||
|
||||
return [svc["name"] for svc in sorted((inv.services[i] for i in hits),
|
||||
key=lambda s: s["_index"])]
|
||||
|
||||
|
||||
def seams_for(inv: Inventory, host: str, paths: list[str]) -> list[str]:
|
||||
"""The Bats seams the selected services require, deduplicated, in order."""
|
||||
seams: list[str] = []
|
||||
for name in select(inv, host, paths):
|
||||
for svc in inv.named(host, name):
|
||||
for seam in svc.get("tests", []):
|
||||
if seam not in seams:
|
||||
seams.append(seam)
|
||||
if seams and "tests/deployment_scoping.bats" not in seams:
|
||||
seams.append("tests/deployment_scoping.bats")
|
||||
return seams
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# renderers + facts
|
||||
# --------------------------------------------------------------------------
|
||||
def render_pihole_hosts(inv: Inventory) -> str:
|
||||
edge = inv.edge_ip()
|
||||
lines = [
|
||||
"# A records for the flat franksdank.cloud scheme (dnsmasq addn-hosts",
|
||||
"# format: <IP> <name>). RENDERED — do not edit by hand:",
|
||||
"#",
|
||||
"# scripts/service-map.py render pihole-hosts > dankclaw/pihole/addn-hosts",
|
||||
"#",
|
||||
"# The source of truth is the `dns:` field of shared/services.yml; the",
|
||||
"# drift gate in tests/service_inventory.bats fails when this file and",
|
||||
"# the inventory disagree. Caddy is consolidated on Dankclaw, so every",
|
||||
"# name — including the services running on dankserver and",
|
||||
"# frankbook-pro — resolves at the Dankclaw edge.",
|
||||
"#",
|
||||
"# dnsmasq strips ONLY whole-line comments in addn-hosts: a trailing",
|
||||
"# comment on a data line becomes a HOSTNAME token. No inline notes",
|
||||
"# here; the service attribution lives in the inventory.",
|
||||
"",
|
||||
]
|
||||
for svc in inv.services:
|
||||
for name in svc.get("dns", []):
|
||||
lines.append(f"{edge} {name}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def scrape_facts(inv: Inventory) -> list[dict[str, str]]:
|
||||
facts = []
|
||||
for svc in inv.services:
|
||||
for job in svc.get("scrape", []):
|
||||
facts.append({"job": job["job"], "target": job["target"],
|
||||
"metrics_path": job.get("metrics_path", "/")})
|
||||
return facts
|
||||
|
||||
|
||||
def prometheus_facts(path: Path) -> list[dict[str, str]]:
|
||||
doc = yaml.safe_load(path.read_text())
|
||||
facts = []
|
||||
for job in doc.get("scrape_configs", []):
|
||||
for sc in job.get("static_configs", []):
|
||||
for target in sc.get("targets", []):
|
||||
facts.append({"job": job["job_name"], "target": target,
|
||||
"metrics_path": job.get("metrics_path", "/")})
|
||||
return facts
|
||||
|
||||
|
||||
def probe_facts(inv: Inventory) -> list[str]:
|
||||
facts: list[str] = []
|
||||
for svc in inv.services:
|
||||
probe = svc.get("probe")
|
||||
if isinstance(probe, dict):
|
||||
facts.extend(str(v) for v in probe.values())
|
||||
elif probe:
|
||||
facts.append(str(probe))
|
||||
return facts
|
||||
|
||||
|
||||
def homepage_probes(path: Path) -> list[str]:
|
||||
return sorted({s["probe"] for s in json.loads(path.read_text()) if s.get("probe")})
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# checks
|
||||
# --------------------------------------------------------------------------
|
||||
def check_ownership(inv: Inventory, graph: Graph) -> list[str]:
|
||||
"""Every repo file belongs to a service, a rule, or the unowned list."""
|
||||
problems: list[str] = []
|
||||
exact: set[str] = set()
|
||||
directories: set[str] = set()
|
||||
for svc in inv.services:
|
||||
for host in svc["hosts"]:
|
||||
for directory in inv.unit_dirs(svc, host):
|
||||
for unit in svc.get("units", []):
|
||||
exact.add(f"{directory}/{unit}.container")
|
||||
for field in ("files", "retires"):
|
||||
exact.update(svc.get(field, []))
|
||||
directories.update(svc.get("dirs", []))
|
||||
# A shared network file is claimed by the units that ride it; the unit
|
||||
# graph is the referrer, and check_units proves every reference resolves.
|
||||
exact.update(graph.networks.values())
|
||||
|
||||
def is_claimed(path: str) -> bool:
|
||||
if path in exact:
|
||||
return True
|
||||
first = PurePosixPath(path).parts[0]
|
||||
if first in NON_RUNTIME_ROOTS or path in NON_RUNTIME_PATHS or is_trash(path):
|
||||
return True
|
||||
if PurePosixPath(path).name in NON_RUNTIME_NAMES:
|
||||
return True
|
||||
for owned in directories | set(inv.unowned):
|
||||
if path == owned or path.startswith(owned + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
for path in tracked_files():
|
||||
if not is_claimed(path):
|
||||
problems.append(f"file belongs to no service and is not in unowned: {path}")
|
||||
|
||||
# The reverse direction: an unowned entry that names nothing is stale.
|
||||
for directory in inv.unowned:
|
||||
if not any(p == directory or p.startswith(directory + "/") for p in tracked_files()):
|
||||
problems.append(f"unowned entry names no tracked file: {directory}")
|
||||
|
||||
# Every inventory file/dir claim names a tracked file.
|
||||
for svc in inv.services:
|
||||
# `retires` names a file whose DELETION selects the service, so by
|
||||
# definition it is not in the tree.
|
||||
for field in ("files", "dirs"):
|
||||
for entry in svc.get(field, []):
|
||||
if entry not in tracked_files() and not any(
|
||||
p.startswith(entry + "/") for p in tracked_files()):
|
||||
problems.append(f"{svc['name']}: {field} entry names no tracked file: {entry}")
|
||||
del graph
|
||||
return problems
|
||||
|
||||
|
||||
def check_units(inv: Inventory, graph: Graph) -> list[str]:
|
||||
"""Every unit file has exactly one owner; every unit reference resolves."""
|
||||
problems: list[str] = []
|
||||
owners: dict[str, list[str]] = {}
|
||||
for svc in inv.services:
|
||||
for host in svc["hosts"]:
|
||||
for directory in inv.unit_dirs(svc, host):
|
||||
for unit in svc.get("units", []):
|
||||
path = f"{directory}/{unit}.container"
|
||||
owners.setdefault(path, []).append(f"{svc['name']}@{host}")
|
||||
if path not in tracked_files():
|
||||
problems.append(f"{svc['name']}: unit file is not tracked: {path}")
|
||||
for path in tracked_files():
|
||||
if not path.endswith(".container") or is_trash(path):
|
||||
continue
|
||||
found = owners.get(path, [])
|
||||
if not found:
|
||||
problems.append(f"unit file claimed by no service: {path}")
|
||||
elif len(found) > 1:
|
||||
problems.append(f"unit file claimed by {len(found)} services: {path}: {found}")
|
||||
|
||||
# Root units (scripts/*.service) are claimed by exactly one `files:` list.
|
||||
for path in tracked_files():
|
||||
if not (path.startswith("scripts/") and path.endswith(".service")) or is_trash(path):
|
||||
continue
|
||||
found = [s["name"] for s in inv.services if path in s.get("files", [])]
|
||||
if not found:
|
||||
problems.append(f"root unit claimed by no service: {path}")
|
||||
elif len(found) > 1:
|
||||
problems.append(f"root unit claimed by {found}: {path}")
|
||||
|
||||
# Every Network= reference in a unit file names a file on the same host.
|
||||
for (host, unit), path in graph.units.items():
|
||||
for value in _network_values(REPO_ROOT / path):
|
||||
if value.endswith(".container"):
|
||||
target = (host, value[: -len(".container")])
|
||||
if target not in graph.units:
|
||||
problems.append(f"{path}: Network={value} names no unit on {host}")
|
||||
elif value.endswith(".network"):
|
||||
target = (host, value[: -len(".network")])
|
||||
if target not in graph.networks:
|
||||
problems.append(f"{path}: Network={value} names no network on {host}")
|
||||
return problems
|
||||
|
||||
|
||||
def check_config(inv: Inventory) -> list[str]:
|
||||
problems: list[str] = []
|
||||
for svc in inv.services:
|
||||
label = f"{'+'.join(svc['hosts'])}/{svc['name']}"
|
||||
for key in REQUIRED_KEYS:
|
||||
if key not in svc:
|
||||
problems.append(f"{label}: missing required key {key}")
|
||||
if svc.get("kind") not in KINDS:
|
||||
problems.append(f"{label}: kind {svc.get('kind')!r} not one of {sorted(KINDS)}")
|
||||
unknown = set(svc.get("hosts", [])) - set(inv.hosts)
|
||||
if unknown:
|
||||
problems.append(f"{label}: undeclared hosts: {sorted(unknown)}")
|
||||
if svc.get("units") and not svc.get("unit_dir"):
|
||||
problems.append(f"{label}: units declared without unit_dir")
|
||||
if svc.get("deployed_by") and not inv.any_named(svc["deployed_by"]):
|
||||
problems.append(f"{label}: deployed_by names no service: {svc['deployed_by']}")
|
||||
if (svc.get("health") and svc.get("wait") is None) or \
|
||||
(svc.get("wait") is not None and not svc.get("health")):
|
||||
problems.append(f"{label}: health and wait are a pair")
|
||||
for seam in svc.get("tests", []):
|
||||
if seam not in tracked_files():
|
||||
problems.append(f"{label}: seam does not exist: {seam}")
|
||||
for name in svc.get("dns", []):
|
||||
if not str(name).replace(".", "").replace("-", "").isalnum():
|
||||
problems.append(f"{label}: dns name is not a plain hostname: {name}")
|
||||
if svc.get("kind") == "root-unit" and svc.get("unit_dir"):
|
||||
problems.append(f"{label}: a root unit lives in scripts/, not unit_dir")
|
||||
seen: set[str] = set()
|
||||
for svc in inv.services:
|
||||
key = (svc["name"], tuple(svc["hosts"]))
|
||||
if key in seen:
|
||||
problems.append(f"duplicate inventory entry: {svc['name']} on {svc['hosts']}")
|
||||
seen.add(key)
|
||||
return problems
|
||||
|
||||
|
||||
# --- node-exporter quadlet template (rank 6) --------------------------------
|
||||
# Parallel per-host copies kept in step by a comment drift apart; the
|
||||
# template is the single source, and `check` drift-gates every tracked
|
||||
# rendering against it.
|
||||
NODE_EXPORTER_TEMPLATE = """\
|
||||
# Quadlet unit: node-exporter (rootless, boot-persistent) — RENDERED ARTIFACT.
|
||||
# Regenerate: python3 scripts/service-map.py render node-exporter-unit --host {host}
|
||||
# Source: shared/services.yml (entry node-exporter) + this template.
|
||||
#
|
||||
# Install path on {host}: ~/.config/containers/systemd/node-exporter.container
|
||||
# Unit name: node-exporter.service (container name: node-exporter)
|
||||
#
|
||||
# Host metrics source for Prometheus. Replaces the compose service of the same
|
||||
# name (the "Observability" block): Dankclaw moved to this quadlet on
|
||||
# 2026-09-11, Dankserver on 2026-09-16 — its compose-only copy died at a reboot
|
||||
# and stayed dead (rootless Podman does not restart bare containers after the
|
||||
# explicit stop shutdown performs), dead for a day and a half before anyone
|
||||
# noticed. One owner per container name; no compose file remains anywhere.
|
||||
#
|
||||
# HOST NAMESPACE. It must run in the host network namespace to report the
|
||||
# host's own interfaces/connections, and --pid=host for the processes and
|
||||
# cgroups collectors. "localhost" inside the container is therefore the host's
|
||||
# loopback, which is what the homepage status timer probes.
|
||||
#
|
||||
# LISTEN ADDRESSES. Exactly two: loopback for the homepage status probe, and
|
||||
# the LAN address, because Prometheus — which runs in a container on
|
||||
# dankclaw_default — scrapes http://{lan}:{port}/metrics
|
||||
# (shared/prometheus/prometheus.yml, job "node-{host}"). node_exporter has no
|
||||
# authentication, so the wildcard bind stays off the table; this is the
|
||||
# pairing that removes the unauthenticated observability surface on every
|
||||
# host, and both consumers are accounted for.
|
||||
#
|
||||
# MOUNTS. The container reads the host's /proc, /sys and / as /host/*
|
||||
# read-only and is pointed at them with --path.*; it writes nothing anywhere.
|
||||
|
||||
[Unit]
|
||||
Description=Prometheus node exporter ({title} host metrics)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Container]
|
||||
ContainerName=node-exporter
|
||||
Image=docker.io/prom/node-exporter:latest
|
||||
Network=host
|
||||
PodmanArgs=--pid=host
|
||||
Exec=--path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/rootfs --web.listen-address=127.0.0.1:{port} --web.listen-address={lan}:{port}
|
||||
Volume=/proc:/host/proc:ro
|
||||
Volume=/sys:/host/sys:ro
|
||||
Volume=/:/rootfs:ro
|
||||
HealthCmd=wget --spider -q http://localhost:{port}/metrics
|
||||
HealthInterval=30s
|
||||
HealthTimeout=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=30s
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
"""
|
||||
|
||||
|
||||
def render_node_exporter_unit(inv: Inventory, host: str) -> str:
|
||||
"""The node-exporter quadlet for one host: shared template, per-host
|
||||
title, LAN address from the hosts map, port from the scrape job."""
|
||||
svc = inv.service("node-exporter")
|
||||
if svc is None:
|
||||
raise SystemExit("inventory has no node-exporter entry")
|
||||
job = f"node-{host}"
|
||||
port = next((str(e["target"]).rsplit(":", 1)[1] for e in svc.get("scrape", [])
|
||||
if e.get("job") == job), None)
|
||||
if port is None:
|
||||
raise SystemExit(f"node-exporter scrape declares no job {job}")
|
||||
lan = inv.hosts[host]["address"]
|
||||
return NODE_EXPORTER_TEMPLATE.format(host=host, title=host.capitalize(),
|
||||
lan=lan, port=port)
|
||||
|
||||
|
||||
def check_drift(inv: Inventory) -> list[str]:
|
||||
problems: list[str] = []
|
||||
rendered = render_pihole_hosts(inv)
|
||||
hosts_file = REPO_ROOT / "dankclaw" / "pihole" / "addn-hosts"
|
||||
if hosts_file.read_text() != rendered:
|
||||
problems.append("dankclaw/pihole/addn-hosts is not the render of the inventory "
|
||||
"(scripts/service-map.py render pihole-hosts > dankclaw/pihole/addn-hosts)")
|
||||
|
||||
want = sorted(json.dumps(f, sort_keys=True) for f in scrape_facts(inv))
|
||||
have = sorted(json.dumps(f, sort_keys=True) for f in
|
||||
prometheus_facts(REPO_ROOT / "shared" / "prometheus" / "prometheus.yml"))
|
||||
for extra in [f for f in have if f not in want]:
|
||||
problems.append(f"prometheus scrapes a target the inventory does not declare: {extra}")
|
||||
for missing in [f for f in want if f not in have]:
|
||||
problems.append(f"inventory declares a scrape prometheus.yml lacks: {missing}")
|
||||
|
||||
svc = inv.service("node-exporter")
|
||||
for host in (svc or {}).get("hosts", []):
|
||||
for directory in inv.unit_dirs(svc, host):
|
||||
path = REPO_ROOT / directory / "node-exporter.container"
|
||||
if path.exists() and path.read_text() != render_node_exporter_unit(inv, host):
|
||||
problems.append(
|
||||
f"{directory}/node-exporter.container is not the render of the "
|
||||
f"template (scripts/service-map.py render node-exporter-unit --host {host})")
|
||||
|
||||
probes_want = sorted(set(probe_facts(inv)))
|
||||
probes_have = homepage_probes(REPO_ROOT / "shared" / "homepage" / "services.json")
|
||||
for extra in [p for p in probes_have if p not in probes_want]:
|
||||
problems.append(f"homepage probes a target the inventory does not declare: {extra}")
|
||||
for missing in [p for p in probes_want if p not in probes_have]:
|
||||
problems.append(f"inventory declares a probe homepage does not list: {missing}")
|
||||
return problems
|
||||
|
||||
|
||||
CHECKS = {
|
||||
"config": lambda inv, graph: check_config(inv),
|
||||
"units": check_units,
|
||||
"ownership": check_ownership,
|
||||
"drift": lambda inv, graph: check_drift(inv),
|
||||
}
|
||||
|
||||
|
||||
def check(inv: Inventory, names: list[str]) -> list[str]:
|
||||
graph = graph_for(tuple(inv.hosts))
|
||||
problems: list[str] = []
|
||||
for name in names or list(CHECKS):
|
||||
problems.extend(CHECKS[name](inv, graph))
|
||||
return problems
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# CLI
|
||||
# --------------------------------------------------------------------------
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--inventory", type=Path, default=DEFAULT_INVENTORY)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
sub.add_parser("hosts")
|
||||
p = sub.add_parser("services")
|
||||
p.add_argument("--host")
|
||||
p = sub.add_parser("allows")
|
||||
p.add_argument("--host", required=True)
|
||||
p.add_argument("--service", required=True)
|
||||
p = sub.add_parser("plan")
|
||||
p.add_argument("--host", required=True)
|
||||
p.add_argument("paths", nargs="+")
|
||||
p = sub.add_parser("seams")
|
||||
p.add_argument("--host")
|
||||
p.add_argument("paths", nargs="+")
|
||||
p = sub.add_parser("owner")
|
||||
p.add_argument("--host", required=True)
|
||||
p.add_argument("path")
|
||||
p = sub.add_parser("units")
|
||||
p.add_argument("--host", required=True)
|
||||
p.add_argument("--service", required=True)
|
||||
p = sub.add_parser("stack")
|
||||
p.add_argument("--host", required=True)
|
||||
p.add_argument("--service", required=True)
|
||||
p = sub.add_parser("get")
|
||||
p.add_argument("--host")
|
||||
p.add_argument("--service", required=True)
|
||||
p.add_argument("field")
|
||||
p = sub.add_parser("render")
|
||||
p.add_argument("artifact", choices=("pihole-hosts", "node-exporter-unit"))
|
||||
p.add_argument("--host", choices=list(Hosts) if False else None)
|
||||
p = sub.add_parser("check")
|
||||
p.add_argument("names", nargs="*")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
inv = Inventory.load(args.inventory)
|
||||
|
||||
if args.command == "hosts":
|
||||
print("\n".join(inv.hosts))
|
||||
elif args.command == "services":
|
||||
services = inv.on(args.host) if args.host else inv.services
|
||||
seen: list[str] = []
|
||||
for svc in services:
|
||||
if svc["name"] not in seen:
|
||||
seen.append(svc["name"])
|
||||
print("\n".join(seen))
|
||||
elif args.command == "allows":
|
||||
return 0 if inv.named(args.host, args.service) else 1
|
||||
elif args.command == "plan":
|
||||
print("\n".join(select(inv, args.host, args.paths)))
|
||||
elif args.command == "seams":
|
||||
if args.host is not None:
|
||||
seams = seams_for(inv, args.host, args.paths)
|
||||
else:
|
||||
# No host: the union across every host (the dev machine's
|
||||
# pre-commit mapper has no deployment target).
|
||||
seams = []
|
||||
for host in inv.hosts:
|
||||
for seam in seams_for(inv, host, args.paths):
|
||||
if seam not in seams:
|
||||
seams.append(seam)
|
||||
print("\n".join(seams))
|
||||
elif args.command == "owner":
|
||||
found = select(inv, args.host, [args.path])
|
||||
print(found[-1] if found else "")
|
||||
elif args.command == "units":
|
||||
matches = inv.named(args.host, args.service)
|
||||
if not matches:
|
||||
print(f"no service {args.service} on {args.host}", file=sys.stderr)
|
||||
return 2
|
||||
for svc in matches:
|
||||
for directory in inv.unit_dirs(svc, args.host):
|
||||
for unit in svc.get("units", []):
|
||||
print(f"{unit} {directory}")
|
||||
elif args.command == "stack":
|
||||
matches = inv.named(args.host, args.service)
|
||||
if not matches:
|
||||
print(f"no service {args.service} on {args.host}", file=sys.stderr)
|
||||
return 2
|
||||
owner = matches[0].get("deployed_by") or args.service
|
||||
print(owner)
|
||||
for svc in inv.on(args.host):
|
||||
if svc.get("deployed_by") == owner and svc["name"] != owner:
|
||||
print(svc["name"])
|
||||
elif args.command == "get":
|
||||
matches = inv.on(args.host) if args.host else inv.services
|
||||
matches = [s for s in matches if s["name"] == args.service]
|
||||
if not matches:
|
||||
print(f"no service {args.service}" + (f" on {args.host}" if args.host else ""),
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
values = [s.get(args.field) for s in matches if args.field in s]
|
||||
print(json.dumps(values[0] if len(values) == 1 else values))
|
||||
elif args.command == "render":
|
||||
if args.artifact == "pihole-hosts":
|
||||
sys.stdout.write(render_pihole_hosts(inv))
|
||||
else:
|
||||
if not args.host:
|
||||
print("render node-exporter-unit requires --host", file=sys.stderr)
|
||||
return 2
|
||||
sys.stdout.write(render_node_exporter_unit(inv, args.host))
|
||||
elif args.command == "check":
|
||||
problems = check(inv, args.names)
|
||||
if problems:
|
||||
for problem in problems:
|
||||
print(f"FAIL: {problem}", file=sys.stderr)
|
||||
return 1
|
||||
print("OK: the service inventory is sound (schema, units, ownership, drift)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -293,12 +293,12 @@ def main():
|
||||
ddt, dsh = meta[f"{pre0}.mlp.experts.0.down_proj.weight"][3:5]
|
||||
gsd, gssh = meta[f"{pre0}.mlp.experts.0.gate_proj.weight_scale_inv"][3:5]
|
||||
dsd, dssh = meta[f"{pre0}.mlp.experts.0.down_proj.weight_scale_inv"][3:5]
|
||||
scale_contract = (gdt == F8 and ddt == F8 and gsd == BF16 and dsd == BF16
|
||||
scale_law = (gdt == F8 and ddt == F8 and gsd == BF16 and dsd == BF16
|
||||
and list(gssh) == [(gsh[0] + 127) // 128, (gsh[1] + 127) // 128]
|
||||
and list(dssh) == [(dsh[0] + 127) // 128, (dsh[1] + 127) // 128])
|
||||
if not scale_contract:
|
||||
if not scale_law:
|
||||
log(f"ERROR: unexpected expert shapes: gate {gdt}{gsh} scale {gsd}{gssh} "
|
||||
f"down {ddt}{dsh} scale {dsd}{dssh} (fp8 block scales cover 128x128 tiles)")
|
||||
f"down {ddt}{dsh} scale {dsd}{dssh} (scale law 128x128)")
|
||||
return 2
|
||||
shapes = {"ffn2_h": (gsh[0] * 2, gsh[1]), "ffn": (dsh[1],),
|
||||
"gs": tuple(gssh), "ds": tuple(dssh)}
|
||||
|
||||
+190
-57
@@ -62,101 +62,234 @@ declare -a TESTS=()
|
||||
add_test() {
|
||||
local test="$1"
|
||||
local existing
|
||||
MAPPED=1
|
||||
for existing in "${TESTS[@]:-}"; do
|
||||
[ "$existing" != "$test" ] || return 0
|
||||
done
|
||||
TESTS+=("$test")
|
||||
}
|
||||
|
||||
# The path->test mapping has two layers:
|
||||
# 1. cross-cutting seams that no single service owns (compose validity,
|
||||
# env consistency, the deployer itself, docs, syntax);
|
||||
# 2. the service inventory (shared/services.yml via service-map.py
|
||||
# "seams"): every service the path selects on ANY host contributes its
|
||||
# declared tests. Service seams are the services' own — the former
|
||||
# shotgun fallbacks (every dankclaw quadlet unit ran the open-webui,
|
||||
# pihole, and searxng seams) are retired; the inventory names each
|
||||
# service's real seams, and adding a service is a data edit.
|
||||
for path in "${CHANGED[@]}"; do
|
||||
MAPPED=0
|
||||
case "$path" in
|
||||
tests/*.bats)
|
||||
add_test "$path"
|
||||
continue
|
||||
;;
|
||||
tests/helpers.bash|tests/run.sh)
|
||||
die "$path is shared test infrastructure; run ./tests/run.sh explicitly"
|
||||
;;
|
||||
tests/e2e/*)
|
||||
add_test tests/e2e_scaffold.bats
|
||||
continue
|
||||
;;
|
||||
.githooks/*)
|
||||
add_test tests/changed_tests.bats
|
||||
continue
|
||||
;;
|
||||
# Archive material and host-level facts: a path under any
|
||||
# .AGENTTRASH/ directory is retired-by-policy (the planner plans
|
||||
# nothing from it), udev rules are applied by manual sudo install,
|
||||
# reports are frozen artifacts, and ignore-lists change nothing at
|
||||
# runtime. They select no focused test; without this case they would
|
||||
# trip the mapping gate below.
|
||||
.AGENTTRASH/*|*/.AGENTTRASH/*|dankclaw/udev/*|report/*|.gitignore|.env.example)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
# Cross-cutting seams.
|
||||
case "$path" in
|
||||
dankserver/.env.example|dankclaw/.env.example)
|
||||
dankserver/compose.yaml|dankclaw/compose.yaml)
|
||||
add_test tests/compose_validity.bats
|
||||
add_test tests/env_consistency.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankserver/Caddyfile|dankclaw/Caddyfile)
|
||||
add_test tests/caddyfile.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
tests/e2e/*)
|
||||
add_test tests/e2e_scaffold.bats
|
||||
;;
|
||||
dankserver/.env.example|dankclaw/.env.example)
|
||||
add_test tests/env_consistency.bats
|
||||
;;
|
||||
shared/homepage/*)
|
||||
add_test tests/homepage_caddy.bats
|
||||
add_test tests/homepage_config.bats
|
||||
add_test tests/homepage_deploy.bats
|
||||
add_test tests/homepage_pages.bats
|
||||
add_test tests/homepage_checker.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
shared/searxng/*)
|
||||
add_test tests/searxng_limiter.bats
|
||||
add_test tests/searxng_quadlet.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# The five observability containers left compose for Quadlet units on
|
||||
# 2026-09-11. Their seam pins boot persistence and the loopback-only
|
||||
# publish posture that keeps them off every host interface. This case
|
||||
# precedes the dankclaw/*/quadlet/*.container fallback below, which
|
||||
# would otherwise swallow it into unrelated seams.
|
||||
dankclaw/observability/*)
|
||||
add_test tests/observability_quadlet.bats
|
||||
add_test tests/caddyfile.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
add_test tests/docs_completeness.bats
|
||||
;;
|
||||
# Caddy left compose.yaml on 2026-08-26 for a quadlet; caddyfile.bats
|
||||
# now asserts against the quadlet itself. Route every change under
|
||||
# dankclaw/caddy/ to both seams (this block must precede the
|
||||
# dankclaw/*.container glob, which would otherwise swallow the unit).
|
||||
dankclaw/caddy/*)
|
||||
add_test tests/caddy_quadlet.bats
|
||||
add_test tests/caddyfile.bats
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankclaw/*.container|dankclaw/*.network)
|
||||
add_test tests/open_webui_quadlet.bats
|
||||
add_test tests/pihole.bats
|
||||
add_test tests/searxng_quadlet.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# Anubis is a quadlet + Caddyfile pair: the chain (public listener ->
|
||||
# anubis:8923 -> internal :8016 listener) spans both, so any change
|
||||
# under the service dir must re-run the caddyfile seam too. This case
|
||||
# precedes the dankclaw/*/quadlet/*.container fallback below, which
|
||||
# would otherwise swallow it into unrelated seams.
|
||||
dankclaw/anubis/*)
|
||||
add_test tests/anubis.bats
|
||||
add_test tests/caddyfile.bats
|
||||
add_test tests/homepage_caddy.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# frank-large is a rootless quadlet again since 2026-09-16 (the
|
||||
# halogen-flash-server restore). Its unit carries the fleet's GPU and
|
||||
# podman posture, so it gets its own seam rather than the generic
|
||||
# fallback below.
|
||||
dankclaw/frank-large/*)
|
||||
add_test tests/frank_large_unit.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
add_test tests/docs_completeness.bats
|
||||
;;
|
||||
# Quadlets live under dankclaw/<svc>/quadlet/ (reorganized out of the
|
||||
# top-level dankclaw/*.container globs above). Map the whole subtree so
|
||||
# changes to the pihole, open-webui, searxng, and gitea quadlets hit
|
||||
# their seam tests. The pihole-specific block must come first: shell
|
||||
# case matches the first pattern, and dankclaw/pihole/quadlet/*.container
|
||||
# also satisfies the dankclaw/*/quadlet/*.container fallback.
|
||||
dankclaw/pihole/*)
|
||||
add_test tests/pihole.bats
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankclaw/*/quadlet/*.container|dankclaw/*/quadlet/*.network)
|
||||
add_test tests/open_webui_quadlet.bats
|
||||
add_test tests/pihole.bats
|
||||
add_test tests/searxng_quadlet.bats
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
scripts/deploy.sh|scripts/deploy-remote.sh|scripts/deploy-plan.py|scripts/deploy-*-quadlet.sh)
|
||||
add_test tests/deployment_flow.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
scripts/service-map.py|shared/services.yml)
|
||||
add_test tests/service_inventory.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
scripts/lib/*.sh)
|
||||
add_test tests/llm_gates.bats
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
serverAppConfigs-docs/*|README.md|AGENTS.md)
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
scripts/mount-entropy.sh)
|
||||
add_test tests/mount_entropy.bats
|
||||
;;
|
||||
# Archive material: a path under any .AGENTTRASH/ directory is
|
||||
# retired-by-policy (deploy-plan.py plans nothing from it), so it
|
||||
# selects no focused test. It still needs its own case: without one
|
||||
# the wrapper refuses retirement commits, whose trash moves show up
|
||||
# as additions, with "no focused-test mapping".
|
||||
.AGENTTRASH/*|*/.AGENTTRASH/*)
|
||||
;;
|
||||
# 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/scripts_syntax.bats
|
||||
;;
|
||||
old/scripts/*)
|
||||
# frank-large's chat template lives in scripts/ (moved there on
|
||||
# 2026-09-26 when its old home was retired): a template edit changes
|
||||
# what the live endpoint renders, so it selects the large unit seam.
|
||||
scripts/qwen-sharp-v22.4.0.jinja)
|
||||
add_test tests/frank_large_unit.bats
|
||||
;;
|
||||
# frank-secondary is llama.cpp on the 3090 as a ROOT unit: a native CUDA
|
||||
# binary, so no generated quadlet is involved. 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-secondary.*|scripts/serve-frank-secondary.sh)
|
||||
add_test tests/frank_secondary_unit.bats
|
||||
add_test tests/llm_units.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
scripts/*.service)
|
||||
add_test tests/llm_units.bats
|
||||
add_test tests/scripts_syntax.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
scripts/*)
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
serverAppConfigs-docs/*)
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankclaw/hermes/*)
|
||||
add_test tests/docs_completeness.bats
|
||||
;;
|
||||
README.md|AGENTS.md)
|
||||
# Project guidance changes can affect the documented deployment
|
||||
# contract; validate the docs vault and scoped deployment seams.
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankclaw/comfyui/*)
|
||||
add_test tests/docs_completeness.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# loki and alloy images are built from this repo's Containerfiles; the
|
||||
# loki one carries the healthcheck command (see its comment), so both
|
||||
# seams read it.
|
||||
dankclaw/loki/*|dankclaw/alloy/*)
|
||||
add_test tests/observability_quadlet.bats
|
||||
add_test tests/caddyfile.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
dankclaw/udev/*)
|
||||
# Host-level udev rules are applied by manual sudo install + udevadm
|
||||
# trigger, not dispatched as a service; nothing here is bats-testable.
|
||||
;;
|
||||
.AGENTTRASH/*)
|
||||
# Trash moves are not testable; skip.
|
||||
;;
|
||||
.gitignore|.env.example)
|
||||
# Ignore/ignore-list changes change nothing at runtime; not testable.
|
||||
;;
|
||||
old/scripts/*|old/scripts/**)
|
||||
add_test tests/scripts_syntax.bats
|
||||
;;
|
||||
# Cutover-era dankserver quadlet units (romm trio, jellyfin, calibre-web,
|
||||
# copyparty, plex, tautulli): the seam checks boot-persistence +
|
||||
# v1-generator contract; deploy-flow seams guard the deployer routing.
|
||||
dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*)
|
||||
add_test tests/dankserver_quadlet.bats
|
||||
add_test tests/deployment_flow.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# FrankBook-Pro units live inside the podman machine VM; the seam
|
||||
# pins boot persistence, the gluetun netns invariant, and the
|
||||
# plan-locked volume layout. Deployer changes route here too.
|
||||
frankbook-pro/*)
|
||||
add_test tests/frankbook_quadlet.bats
|
||||
add_test tests/deployment_flow.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
# Seerr unit on dankclaw: boot persistence, loopback-only port
|
||||
# posture, and the deployer routing that installs it.
|
||||
dankclaw/seerr/*)
|
||||
add_test tests/seerr_quadlet.bats
|
||||
add_test tests/deployment_flow.bats
|
||||
add_test tests/deployment_scoping.bats
|
||||
;;
|
||||
report/*)
|
||||
# Frozen audit reports are point-in-time artifacts; not testable.
|
||||
;;
|
||||
*)
|
||||
die "no focused-test mapping for '$path'; add one before committing"
|
||||
;;
|
||||
esac
|
||||
|
||||
# The inventory's declared seams for every service this path selects.
|
||||
while IFS= read -r seam; do
|
||||
[ -n "$seam" ] && add_test "$seam"
|
||||
done < <(python3 "$REPO_ROOT/scripts/service-map.py" seams -- "$path" 2>/dev/null || true)
|
||||
|
||||
# Mapping gate: a path that selected no test at all — no service seam,
|
||||
# no cross-cutting rule — is an unmapped area of the repo; refuse to
|
||||
# commit it silently (the old catch-all `die`).
|
||||
if [ "$MAPPED" -eq 0 ]; then
|
||||
die "no focused-test mapping for '$path'; map it in the inventory (shared/services.yml) before committing"
|
||||
fi
|
||||
done
|
||||
|
||||
# Mapped-but-untestable paths (udev rules, trash moves) legitimately produce an
|
||||
|
||||
@@ -16,13 +16,13 @@ this file is repo-managed; edits are harness-side only.
|
||||
**Model ids are the StarCraft unit names (official 2026-10-08, on the wire
|
||||
since 2026-10-09):** `frank-archon` (dankserver :7770, radiance),
|
||||
`frank-dragoon` (dankclaw :7770, flash-server), `frank-zealot` (dankclaw
|
||||
:7771, the 3090 compaction hub) — formerly `frank-archon`, `frank-dragoon`,
|
||||
`frank-zealot`. The SERVICES keep the old names (units, scripts, env
|
||||
:7771, the 3090 compaction hub) — formerly `frank-primary`, `frank-large`,
|
||||
`frank-secondary`. The SERVICES keep the old names (units, scripts, env
|
||||
files, runbook pages); pages written in the old era name the service,
|
||||
the harness names the model. The wire now matches: each endpoint's
|
||||
`/v1/models` answers with the Protoss name, so `models.yml`, the OpenWebUI
|
||||
picker/grants, the homepage docs and the raw API all use one id per model
|
||||
(see [[OpenWebUI]] for the grant re-key and the [[frank-archon-radiance-swift15-w4dn8]]
|
||||
(see [[OpenWebUI]] for the grant re-key and the [[frank-primary-radiance-swift15-w4dn8]]
|
||||
note on radiance echoing the requested name).
|
||||
|
||||
## Compaction chain arithmetic (verify before ANY window change)
|
||||
@@ -45,14 +45,14 @@ The arithmetic (the coupled contract every endpoint's maxTokens comment
|
||||
encodes):
|
||||
|
||||
1. **Source trigger** = `thresholdPercent × source contextWindow`
|
||||
(0.70 × 200,704 = 140,492 for frank-archon on the current shape)
|
||||
(0.70 × 200,704 = 140,492 for frank-primary on the current shape)
|
||||
2. **Output budget** = capped at the target model's `maxTokens`
|
||||
(32,768 for frank-zealot; 36,864 frank-archon; 57,344 frank-dragoon — the
|
||||
(32,768 for frank-secondary; 36,864 frank-primary; 57,344 frank-large — the
|
||||
largest, because halogen's `HALOGEN_MAX_TOKENS_CAP` must stay ≥ it)
|
||||
3. **REQUIREMENT**: prompt (≤ trigger) + budget ≤ the target's per-slot
|
||||
window. Worked pairs from the live registry: 140,492 + 36,864 = 177,356 ≤
|
||||
200,704 (frank-archon); 140,490 + 57,344 = 197,834 ≤ 200,700
|
||||
(frank-dragoon); 140,492 + 32,768 = 173,260 ≤ 200,704 (frank-zealot).
|
||||
200,704 (frank-primary); 140,490 + 57,344 = 197,834 ≤ 200,700
|
||||
(frank-large); 140,492 + 32,768 = 173,260 ≤ 200,704 (frank-secondary).
|
||||
|
||||
Procedure (recompute from the LIVE files every time — windows rotate with
|
||||
every swap; the numbers above are the 2026-10-05 state):
|
||||
@@ -112,19 +112,19 @@ Use **wire names**, not OMP camelCase. Two traps:
|
||||
`qwenPreserveThinking` is auto-derived in `buildOpenAICompat` from the
|
||||
*auto-detected* `thinkingFormat` + local baseUrl, **before**
|
||||
`applyCompatOverrides`. Setting `compat.thinkingFormat: qwen` in
|
||||
models.yml does **not** enable it. Custom-named models (`frank-dragoon`)
|
||||
models.yml does **not** enable it. Custom-named models (`frank-large`)
|
||||
match no Qwen id pattern → use `extraBody: {preserve_thinking: true}`.
|
||||
2. **Never put `chat_template_kwargs` in `extraBody`** where a compat policy
|
||||
also sets it — `Object.assign` is shallow and replaces the whole object,
|
||||
silently dropping the `reasoning_effort` kwarg the compat policy set.
|
||||
Top-level `preserve_thinking` is honoured by halogen. (frank-archon's
|
||||
Top-level `preserve_thinking` is honoured by halogen. (frank-primary's
|
||||
FreeToken entry currently carries a `chat_template_kwargs` extraBody with
|
||||
a comment asking to verify-and-prune — the exact trap to check at that
|
||||
endpoint's next swap.)
|
||||
|
||||
**Gotcha: silent greedy decode.** halogen has **no server-side sampler
|
||||
defaults** — `temperature` absent or `0` = greedy. If OMP sends nothing, the
|
||||
endpoint decodes greedy while looking configured. This is why frank-dragoon's
|
||||
endpoint decodes greedy while looking configured. This is why frank-large's
|
||||
entry carries the recipe on the wire and a comment saying "do not drop it".
|
||||
|
||||
**Reasoning-effort folding.** Qwen3.8's template accepts only
|
||||
|
||||
@@ -23,7 +23,7 @@ marker. So `marker != HEAD` means "not dispatched via deploy.sh", NOT
|
||||
|
||||
**Why it matters:** a plain `deploy.sh` run re-selects and restarts a service
|
||||
whose marker lags. On llama.cpp/R9V endpoints that is a ~16–19 min reload of
|
||||
a 100+ GiB payload — and frank-archon (dankserver:7770) is also the
|
||||
a 100+ GiB payload — and frank-primary (dankserver:7770) is also the
|
||||
harness's own model backend, so a blind deploy is a self-inflicted outage for
|
||||
possibly zero behavioral change.
|
||||
|
||||
|
||||
@@ -62,8 +62,8 @@ service on the other host.
|
||||
|---|---|---|
|
||||
| Dankclaw compose services | `podman-compose -f dankclaw/compose.yaml up -d --no-deps NAME` | Recreates only `NAME`; Caddy is handled separately because it fronts both hosts. |
|
||||
| SearXNG, Gluetun, Valkey, OpenWebUI, Pi-hole, Gitea database/server | user Quadlet + `systemctl --user` | Installs the selected unit and restarts only the required dependency-coupled units. Gitea's web proxy is the canonical Caddy compose service. |
|
||||
| `frank-dragoon` | systemd + `systemctl` (root unit) | Relinks and restarts only the large inference service; waits up to 20 minutes for `/health`. |
|
||||
| `frank-zealot` | systemd + `systemctl` (root unit) | Relinks and restarts only the small inference service; waits up to 20 minutes for `/health`. |
|
||||
| `frank-large` | systemd + `systemctl` (root unit) | Relinks and restarts only the large inference service; waits up to 20 minutes for `/health`. |
|
||||
| `frank-secondary` | systemd + `systemctl` (root unit) | Relinks and restarts only the small inference service; waits up to 20 minutes for `/health`. |
|
||||
| `frank-medium` | rootless Quadlet + `systemctl --user` | Installs `dankserver/frank-medium/quadlet/frank-medium.container`; on first deploy retires the llama.cpp root unit and the vLLM replica quadlets into the home trash, then restarts the generated unit. Waits up to 20 minutes for `/health`. |
|
||||
| Dankserver media stack (RomM trio, Jellyfin, Calibre-Web, Copyparty) | user Quadlet + `systemctl --user` | Installs `dankserver/<svc>/quadlet/*.container`, restarts the generated unit. The first cutover per service is idempotent: it retires the compose-era bare container, rescues any anonymous volume into the data root, and merge-moves `/opt/<svc>` to `/home/dank/Containers/<svc>` (mapped-uid roots move without chown). Steady state is restart-only. |
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
type: reference
|
||||
title: Service Inventory
|
||||
description: shared/services.yml is the one inventory of deployable services; scripts/service-map.py is its only reader. The planner, both deploy allow-lists, the changed-test seam mapper, the Pi-hole hosts render, the Prometheus scrape facts, and the homepage probes all derive from it.
|
||||
tags: [deployment, inventory, planner, quadlet, prometheus, pihole, homepage]
|
||||
timestamp: 2026-10-13T00:00:00-07:00
|
||||
---
|
||||
|
||||
# Service Inventory
|
||||
|
||||
Before 2026-10-13 the same facts lived in four hand-maintained lists — the
|
||||
planner's path rules (`deploy-plan.py`), the `--service` allow-list in
|
||||
`deploy.sh`, the near-copy in `deploy-remote.sh`, and the changed-test mapper
|
||||
(`test-changed.sh`) — plus per-fact copies in `prometheus.yml`,
|
||||
`addn-hosts`, and `services.json`. They drifted on every swap (the planner
|
||||
still mapped the retired `qwen-sharp` template to frank-dragoon; the allow-list
|
||||
still listed the *arr stack under Dankserver after it moved to FrankBook-Pro).
|
||||
Now there is **one inventory and one reader**:
|
||||
|
||||
- `shared/services.yml` — the data: every deployable service, its hosts, unit
|
||||
dir(s), unit files, path claims, tests, scrape/DNS/probe facts, aliases.
|
||||
- `scripts/service-map.py` — the only reader. CLI subcommands:
|
||||
`hosts`, `services`, `allows`, `plan`, `seams`, `owner`, `units`, `stack`,
|
||||
`get`, `render`, `check`.
|
||||
|
||||
## 1. Consumers
|
||||
|
||||
|Consumer|Call|
|
||||
|---|---|
|
||||
|`scripts/deploy-plan.py`|thin wrapper → `plan --host TARGET [paths...]` (keeps the `--repo/--base/--head` and `--paths` CLIs)|
|
||||
|`scripts/deploy.sh` (`--service` gate)|`allows --host T --service S`|
|
||||
|`scripts/deploy-remote.sh` (`service_is_allowed`)|same|
|
||||
|`scripts/test-changed.sh`|`seams [paths...]` — a service's declared `tests` are its seams|
|
||||
|`dankclaw/pihole/addn-hosts`|`render pihole-hosts` — the tracked file is a rendered artifact|
|
||||
|`*/observability/quadlet/node-exporter.container`|`render node-exporter-unit --host H` — the MIRROR pair became one template; per-host title, LAN address (hosts map), and port (scrape job) fill it|
|
||||
|Prometheus scrape jobs, LAN DNS, homepage probes|`check` compares the tracked configs against the inventory's `scrape`/`dns`/`probe` fields (drift gate)|
|
||||
|
||||
Selection rules the engine encodes (see `tests/fixtures/service-plan-golden.tsv`,
|
||||
the pinned characterization of every tracked path × every target):
|
||||
|
||||
- **Host scoping** — a host's top-level directory can only select that host's
|
||||
services. `frankbook-pro/**` plans nothing on Dankserver (the old planner
|
||||
had a stale Dankserver *arr list).
|
||||
- **Quadlet coupling graph** — `Network=x.container` rides x's netns;
|
||||
`Network=y.network` joins a shared network. Changing the parent (or the
|
||||
network file) selects every rider transitively, in their own directories
|
||||
(`dankclaw/searxng/quadlet/gluetun.container` → gluetun + searxng + marreta;
|
||||
`frankbook-pro/gluetun/quadlet/gluetun.container` → the whole arr stack).
|
||||
- **Stacks** — a service's `stack` members deploy and restart as one handler
|
||||
(romm trio, hermes trio).
|
||||
- **Retirement** — removing a service from `services.yml` removes it from the
|
||||
planner, both allow-lists, and the seam mapper in one data edit; a pending
|
||||
deploy-state entry for it is auto-triaged (see [[Deploy State Triage]]).
|
||||
|
||||
## 2. Integrity gates (`check`)
|
||||
|
||||
`python3 scripts/service-map.py check` runs, and
|
||||
`tests/service_inventory.bats` pins:
|
||||
|
||||
- **schema** — required fields per entry, known keys only;
|
||||
- **units** — every unit file named by an entry exists under its `unit_dir`
|
||||
on the right host; each unit name is claimed by exactly one service per host;
|
||||
- **ownership** — every tracked quadlet `.container`/`.network` file is claimed
|
||||
by exactly one service (trash excluded);
|
||||
- **paths** — every claimed `files`/`dirs`/`retires` glob matches ≥1 tracked file;
|
||||
- **compose** — the container names in each host's `compose.yaml` have exactly
|
||||
one quadlet owner (the [[Quadlet Lifecycle]] two-owners trap);
|
||||
- **drift** — `addn-hosts` equals its render, each tracked node-exporter unit
|
||||
equals its per-host render; the Prometheus job set, the DNS names, and the
|
||||
homepage probe ports equal the inventory's fields.
|
||||
|
||||
## 3. Adding, moving, retiring a service
|
||||
|
||||
- **Add**: append an entry (name, hosts, kind, unit_dir, units, paths, tests,
|
||||
…), add the quadlet file, run `check` — `plan`/`allows`/`seams` pick it up
|
||||
with zero script edits.
|
||||
- **Move between hosts**: edit `hosts` (and per-host `unit_dir`/`probe` maps).
|
||||
- **Retire**: delete the entry, move files to `.AGENTTRASH/` (see
|
||||
[[Testing Policy]] for the seam the mapper applies to trash paths).
|
||||
|
||||
Field notes: `alias` carries deployable names that differ from the service
|
||||
name (`calibre-web` deploys the unit `calibre-server`); `probe` for multi-host
|
||||
services is a `{host: port}` map; `dependents` names services whose config
|
||||
embeds a reference to this one (config-only coupling — a `.env` edit selects
|
||||
both, a unit edit selects the graph closure).
|
||||
|
||||
## 4. Known cutover deltas (2026-10-13)
|
||||
|
||||
The 17 deltas against the old planner are all in these classes: anubis paths
|
||||
now select `anubis` (the old planner had no rule at all); cross-host paths
|
||||
select nothing; `scripts/qwen-sharp-v22.4.0.jinja` (retired template) plans
|
||||
nothing while `scripts/qwen38-froggeric-v22.5.jinja` (the live frank-archon
|
||||
template, previously unmapped) plans frank-archon;
|
||||
`dankserver/romm/romm.container` (top-level quadlet dir the old unit-file rule
|
||||
missed) now plans the romm trio.
|
||||
|
||||
---
|
||||
|
||||
Related: [[Deployment Flow]], [[Testing Policy]], [[Quadlet Lifecycle]],
|
||||
[[Container Network Namespaces]], [[Deploy State Triage]]
|
||||
@@ -15,9 +15,10 @@ This repository tests its configuration at defined seams. The tests run with `ba
|
||||
|
||||
| Seam | File | What it asserts |
|
||||
|------|------|-----------------|
|
||||
| Compose validity | `tests/compose_validity.bats` | YAML parses, services have images, no named volumes |
|
||||
| Env consistency | `tests/env_consistency.bats` | every `${VAR}` is defined in `.env.example`; no real secrets; the live gitea password never appears |
|
||||
| Caddyfile validity | `tests/caddyfile.bats` | per-host Caddyfile, `.local` convention, balanced braces, basic auth present |
|
||||
| Docs completeness | `tests/docs_completeness.bats` | every service has a docs page; index links every page; no unresolved vault links |
|
||||
| Docs completeness | `tests/docs_completeness.bats` | every compose service has a docs page; index links every page |
|
||||
| Script syntax | `tests/scripts_syntax.bats` | `bash -n` clean, executable bits, shellcheck when available |
|
||||
| Deployment flow | `tests/deployment_flow.bats`, `tests/deployment_scoping.bats` | scoped dispatcher exists, targets are known, service plans isolate inference, Caddy ownership is guarded, and SSH reachability is checked (skips offline) |
|
||||
| Quadlet units | `tests/*quadlet*.bats` | every unit is boot-persistent, self-healing, repo-provenanced, secret-free |
|
||||
|
||||
@@ -95,7 +95,7 @@ host-side `workflows/` dir and the repo copy.
|
||||
## Recommended additions
|
||||
|
||||
Quantized preferred, sized for the 8060S iGPU (GTT-backed; the RTX 3090 is
|
||||
frank-zealot's). All repos verified 2026-10-05. None are on disk yet.
|
||||
frank-secondary's). All repos verified 2026-10-05. None are on disk yet.
|
||||
|
||||
| # | What | Repo | Grab | Size | Why |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
@@ -13,7 +13,7 @@ timestamp: 2026-08-26
|
||||
# Dankclaw Stack
|
||||
|
||||
Dankclaw (192.168.0.143) runs the AI, search, development, and observability
|
||||
stack. Its LLM inference endpoint `frank-dragoon` (`:7770`) is a ROCmFPX
|
||||
stack. Its LLM inference endpoint `frank-large` (`:7770`) is a ROCmFPX
|
||||
llama.cpp fork serving the kingjones Qwen3.8-Flash-Next-ROCmFP4-STRIX GGUF on
|
||||
the card's pinned runtime; an NPU endpoint (`frank-npu`, `:7771`,
|
||||
FLM v1.0.1) serves sight+sound models. The completion endpoint
|
||||
@@ -30,7 +30,7 @@ graph LR
|
||||
S --> V[Gluetun VPN]
|
||||
O --> L[llama.cpp dankserver :7770]
|
||||
O --> H[Hermes]
|
||||
O --> E[frank-dragoon :7770]
|
||||
O --> E[frank-large :7770]
|
||||
```
|
||||
|
||||
## Services
|
||||
@@ -48,14 +48,14 @@ graph LR
|
||||
| Seerr | `seerr.franksdank.cloud` (guarded) | Caddy routes post-auth to `seerr:5055` | [[Seerr]] |
|
||||
| Homepage | `franksdank.cloud` + `www.` (root site; guarded for admin actions) | served by Caddy itself from `/srv/homepage` | — |
|
||||
| Pi-hole | `pihole.franksdank.cloud` (basicauth edge, not the guard chain) | admin UI `8083`; DNS on `:53` LAN-wide | [[Pi-hole]] |
|
||||
| frank-dragoon (host service) | — | `7770` | [[deployment]] |
|
||||
| frank-large (host service) | — | `7770` | [[deployment]] |
|
||||
| frank-npu (host service) | — | `7771` (`/v1/version` health) | [[frank-npu]] |
|
||||
| frank-medium (host service, Dankserver) | — | `7770` | [[frank-medium-rocmfpx]] |
|
||||
|
||||
The LLM rows are bare-metal systemd services, not containers. This
|
||||
repo owns their scripts, units, and knowledge base (see
|
||||
[LLM fleet knowledge base](../LLM/index.md)). Current mapping (2026-09-03):
|
||||
`frank-dragoon` runs the kingjones Qwen3.8-Flash-Next-ROCmFP4-STRIX GGUF
|
||||
`frank-large` runs the kingjones Qwen3.8-Flash-Next-ROCmFP4-STRIX GGUF
|
||||
on the card's pinned `kingjones30/ROCmFPX` llama.cpp build;
|
||||
`frank-npu` runs FLM v1.0.1 on the XDNA2 NPU;
|
||||
`frank-medium` runs charlie12345/ROCmFPX llama.cpp serving
|
||||
|
||||
@@ -47,7 +47,7 @@ is not durable. The tracked, sanitized configuration baseline is
|
||||
`0.0.0.0:8080`, so the Signal HTTP API is reachable from the whole LAN.
|
||||
- The data volume persists skills, memory, and session history.
|
||||
- Hermes has two named local providers: `dankclaw-large` reaches
|
||||
`http://192.168.0.143:7770/v1` and exposes `frank-dragoon`;
|
||||
`http://192.168.0.143:7770/v1` and exposes `frank-large`;
|
||||
`dankserver-small` reaches `http://192.168.0.235:7770/v1` and exposes
|
||||
`frank-medium` (renamed from `frank-small` on 2026-09-15).
|
||||
- The live `config.yaml` contains secrets and user state and is not committed;
|
||||
@@ -64,7 +64,7 @@ is not durable. The tracked, sanitized configuration baseline is
|
||||
## Gotchas
|
||||
|
||||
- Hermes is under active development; pin the image tag for reproducibility.
|
||||
- Model choice drives quality; the local `frank-dragoon` (Dankclaw, 7770) is the intended backend.
|
||||
- Model choice drives quality; the local `frank-large` (Dankclaw, 7770) is the intended backend.
|
||||
- Do not expose the gateway without auth — put it behind [[Caddy]] with basic auth.
|
||||
|
||||
## Browser automation (Chrome DevTools Protocol daemon)
|
||||
@@ -189,7 +189,7 @@ What actually breaks, and the fix:
|
||||
auxiliary:
|
||||
title_generation:
|
||||
provider: dankserver-small # names track the live config; diff first
|
||||
model: frank-zealot
|
||||
model: frank-secondary
|
||||
api_mode: codex_responses # real per-task key
|
||||
extra_body:
|
||||
reasoning:
|
||||
|
||||
@@ -24,7 +24,7 @@ https://github.com/open-webui/open-webui
|
||||
| Upstream latest | 0.11.4 (2026-09-21) — security/access-control advisory; `:main` is the rolling image, not a stable pin. Imagegen endpoint is `POST /api/v1/images/generations` (the `comfyui` engine is still upstream). |
|
||||
| Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. |
|
||||
| Data volume | `/home/dank/Containers/open-webui/data` → `/app/backend/data` |
|
||||
| Backends | Picker exposes exactly one model — `frank-zealot` (host.containers.internal:7771). The `frank-dragoon` and `frank-archon` connections remain configured in the DB, disabled (2026-10-07) — see [The LLM backends](#the-llm-backends) |
|
||||
| Backends | Picker exposes two models — `frank-zealot` (host.containers.internal:7771) and `frank-dragoon` (host.containers.internal:7770, added 2026-10-09). The `frank-archon` (Dankserver) connection remains configured in the DB, disabled — see [The LLM backends](#the-llm-backends) |
|
||||
| Network | Pinned bridge `systemd-open-webui` (`10.89.3.0/24`, gateway `10.89.3.1`) — see [History](#history) |
|
||||
| Persistence | Quadlet unit `open-webui.service`; `[Install] WantedBy=default.target` |
|
||||
| Deploy | `scripts/deploy-open-webui-quadlet.sh dankclaw` |
|
||||
@@ -34,17 +34,20 @@ https://github.com/open-webui/open-webui
|
||||
The picker is built from the `model` table in `webui.db` (one row per model)
|
||||
plus the `model_ids` of each **enabled** OpenAI connection, NOT from live
|
||||
connection discovery — so the picker shows exactly what is seeded there.
|
||||
As of 2026-10-07 the picker exposes **exactly one model: `frank-zealot`**.
|
||||
As of 2026-10-09 the picker exposes **two models: `frank-zealot` and
|
||||
`frank-dragoon`**.
|
||||
|
||||
| Connection (`openai.api_configs` idx) | URL | Enabled | `model_ids` | Runtime | Host |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | `http://host.containers.internal:7770/v1` | no (2026-10-07) | `frank-dragoon` | halogen flash-server (Strix Halo iGPU) | Dankclaw |
|
||||
| 0 | `http://host.containers.internal:7770/v1` | yes (2026-10-09) | `frank-dragoon` | halogen flash-server (Strix Halo iGPU) | Dankclaw |
|
||||
| 1 | `http://192.168.0.235:7770/v1` | no (2026-10-07) | `frank-archon` | radiance TP=2 (2× R9700) | Dankserver |
|
||||
| 2 | `http://host.containers.internal:7771/v1` | yes | `frank-zealot` | llama.cpp CUDA (RTX 3090, 2×200,704) | Dankclaw |
|
||||
|
||||
The `model` table carries exactly one decoration row, `frank-zealot`
|
||||
(context length 200,704 = the per-slot window; description: Qwen3.5-9B
|
||||
Q6_K + BF16 vision mmproj).
|
||||
The `model` table carries one decoration row per picker model:
|
||||
`frank-zealot` (context length 200,704 = the per-slot window; description:
|
||||
Qwen3.5-9B Q6_K + BF16 vision mmproj) and `frank-dragoon` (context length
|
||||
262,144 = the per-slot `HALOGEN_CTX`; description: Swift1.5 v2 abliterated,
|
||||
MTP, vision ON).
|
||||
|
||||
A third gate sits *after* the table, and it is the one that bites family
|
||||
accounts (2026-10-07): a model row is only visible to a non-admin user when
|
||||
@@ -62,7 +65,7 @@ Two gotchas make the wiring non-obvious:
|
||||
- **llama.cpp `/v1/models` is Ollama-shaped.** llama.cpp returns
|
||||
`{"models":[{"name":...}]}`; OpenWebUI's `extract_data()` only reads the
|
||||
OpenAI `{"data":[{"id":...}]}` shape, so connection discovery silently misses
|
||||
the llama.cpp endpoints (frank-dragoon, frank-zealot). The fix is to set
|
||||
the llama.cpp endpoints (frank-large, frank-secondary). The fix is to set
|
||||
`model_ids` in each connection's `api_config` — when non-empty, OpenWebUI
|
||||
builds the model list directly and never fetches `/v1/models`. frank-medium
|
||||
(vLLM) serves the OpenAI shape and would discover fine, but uses `model_ids`
|
||||
@@ -112,11 +115,14 @@ The Ollama connection is disabled (`ollama.enable=false`).
|
||||
|
||||
## Verification battery
|
||||
|
||||
1. `GET /api/v1/models/list?limit=100` → returns EXACTLY the intended
|
||||
models. Response key is `items`, not `data`.
|
||||
1. `GET /api/models` (the picker endpoint; response key `data`) → lists
|
||||
EXACTLY the intended models, **as an admin AND as a `role='user'` token**.
|
||||
Do NOT gate on `GET /api/v1/models/list` (the Admin Panel → Models page):
|
||||
on the 0.6.x image it returns `items: []` for connection-backed models
|
||||
(workspace-list only; measured 2026-10-09 on both picker ids).
|
||||
2. `POST /api/chat/completions` per model with a trivial prompt ("Reply
|
||||
with exactly one word: pong") → each returns content `pong`. Reasoning
|
||||
models need a generous budget: frank-zealot (Qwen3.5 template) returns
|
||||
models need a generous budget: frank-secondary (Qwen3.5 template) returns
|
||||
empty content + `finish_reason: "length"` at `max_tokens 200` (the
|
||||
thinking block eats it) and answers `pong` at 1500.
|
||||
3. A 500 "Server Connection Error" → `podman logs open-webui` for
|
||||
@@ -191,9 +197,9 @@ browser-driven and only touches the two exempt vhosts (`ai.` and `auth.`).
|
||||
run with host `python3` over ssh against
|
||||
`/home/dank/Containers/open-webui/data/webui.db` (no `sqlite3` CLI on
|
||||
Dankclaw; table is `user`, singular; no `active` column). No per-user model
|
||||
grant is needed: the picker model `frank-zealot` carries the global
|
||||
`access_grant` row `(model, frank-zealot, user, *, read)`, so any
|
||||
`role='user'` account sees it. Flip on FIRST-LOGIN SIGHTING — the row does
|
||||
grant is needed: both picker models (`frank-zealot`, `frank-dragoon`) carry
|
||||
the global `access_grant` row `(model, <id>, user, *, read)`, so any
|
||||
`role='user'` account sees them. Flip on FIRST-LOGIN SIGHTING — the row does
|
||||
not exist until the account signs in (2026-10-09: five provisioned accounts
|
||||
had no row).
|
||||
- **IdP reachability:** the container is on its own bridge (10.89.3.0/24) and
|
||||
@@ -271,18 +277,18 @@ on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.
|
||||
|
||||
- **2026-08-22 — slirp4netns → pinned bridge.** The unit ran on `Network=slirp4netns`. The slirp4netns helper process died (no OOM, no kill entry, no crash log found), and the container kept running with only `lo` in its netns: published port 3005 accepted TCP and reset every request, so chat served empty responses for ~2 days. Nothing logged an error. Moving to a kernel bridge removes the per-container helper entirely, and the healthcheck's interface check makes the zombie state visible if it ever returns.
|
||||
|
||||
- **2026-09-21 — model picker rebuilt for the 3-model fleet.** The picker carried 39 stale rows (37 dead Ollama models + 2 `frank-default-*`) and was missing `frank-medium`. Rebuilt the `model` table to exactly `frank-dragoon`/`frank-medium`/`frank-zealot` and set `model_ids` on each connection (llama.cpp's Ollama-shaped `/v1/models` is invisible to OpenWebUI's discovery). Pointed the two on-host endpoints at `host.containers.internal` — the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via `/openai/chat/completions`.
|
||||
- **2026-09-21 — model picker rebuilt for the 3-model fleet.** The picker carried 39 stale rows (37 dead Ollama models + 2 `frank-default-*`) and was missing `frank-medium`. Rebuilt the `model` table to exactly `frank-large`/`frank-medium`/`frank-secondary` and set `model_ids` on each connection (llama.cpp's Ollama-shaped `/v1/models` is invisible to OpenWebUI's discovery). Pointed the two on-host endpoints at `host.containers.internal` — the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via `/openai/chat/completions`.
|
||||
|
||||
- **2026-09-24 — SSO via authentik.** OIDC authorization-code flow wired per the [SSO (authentik)](#sso-authentik) section: application/provider/entitlements created in authentik, client secret in the host env file, roles from the `roles` scope claim. E2E coverage in `tests/e2e/specs/18-openwebui-sso.spec.ts`.
|
||||
|
||||
- **2026-09-24 — flipped to SSO-only.** The OIDC round-trip verified in a real browser (login view renders no password form, only "Continue with authentik", and the flow lands on a signed-in session), so the local username/password path was retired: `ENABLE_LOGIN_FORM=false` + `ENABLE_PASSWORD_AUTH=false` in the quadlet and `ui.enable_login_form` set to `false` in the config DB (the DB wins over env, so both were changed together). `qa-e2e-admin` was demoted out of the `authentik Admins` group (it keeps `openwebui_users` / `homepage-admins` for app access).
|
||||
|
||||
- **2026-10-07 — picker narrowed to `frank-zealot` only.** Per Frank: the only model accessible in OpenWebUI is `frank-zealot`. Rebuilt the `model` table to a single `frank-zealot` row (the old rows `frank-dragoon`/`frank-archon`/`frank-small` are deleted), set `openai.api_configs` indices 0 (frank-dragoon) and 1 (frank-archon) to `enable: false`, and retargeted index 2's `model_ids` from the stale `frank-small` label to `frank-zealot` — llama.cpp serves by alias, so the old label addressed a model name the server does not know. Pre-change snapshot: `/home/dank/Containers/open-webui/data/webui.db.bak-20261007` (+ `-wal`/`-shm`). Verified: `/api/v1/models/list` returns exactly `["frank-zealot"]`, chat completion answers `pong` (needs `max_tokens≥1500` — the Qwen3.5 thinking block consumes a 200-token budget), and anonymous API probes are all 401 (see [API access](#api-access-auth-gated)).
|
||||
- **2026-10-07 — picker narrowed to `frank-secondary` only.** Per Frank: the only model accessible in OpenWebUI is `frank-secondary`. Rebuilt the `model` table to a single `frank-secondary` row (the old rows `frank-large`/`frank-primary`/`frank-small` are deleted), set `openai.api_configs` indices 0 (frank-large) and 1 (frank-primary) to `enable: false`, and retargeted index 2's `model_ids` from the stale `frank-small` label to `frank-secondary` — llama.cpp serves by alias, so the old label addressed a model name the server does not know. Pre-change snapshot: `/home/dank/Containers/open-webui/data/webui.db.bak-20261007` (+ `-wal`/`-shm`). Verified: `/api/v1/models/list` returns exactly `["frank-secondary"]`, chat completion answers `pong` (needs `max_tokens≥1500` — the Qwen3.5 thinking block consumes a 200-token budget), and anonymous API probes are all 401 (see [API access](#api-access-auth-gated)).
|
||||
|
||||
- **2026-10-09 — re-keyed to the Protoss ids (wire rename).** The endpoints
|
||||
began serving `frank-zealot` / `frank-dragoon` / `frank-archon`, so three
|
||||
id-keyed objects had to move in the same window as the alias flip:
|
||||
`model.id`+`name` (`frank-zealot` → `frank-zealot`, `params`
|
||||
`model.id`+`name` (`frank-secondary` → `frank-zealot`, `params`
|
||||
`{"context":{"length":200704}}` and the `meta.description` kept),
|
||||
`access_grant` (`resource_type='model'`, `resource_id`, principal
|
||||
`user`/`*`, permission `read`), and `openai.api_configs` per-backend
|
||||
@@ -308,6 +314,21 @@ on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.
|
||||
name: /selected model/i })`) and the welcome text "How can I help you
|
||||
today?"; that snapshot is also the cheapest proof the picker is populated.
|
||||
|
||||
- **2026-10-09 — `frank-dragoon` added to the picker.** Per Frank: expose the
|
||||
halogen endpoint (service `frank-large`, `host.containers.internal:7770`)
|
||||
beside `frank-zealot`. Three id-keyed writes (the trio the 10-07 rebuild
|
||||
proved): `openai.api_configs` idx 0 `enable: true` (its `model_ids` already
|
||||
carried `frank-dragoon` from the wire rename), a `model` row (`id`/`name`
|
||||
`frank-dragoon`, `params.context.length` 262144 = the per-slot
|
||||
`HALOGEN_CTX`, description of the v2 abliterated payload), and the global
|
||||
`access_grant` `(model, frank-dragoon, user, *, read)`. Snapshot
|
||||
`webui.db.bak-20261009b`, apply with host `python3` over ssh, then
|
||||
`systemctl --user restart open-webui.service`. Verified: `/api/models`
|
||||
lists `frank-dragoon` as admin AND as the QA `role='user'` account, and
|
||||
`POST /api/chat/completions` through the app answers `pong` at
|
||||
`max_tokens 1500`. Halogen's `/v1/models` is OpenAI-shaped, so discovery
|
||||
would have worked too — `model_ids` stays authoritative.
|
||||
|
||||
## See Also
|
||||
|
||||
- [[Dankclaw Stack]]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user