Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6d8610b3 | ||
|
|
933420fd29 | ||
|
|
9ffa99902d | ||
|
|
b8f564f3bd | ||
|
|
d42d053abe | ||
|
|
7eed636bd1 | ||
|
|
d9b82b372e | ||
|
|
3d10a4ca4f | ||
|
|
e5b731216c | ||
|
|
ce8d82d4a7 | ||
|
|
02d04bc1f6 | ||
|
|
428ac22736 | ||
|
|
5d5cac4da3 | ||
|
|
3eaba57925 | ||
|
|
9b9c32005f | ||
|
|
ea2fc6440b | ||
|
|
6248f02ca2 | ||
|
|
25df9d05a2 | ||
|
|
7601edfe28 | ||
|
|
e133d2412d | ||
|
|
9971cb559e | ||
|
|
065d796d73 | ||
|
|
1e6b39c382 | ||
|
|
1e52e45fae | ||
|
|
88342c144b | ||
|
|
4d41366f4c | ||
|
|
d09d2745cf | ||
|
|
a3bce7d123 | ||
|
|
327e78b950 | ||
|
|
8775e64ed0 | ||
|
|
64765c6a47 | ||
|
|
d5934a6b47 | ||
|
|
40ad1f4a60 | ||
|
|
9de58ed6fd | ||
|
|
e3a02e25c7 |
No files matched your search
@@ -27,7 +27,7 @@ merge absorbed the former `vllmScripts` repo, history preserved).
|
|||||||
|--------|---------|---------|---------|------|
|
|--------|---------|---------|---------|------|
|
||||||
| Dankserver | `frank-archon` | radiance, digest-pinned, root unit, TP=2 | publisher `qwen3.8-next-flash-fp8-iq4r-moe.rad` | 7770 |
|
| Dankserver | `frank-archon` | radiance, digest-pinned, root unit, TP=2 | publisher `qwen3.8-next-flash-fp8-iq4r-moe.rad` | 7770 |
|
||||||
| Dankclaw | `frank-dragoon` | halogen-flash-server 0.16.2, digest-pinned, root unit | `qwen38-flash-next-v2-abliterated.hgn` + n-gram + vision tower | 7770 |
|
| Dankclaw | `frank-dragoon` | halogen-flash-server 0.16.2, digest-pinned, root unit | `qwen38-flash-next-v2-abliterated.hgn` + n-gram + vision tower | 7770 |
|
||||||
| Dankclaw | `frank-zealot` | llama.cpp PrismML fork build 10777, root unit — the only loader for this payload | Ternary-Bonsai-2-9B PQ2_0 + Q8_0 mmproj | 7771 |
|
| Dankclaw | `frank-zealot` | llama.cpp PrismML fork build 10777, root unit — the only loader for this payload | Ternary-Bonsai-2-9B PQ2_0 + Q8_0 mmproj + rank-1 refusal adapter | 7771 |
|
||||||
|
|
||||||
Payload identity, shape, eras, laws and rollback live in ONE page each:
|
Payload identity, shape, eras, laws and rollback live in ONE page each:
|
||||||
[[frank-archon]], [[frank-dragoon]], [[frank-zealot]]. `scripts/<service>.env`
|
[[frank-archon]], [[frank-dragoon]], [[frank-zealot]]. `scripts/<service>.env`
|
||||||
@@ -47,7 +47,7 @@ per attempt and no separate swap-history file.
|
|||||||
The managed skill library is distilled there one page per skill-cluster:
|
The managed skill library is distilled there one page per skill-cluster:
|
||||||
`LLM/index.md` → *Cross-Cutting References*, plus `Operations/index.md`,
|
`LLM/index.md` → *Cross-Cutting References*, plus `Operations/index.md`,
|
||||||
`Agent/index.md`, and the Infrastructure/Dankclaw/FrankBook-Pro service
|
`Agent/index.md`, and the Infrastructure/Dankclaw/FrankBook-Pro service
|
||||||
pages (`serverAppConfigs-docs/log.md` carries the wave plan and results)
|
pages. There is no `log.md`: the change diary was destroyed 2026-10-19
|
||||||
- `tests/` — bats suite + Playwright e2e
|
- `tests/` — bats suite + Playwright e2e
|
||||||
|
|
||||||
## Git Workflow — trunk-based (fleet law, aligned 2026-10-18)
|
## Git Workflow — trunk-based (fleet law, aligned 2026-10-18)
|
||||||
@@ -318,35 +318,36 @@ the next), so never report request #1.
|
|||||||
deletes the unit file from `/etc/systemd/system/`, not just the symlink.
|
deletes the unit file from `/etc/systemd/system/`, not just the symlink.
|
||||||
`mask` symlinks to `/dev/null` and is safe/reversible.
|
`mask` symlinks to `/dev/null` and is safe/reversible.
|
||||||
|
|
||||||
## Documentation Policy (fleet law, 2026-10-18)
|
## Documentation Policy (fleet law, 2026-10-19)
|
||||||
|
|
||||||
**One fact, one owner.** A page owns exactly one thing — an endpoint, a
|
**Documentation exists for point of use.** A page earns its bytes only if it
|
||||||
service, or a technique — and every other file links to it with `[[Page]]`.
|
helps whoever performs the next task — the shape to deploy, the flag that must
|
||||||
Restating a shape, a digest, a port list, or a measured number in a second
|
be passed, the failure signature to recognise, the rollback to run. It is not a
|
||||||
file is a defect: it is the next thing to go stale.
|
lab notebook and not a ledger. **Losing information is an acceptable, usually
|
||||||
|
correct outcome.** Never let an orphan census ("this is the only copy of that
|
||||||
|
fact") decide what to keep: sole custody of a useless fact is still useless.
|
||||||
|
|
||||||
- **One page per endpoint, forever:** `serverAppConfigs-docs/LLM/<endpoint>.md`
|
- **The purpose test — before you write, and again before you keep:** would the
|
||||||
— current shape, an **Eras** table (`window | payload | runtime | shape |
|
reader act differently because of this line? No → delete it.
|
||||||
replaced because | rollback`), the **Laws** the endpoint taught, the wire
|
- **Measurements are disposable.** A rate or throughput figure (t/s, tok/s, ms,
|
||||||
contract, and the files that own it. A payload swap edits the shape block
|
acceptance %, a bench ladder, a measured MiB) for a payload, runtime, or
|
||||||
and appends one Era row. It never creates a page, and the retired era's
|
layout that is not running now and not being compared now is destroyed, not
|
||||||
page goes to `.AGENTTRASH/` once its rollback path and its laws are in the
|
filed — not in an Eras row, not on a technique page, not "preserved for
|
||||||
table.
|
history". AI moves faster than this vault: the number is wrong before anyone
|
||||||
- **`log.md` records incidents and laws** — anything with a causal story
|
reads it, and a wrong number in a runbook gets believed. Keep the **law** the
|
||||||
(why the host went down, why a knob resets at reboot, why the RAM was the
|
measurement taught; drop the measurement.
|
||||||
fault). A model swap is not an incident: it is an Era row and nothing else.
|
- **One fact, one owner**, every other file links with `[[Page]]`. The env file
|
||||||
- **`AGENTS.md` carries operator-critical numbers only** — port, context
|
beside the serve script is the authority for every live number; the endpoint
|
||||||
ceiling, slot shape, unit/env/serve trio — plus the cross-cutting laws.
|
page is the authority for the shape and the wire contract.
|
||||||
|
- **One page per endpoint:** current shape, **Eras** (`window | payload |
|
||||||
|
runtime | replaced because | rollback` — the rollback is a commit, not a
|
||||||
|
number), the **Laws**, the wire contract, the files that own it. A swap edits
|
||||||
|
the shape block and appends one row; the retired era's own page is
|
||||||
|
**destroyed** to `.AGENTTRASH/`, never merged into anything.
|
||||||
|
- **`log.md` is retired** (2026-10-19: 2,380 lines destroyed). Git holds the
|
||||||
|
chronology. A causal law belongs to the page that owns the thing it governs,
|
||||||
|
or to *Key Lessons Learned* below if it is cross-cutting — never to a diary.
|
||||||
- **Index pages are locators:** one row, one clause, no numbers.
|
- **Index pages are locators:** one row, one clause, no numbers.
|
||||||
- **A measured number is written once:** in the env-file header beside the
|
|
||||||
serve script that consumes it, and in the endpoint page. Every other page
|
|
||||||
cites it by link.
|
|
||||||
- Technique pages (spec decode, KV math, offload budgets, PP bandwidth) stay
|
|
||||||
technique pages: they stop carrying a per-endpoint throughput table or a live
|
|
||||||
shape, but a campaign ledger that exists nowhere else is owned by the page that
|
|
||||||
measured it. The defect is two pages carrying one figure, not a number living on a
|
|
||||||
technique page — move a figure only when an endpoint page or an env header is its
|
|
||||||
home; otherwise leave it in place and name the era it measured.
|
|
||||||
|
|
||||||
## Model Config (harness)
|
## Model Config (harness)
|
||||||
|
|
||||||
@@ -439,7 +440,11 @@ disagrees with it is the bug.
|
|||||||
`prism.hadamard.*` dequant metadata — stock llama.cpp refuses it, and a
|
`prism.hadamard.*` dequant metadata — stock llama.cpp refuses it, and a
|
||||||
loader ignoring that metadata dequantizes wrong); payload
|
loader ignoring that metadata dequantizes wrong); payload
|
||||||
`Bonsai2-9B-MTP/…PQ2_0.gguf` + the publisher's **Q8_0** mmproj (the BF16
|
`Bonsai2-9B-MTP/…PQ2_0.gguf` + the publisher's **Q8_0** mmproj (the BF16
|
||||||
tower can crash the server with concurrent image requests).
|
tower can crash the server with concurrent image requests) + the rank-1
|
||||||
|
refusal-direction adapter applied at load by `--lora-scaled …:1`. The
|
||||||
|
adapter's rollback is `LORA_FILE=` (empty) in the env — one line, no rebuild
|
||||||
|
— and build 10777 is also the gate that lets it load: older forks refuse it
|
||||||
|
with `unsupported prism.hadamard.version: 2`.
|
||||||
- Shape: `--ctx-size 602112 --parallel 3` = **three conversations at
|
- Shape: `--ctx-size 602112 --parallel 3` = **three conversations at
|
||||||
200,704 each**, `CACHE_TYPE_K=f16` + `CACHE_TYPE_V=q8_0` (asymmetric: 25,088
|
200,704 each**, `CACHE_TYPE_K=f16` + `CACHE_TYPE_V=q8_0` (asymmetric: 25,088
|
||||||
B/token, 14,406 MiB of pool), batch 6144 / ubatch 1024,
|
B/token, 14,406 MiB of pool), batch 6144 / ubatch 1024,
|
||||||
@@ -457,12 +462,15 @@ disagrees with it is the bug.
|
|||||||
- Owns: `scripts/frank-archon.env`, `scripts/serve-frank-archon.sh`,
|
- Owns: `scripts/frank-archon.env`, `scripts/serve-frank-archon.sh`,
|
||||||
`scripts/frank-archon.service`, `scripts/ttm-preflight.sh`, template
|
`scripts/frank-archon.service`, `scripts/ttm-preflight.sh`, template
|
||||||
`scripts/qwen38-froggeric-v22.5.jinja`; image digest-pinned
|
`scripts/qwen38-froggeric-v22.5.jinja`; image digest-pinned
|
||||||
`stilldeadcode/radiance@sha256:4c3f4845…`; container name = unit name.
|
`stilldeadcode/radiance@sha256:d590e300…` (radiance 1.8.1); container name = unit name.
|
||||||
- Shape: TP=2 over `wht6`, `--max-num-seqs 3`, `--max-model-len 200000`,
|
- Shape: TP=2 over `wht6`, `--max-num-seqs 2`, `--max-model-len 200000`,
|
||||||
`expert_tiered` placement, 44,928 MiB pinned host pool per rank, VRAM pair
|
`expert_tiered` placement, 44,928 MiB pinned host pool per rank, `fp8` KV,
|
||||||
`--vram-weights-mib 25600 --vram-kv-mib 5120` (ONE budget: weights + kv ≤
|
MTP depth 2, MNBT 2048, `--busy-prefill-chunk auto`, prefix cache 8 GiB host
|
||||||
30,969 MiB/card), `fp8` KV, MTP depth 2, MNBT 2048, prefix cache 8 GiB host
|
tier only, n-gram table pinned to RAM. The per-card weights/KV split is derived
|
||||||
+ 64 GiB disk, n-gram table pinned to RAM.
|
by the engine's budget resolver from `--gpu-headroom-mib`, the cache bound and
|
||||||
|
the host pool; the `--vram-weights-mib`/`--vram-kv-mib` pair and the prefix disk
|
||||||
|
tier are gone (removed at 1.3.0 / 1.8.1) and naming either is a boot refusal,
|
||||||
|
not a no-op.
|
||||||
- Harness/gate contract: `models.yml` `contextWindow` MUST quote 200,000
|
- Harness/gate contract: `models.yml` `contextWindow` MUST quote 200,000
|
||||||
(over-length is refused, never truncated); the ~147 GiB pinned-RAM ledger
|
(over-length is refused, never truncated); the ~147 GiB pinned-RAM ledger
|
||||||
is why this is a root unit, and `ttm-preflight.sh` raises
|
is why this is a root unit, and `ttm-preflight.sh` raises
|
||||||
@@ -487,36 +495,10 @@ linked runbooks.
|
|||||||
6 set. AMD desktop platforms put one card on CPU root ports (atomics work) and the rest behind the
|
6 set. AMD desktop platforms put one card on CPU root ports (atomics work) and the rest behind the
|
||||||
chipset (usually not) — that asymmetry is what blocked TP=2 here. A one-card collective proves
|
chipset (usually not) — that asymmetry is what blocked TP=2 here. A one-card collective proves
|
||||||
nothing: NCCL short-circuits at `world_size=1`. [[PCIe Link and Atomics]]
|
nothing: NCCL short-circuits at `world_size=1`. [[PCIe Link and Atomics]]
|
||||||
- **That wall was physical, and a board swap removed it on 2026-09-20.** dankserver took a board
|
|
||||||
whose CPU lanes bifurcate to x8/x8 Gen5: both cards on root ports, every bridge `Routing+`, BOTH
|
|
||||||
endpoints `ReqEn+`. Bandwidth was never the constraint (a 27B TP=2 allreduce at batch 4 is ≈10 MB
|
|
||||||
per token-step, under 0.4 ms at Gen5 x8); the collective choice is — TheRock's RCCL builds lack
|
|
||||||
gfx1201 device code (`invalid device function`), which is why vLLM runs its R4D one-shot allreduce
|
|
||||||
here at all. Evidence matrix and the three-arm verdict: [[Dual-R9700-Cooperation]].
|
|
||||||
- **When a GPU collective dies, read the runtime, not the framework.** The message that identified
|
- **When a GPU collective dies, read the runtime, not the framework.** The message that identified
|
||||||
the cause came from `AMD_LOG_LEVEL=3` (`Pcie atomics not enabled, hostcall not supported` →
|
the cause came from `AMD_LOG_LEVEL=3` (`Pcie atomics not enabled, hostcall not supported` →
|
||||||
`AQL dispatch failed!`), never from vLLM or RCCL — the env var costs one line and prints the
|
`AQL dispatch failed!`), never from vLLM or RCCL — the env var costs one line and prints the
|
||||||
failing HIP call with its return code. [[PCIe Link and Atomics]]
|
failing HIP call with its return code. [[PCIe Link and Atomics]]
|
||||||
- **Layer split is the no-collective path, on one vendor's cards and across two — and
|
|
||||||
`--override-tensor` is what cannot ride with it.** Layers go in order and only the boundary
|
|
||||||
activation crosses PCIe once per forward pass, so it works wherever a collective cannot be
|
|
||||||
dispatched (on an RCCL-less fork it beats tensor mode, which falls back to a butterfly allreduce),
|
|
||||||
and it is the cross-vendor answer too (a CUDA + Vulkan split held a real two-device graph). But
|
|
||||||
`--override-tensor <group>=<device>` sets `has_tensor_overrides()`, a REQUIRED term of the
|
|
||||||
scheduler's `pipeline_parallel` flag, and that flag sets `n_copies`: with `n_copies == 1` a
|
|
||||||
pre-allocated weight on a non-primary device has no backend and the graph aborts (reported with
|
|
||||||
the leaf's op name `(NONE)`). `=CPU` is the only override target that works — the CPU backend is
|
|
||||||
last in the list — and pure Vulkan changes nothing: the gate is the override, not the driver mix.
|
|
||||||
Pin the spec-decode drafter to one device; on a split its home is the card holding the target's
|
|
||||||
last layers. [[Llama Cpp Multi-Device Splits]] [[x4-pair-experiment]]
|
|
||||||
- **A host↔3090 link at x4 measures 6.72 GB/s, and that is a design constraint, not a detail.**
|
|
||||||
Pinned H2D 6.72 / D2H 6.60 GB/s (≈85 % of the Gen4 x4 ceiling), pageable within 1 % of pinned —
|
|
||||||
the copies are LINK-limited, so the usual "pageable costs 2×" rule does not apply; the x4 lives in
|
|
||||||
the adapter or cable (card and port are both wider). For any engine that STREAMS weights across
|
|
||||||
it: a 3 MiB slab costs 474 µs, so a 111 GiB A3B model needing ~1.70 GiB of active experts per
|
|
||||||
token lands at 3.7 tok/s against 20.46 tok/s with the experts COMPUTED in host RAM
|
|
||||||
(`-ot exps=CPU`, where the link carries activations only). Streaming loses; RAM does not care.
|
|
||||||
[[x4-pair-experiment]]
|
|
||||||
|
|
||||||
|
|
||||||
**ROCm / GPU**
|
**ROCm / GPU**
|
||||||
@@ -530,7 +512,7 @@ linked runbooks.
|
|||||||
the destination, then copy the contents.
|
the destination, then copy the contents.
|
||||||
- **mmap must stay on** for the big-PLE models: `--no-mmap` forces the ~50 GiB PLE table into
|
- **mmap must stay on** for the big-PLE models: `--no-mmap` forces the ~50 GiB PLE table into
|
||||||
anonymous memory → silent cgroup kill, with dmesg as the only evidence. mmap pages ARE page cache,
|
anonymous memory → silent cgroup kill, with dmesg as the only evidence. mmap pages ARE page cache,
|
||||||
and pinning the PLE tensor with `-ot` cost 23 → 13.4 t/s. [[Llama Cpp Serving Sizing]]
|
and pinning the PLE tensor with `-ot` was slower than letting it page. [[Llama Cpp Serving Sizing]]
|
||||||
- **HIP reads bare env vars** `TEMP`, `HOST`, `GPU_MAX_HEAP_SIZE`. Serve scripts export env files
|
- **HIP reads bare env vars** `TEMP`, `HOST`, `GPU_MAX_HEAP_SIZE`. Serve scripts export env files
|
||||||
with `set -a`, so a tunable named `TEMP` leaks into HIP (`TEMP=1.0` sent the JIT cache to path
|
with `set -a`, so a tunable named `TEMP` leaks into HIP (`TEMP=1.0` sent the JIT cache to path
|
||||||
`1.0` and segfaulted every load, fixed 2026-08-13 by renaming to `TEMPERATURE`). Never name a
|
`1.0` and segfaulted every load, fixed 2026-08-13 by renaming to `TEMPERATURE`). Never name a
|
||||||
@@ -545,9 +527,6 @@ linked runbooks.
|
|||||||
- **Flash-Next MTP heads do not load on any current loader** — they need full per-layer
|
- **Flash-Next MTP heads do not load on any current loader** — they need full per-layer
|
||||||
`blk.<il>.hc_attn_norm.weight` and ship only `blk.48` (verified 3×). Do not re-attempt without new
|
`blk.<il>.hc_attn_norm.weight` and ship only `blk.48` (verified 3×). Do not re-attempt without new
|
||||||
loader support.
|
loader support.
|
||||||
- **QSA flash-attn assert:** `GGML_ASSERT(a-type == GGML_TYPE_F32)` on qwen4exp needs
|
|
||||||
`scripts/patches/qwen4exp-qsa-f32-mask.patch` — but only on the agentionai FAST layer set; the
|
|
||||||
kingjones STRIX set works unpatched. [[Llama Cpp Serving Sizing]]
|
|
||||||
- **The GPU budget on Dankclaw is the GTT aperture, not the rocm-smi "dedicated" figure.** The
|
- **The GPU budget on Dankclaw is the GTT aperture, not the rocm-smi "dedicated" figure.** The
|
||||||
Radeon 8060S (gfx1151) reports 116,732 MiB (`llama-server --list-devices`; `gtt_total` in
|
Radeon 8060S (gfx1151) reports 116,732 MiB (`llama-server --list-devices`; `gtt_total` in
|
||||||
`/sys/class/drm/card1/device/`), and the older "64 GiB, SVM-backed" lesson was already stale.
|
`/sys/class/drm/card1/device/`), and the older "64 GiB, SVM-backed" lesson was already stale.
|
||||||
@@ -602,13 +581,11 @@ linked runbooks.
|
|||||||
`LimitMEMLOCK=infinity` — systemd is privileged and the value flows through podman to runc without
|
`LimitMEMLOCK=infinity` — systemd is privileged and 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
|
any raise. Upstream halogen's own run lines pass NO `--ulimit` at all: copy vendor run lines
|
||||||
before adding flags. [[frank-dragoon]]
|
before adding flags. [[frank-dragoon]]
|
||||||
- **A GGUF's custom tensor type picks the runtime — read the histogram before promising a file to a
|
- **A GGUF's custom tensor type picks the runtime — read the histogram before promising a
|
||||||
loader.** `Qwen3.8-27B-Q8_0_ROCMFPX_AGENT.gguf` is ftype 115 over 311 tensors of type 103
|
file to a loader.** A non-standard tensor type in the KV section makes stock loaders refuse
|
||||||
(`GGML_TYPE_Q8_0_ROCMFPX`), so stock llama.cpp and gufo refuse it at load and the fork was not a
|
the file at load, so a fork can be the only door rather than a preference. Counting tensor
|
||||||
preference but the only door. Counting tensor types out of the GGUF KV section takes one python
|
types takes one python read and settles a runtime argument in minutes; build identity matters
|
||||||
read and settles a runtime argument in minutes. Build identity matters as much as the fork: the
|
as much as the fork, so pin the commit by SHA in any build script. [[GGUF Compatibility]]
|
||||||
2026-08-25 ROCmFPX builds SIGSEGV in `ggml_compute_forward_set_rows` on this same file, so
|
|
||||||
`scripts/build-rocmfpx.sh` pins commit `36e9acd` by SHA. [[GGUF Compatibility]]
|
|
||||||
- **Speculative-decode flags move as a SET.** Handing the fork `--model-draft` with no `--spec-type`
|
- **Speculative-decode flags move as a SET.** Handing the fork `--model-draft` with no `--spec-type`
|
||||||
(the naive way to build a "no MTP" bench arm) segfaulted at load, core dumped, no diagnostic. A
|
(the naive way to build a "no MTP" bench arm) segfaulted at load, core dumped, no diagnostic. A
|
||||||
baseline arm must strip `--model-draft` AND every `--spec-draft-*` AND `--spec-type`, or it
|
baseline arm must strip `--model-draft` AND every `--spec-draft-*` AND `--spec-type`, or it
|
||||||
@@ -617,12 +594,6 @@ linked runbooks.
|
|||||||
exception kept ~26 GiB of weights resident on a 121 GiB box and pushed `available` to 3 GiB, so
|
exception kept ~26 GiB of weights resident on a 121 GiB box and pushed `available` to 3 GiB, so
|
||||||
every later rate was suspect. Record the child pid, kill it in a `finally`, then prove `free` came
|
every later rate was suspect. Record the child pid, kill it in a `finally`, then prove `free` came
|
||||||
back before believing a number — and kill by pid, never `pkill -f llama-server`.
|
back before believing a number — and kill by pid, never `pkill -f llama-server`.
|
||||||
[[LLM Bench Windows and Measurement]]
|
|
||||||
- Retired-runtime traps that can come back: **Kairic Edge** needs
|
|
||||||
`third_party/composable_kernel/include/ck/config.h` generated manually (untracked, so `git clean`
|
|
||||||
removes it) and its greedy-argmax fastpath rejects non-greedy requests and reasoning budgets, so
|
|
||||||
re-test chat before production; the **FastMTP sidecar** must be commit `4df29be4` + the HauhauCS
|
|
||||||
patch, since stock binaries fail the draft load with a tensor-shape mismatch.
|
|
||||||
|
|
||||||
**llama.cpp serving**
|
**llama.cpp serving**
|
||||||
|
|
||||||
@@ -635,10 +606,10 @@ linked runbooks.
|
|||||||
`--spec-type a,b` chains in a fixed internal order (CLI order irrelevant) and the gate is
|
`--spec-type a,b` chains in a fixed internal order (CLI order irrelevant) and the gate is
|
||||||
`ctx_dft`: the ngram family is added unconditionally while `draft-mtp`, `draft-dflash`,
|
`ctx_dft`: the ngram family is added unconditionally while `draft-mtp`, `draft-dflash`,
|
||||||
`draft-dspark` and `draft-eagle3` all need a draft CONTEXT. So `draft-dflash,ngram-mod` is
|
`draft-dspark` and `draft-eagle3` all need a draft CONTEXT. So `draft-dflash,ngram-mod` is
|
||||||
legal (measured 2.6x the drafter alone on prompt-echo traffic) but `draft-mtp,draft-dflash`
|
legal and it wins on prompt-echo traffic, but `draft-mtp,draft-dflash`
|
||||||
dies at load — *"context type MTP requested but model doesn't contain MTP layers"*. **A LEGAL
|
dies at load — *"context type MTP requested but model doesn't contain MTP layers"*. **A LEGAL
|
||||||
chain can still lose on a recurrent model:** `draft-mtp,ngram-mod` on frank-dragoon's qwen35
|
chain can still lose on a recurrent model:** `draft-mtp,ngram-mod` on frank-dragoon's qwen35
|
||||||
hybrid measured −15 % prose / −7 % block-copy, because adding the ngram layer logs
|
hybrid loses, because adding the ngram layer logs
|
||||||
`recurrent state rollback is not compatible with 'ngram-mod' - disabling rollback support` and
|
`recurrent state rollback is not compatible with 'ngram-mod' - disabling rollback support` and
|
||||||
MTP verification depends on that rollback. Measure chains per architecture, never by analogy.
|
MTP verification depends on that rollback. Measure chains per architecture, never by analogy.
|
||||||
[[frank-dragoon]]
|
[[frank-dragoon]]
|
||||||
@@ -647,7 +618,7 @@ linked runbooks.
|
|||||||
declaring NO sliding window caches the whole window on `--spec-draft-device` and can OOM the
|
declaring NO sliding window caches the whole window on `--spec-draft-device` and can OOM the
|
||||||
first draft graph capture AFTER `/health` has answered. Read `sliding_window` out of the
|
first draft graph capture AFTER `/health` has answered. Read `sliding_window` out of the
|
||||||
drafter's GGUF header before sizing, and prefer a skewed `-ts` over a context cut.
|
drafter's GGUF header before sizing, and prefer a skewed `-ts` over a context cut.
|
||||||
[[Llama Cpp Multi-Device Splits]]
|
[[Llama Cpp Serving Sizing]]
|
||||||
- **A draft context bills its own f16 KV over the WHOLE POOL, not the slot**, plus its own
|
- **A draft context bills its own f16 KV over the WHOLE POOL, not the slot**, plus its own
|
||||||
compute buffers, so on a single-card endpoint that squeeze can cost more than the drafter
|
compute buffers, so on a single-card endpoint that squeeze can cost more than the drafter
|
||||||
wins. The MTP question is never "does the file carry the head" but what draft KV + buffers
|
wins. The MTP question is never "does the file carry the head" but what draft KV + buffers
|
||||||
@@ -664,21 +635,20 @@ linked runbooks.
|
|||||||
re-measure the balance after any batch change. The logical `--batch-size` is nearly free; a
|
re-measure the balance after any batch change. The logical `--batch-size` is nearly free; a
|
||||||
large-batch allocation can fail on BUFFER CONTIGUITY while totals still fit, which no totals
|
large-batch allocation can fail on BUFFER CONTIGUITY while totals still fit, which no totals
|
||||||
model can see. A prompt shorter than the ubatch cannot see the knob at all — that is the
|
model can see. A prompt shorter than the ubatch cannot see the knob at all — that is the
|
||||||
control that proves the knob, not the traffic, moved the number. The 2× R9700 split peaks at
|
control that proves the knob, not the traffic, moved the number. The optimum is a property of
|
||||||
512; the CUDA 3090 has no interior optimum up to 2048. When a bench IS ordered, A/B it by
|
the backend, so re-find it after any device or payload change. When a bench IS ordered, A/B it
|
||||||
restarting the SERVICE through a transient `FRANK_<SVC>_ENV_FILE` drop-in, never a scratch
|
by restarting the SERVICE through a transient `FRANK_<SVC>_ENV_FILE` drop-in, never a scratch
|
||||||
port: one card holds one server. Ladders: [[frank-medium-llamacpp-dflash2]] and
|
port: one card holds one server.
|
||||||
[[frank-zealot]].
|
|
||||||
- **One `-ts` step is one LAYER, and a hybrid stack's layers are not the same size** (a DeltaNet
|
- **One `-ts` step is one LAYER, and a hybrid stack's layers are not the same size** (a DeltaNet
|
||||||
boundary and a full-attention boundary can differ by ~1 GiB each with its own KV), so balance
|
boundary and a full-attention boundary can differ by ~1 GiB each with its own KV), so balance
|
||||||
by measurement. An affine fit reproduces the deployed point and overstates the gaps in between.
|
by measurement. An affine fit reproduces the deployed point and overstates the gaps in between.
|
||||||
[[Llama Cpp Multi-Device Splits]]
|
[[Llama Cpp Serving Sizing]]
|
||||||
- **`ngram-mod` prints no startup banner.** The load log names `draft-dflash` and nothing for the
|
- **`ngram-mod` prints no startup banner.** The load log names `draft-dflash` and nothing for the
|
||||||
matcher, so a chain can look unengaged while it works. Identify it behaviourally (`draft_n` > 0
|
matcher, so a chain can look unengaged while it works. Identify it behaviourally (`draft_n` > 0
|
||||||
only where text repeats) or by the per-task `draft acceptance = ... mean len = ...` line. It
|
only where text repeats) or by the per-task `draft acceptance = ... mean len = ...` line. It
|
||||||
costs no VRAM (a 64 MiB host-RAM hash table) and its window decides which traffic it can see:
|
costs no VRAM (a 64 MiB host-RAM hash table) and its window decides which traffic it can see:
|
||||||
too short and it proposes on ordinary prose fragments, costing verification without acceptance.
|
too short and it proposes on ordinary prose fragments, costing verification without acceptance.
|
||||||
[[Llama Cpp Multi-Device Splits]]
|
[[Llama Cpp Serving Sizing]]
|
||||||
- **`--parallel` divides the pool, so MORE conversations cost WINDOW, and the per-slot number is
|
- **`--parallel` divides the pool, so MORE conversations cost WINDOW, and the per-slot number is
|
||||||
the contract — not `ctx-size`.** `n_ctx_slot = CTX_SIZE / PARALLEL`, and llama.cpp refuses a
|
the contract — not `ctx-size`.** `n_ctx_slot = CTX_SIZE / PARALLEL`, and llama.cpp refuses a
|
||||||
prompt longer than the SLOT with a 400 even when the pool has room, so `models.yml`
|
prompt longer than the SLOT with a 400 even when the pool has room, so `models.yml`
|
||||||
@@ -793,17 +763,24 @@ linked runbooks.
|
|||||||
`curl -o FILE URL` exits 0 on HTTP 401/403 and writes the error body as the "artifact": a gated
|
`curl -o FILE URL` exits 0 on HTTP 401/403 and writes the error body as the "artifact": a gated
|
||||||
repo yielded three 153-byte shards with identical sha256 and a cheerful `DOWNLOAD_DONE` in 20
|
repo yielded three 153-byte shards with identical sha256 and a cheerful `DOWNLOAD_DONE` in 20
|
||||||
seconds for 104 GiB. Use `-f`/`--fail-with-body`, compare each file's byte count to the Hub's
|
seconds for 104 GiB. Use `-f`/`--fail-with-body`, compare each file's byte count to the Hub's
|
||||||
declared size, and check the LFS sha256 (`scripts/download-frank-medium-gguf.sh` does all three;
|
declared size, and check the LFS sha256 — the live `scripts/download-frank-*.sh` scripts do
|
||||||
ad-hoc fetches tend not to). Gated repos 403 even with the host's HF token until that account
|
all three; ad-hoc fetches tend not to. Gated repos 403 even with the host's HF token until
|
||||||
accepts the gate. [[Model Swap and Rollback]]
|
that account accepts the gate. [[Model Swap and Rollback]]
|
||||||
- **A scout verdict is a lead, never a finding.** In the 2026-10-19 vault de-dup campaign six
|
- **A verdict is a lead — the scout's and the tool's alike.** In the 2026-10-19
|
||||||
read-only scouts worked the same pages: one classified era pages as stale *because* they
|
documentation campaign six scouts worked the same pages and produced invented
|
||||||
document retired payloads (that is an era page's content), one reported files that do not exist,
|
bullet IDs, files that do not exist, "duplications" grep disproved, line
|
||||||
one reported duplications that grep disproved (`457.9 t/s` and the ping-RTT table are solo-held
|
numbers past the end of the file they judged, and Eras tables with invented
|
||||||
by `Ethernet Bandwidth Math`), one cited a line number past the end of the file it judged, and
|
payloads. The mechanical tool was worse: `scripts/port-census.py` built its
|
||||||
one drafted an Eras table with invented windows, payloads and rollback paths. Re-verify every
|
corpus from the directory containing the page under test, so each page was held
|
||||||
claimed duplicate, every claimed staleness and every quoted holder against the file and today's
|
by *itself* and ten era pages reported "0 sole-record lines" — which reads as
|
||||||
fleet state before editing; a scout yield buys no evidence.
|
"pure duplication, trash it". Nothing was cut on a verdict; every holder was
|
||||||
|
re-grepped and every tool re-measured before it was believed.
|
||||||
|
- **Never let a census decide what to keep.** That same campaign then concluded
|
||||||
|
the era pages were irreplaceable because each held 14–63 sole-record lines —
|
||||||
|
wrong in the opposite direction: those were throughput figures for payloads that
|
||||||
|
no longer run. Point of use is the only ownership test, the operator applies it,
|
||||||
|
and 53 pages plus the 2,380-line change diary were destroyed as a result. A tool
|
||||||
|
can say where a fact lives; it cannot say whether anything needs it.
|
||||||
|
|
||||||
## Runbook Index
|
## Runbook Index
|
||||||
|
|
||||||
@@ -818,12 +795,13 @@ per-attempt page and no per-swap ledger anywhere else.
|
|||||||
| `frank-zealot` — llama.cpp (PrismML fork), RTX 3090 | `serverAppConfigs-docs/LLM/frank-zealot.md` |
|
| `frank-zealot` — llama.cpp (PrismML fork), RTX 3090 | `serverAppConfigs-docs/LLM/frank-zealot.md` |
|
||||||
| `frank-tts` — cloned-voice speech, Dankclaw | `serverAppConfigs-docs/Dankclaw/Frank TTS.md` |
|
| `frank-tts` — cloned-voice speech, Dankclaw | `serverAppConfigs-docs/Dankclaw/Frank TTS.md` |
|
||||||
| Dankclaw kernel posture (`iommu=pt` since 2026-10-18) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
|
| Dankclaw kernel posture (`iommu=pt` since 2026-10-18) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
|
||||||
| Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` |
|
| Deployment flow | `serverAppConfigs-docs/Architecture/Deployment Flow.md` |
|
||||||
|
|
||||||
Retired **endpoints** (not payloads) keep their own pages — `frank-medium`,
|
There is no page for a retired endpoint and no change diary. A payload swap
|
||||||
`frank-npu`, the pre-name Dankclaw/Dankserver eras — because no live page owns
|
appends one row to the live endpoint's **Eras** table; when the endpoint itself
|
||||||
them; `serverAppConfigs-docs/index.md` is the locator for every page in the
|
dies, its page is destroyed. Rollback is a commit, not a document.
|
||||||
vault. Retired scripts live in `old/` and `.AGENTTRASH/`.
|
`serverAppConfigs-docs/index.md` is the locator for every page that survives;
|
||||||
|
retired scripts live in `old/` and `.AGENTTRASH/`.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,22 @@
|
|||||||
# without `podman unshare` — accepted for a datastore nobody edits by hand;
|
# without `podman unshare` — accepted for a datastore nobody edits by hand;
|
||||||
# episode AUDIO is a host directory for the opposite reason (see the app unit).
|
# episode AUDIO is a host directory for the opposite reason (see the app unit).
|
||||||
#
|
#
|
||||||
# HEALTH. The base image is distroless: no shell, no curl, no wget, so no
|
# HEALTH. The base image is distroless — measured against this digest, not even
|
||||||
# CMD-SHELL probe and no wget/curl probe is possible here (same class of unit
|
# `ls` exists (`exec: "ls": executable file not found in $PATH`), so there is no
|
||||||
# as loki, whose probe had to move into the image). SurrealDB ships its own
|
# `/bin/sh`, no curl and no wget. SurrealDB ships its own probe instead:
|
||||||
# probe instead — `surreal is-ready` connects to ws://localhost:8000 by default
|
# `surreal is-ready` connects to ws://localhost:8000 by default, needs no
|
||||||
# and needs no credentials — verified against this digest: it prints `OK` and
|
# credentials, prints `OK` and exits 0 while the server is up.
|
||||||
# exits 0 while the server is up. Argument-only HealthCmd, like every unit on
|
#
|
||||||
# this host, because the quadlet generator splits it on spaces.
|
# It must be handed over as a QUADLET-SIDE JSON ARRAY, not a bare command.
|
||||||
|
# podman wraps a bare `HealthCmd` string as `CMD-SHELL` (measured on this host's
|
||||||
|
# podman 5.7: `--health-cmd "/surreal is-ready"` stores
|
||||||
|
# `{"Test":["CMD-SHELL","/surreal is-ready"]}`), and with no `/bin/sh` in the
|
||||||
|
# image that probe fails in ~30 ms with empty output on every interval while the
|
||||||
|
# database answers perfectly — which is exactly how this service's first deploy
|
||||||
|
# died on 2026-10-19 (`did not become healthy in 90s`). The quoted array survives
|
||||||
|
# the generator (`--health-cmd "[\"/surreal\",\"is-ready\"]"`) and stores
|
||||||
|
# `["CMD","/surreal","is-ready"]`, which execs directly and goes healthy. Do not
|
||||||
|
# "simplify" it back to a bare command.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=SurrealDB (open-notebook datastore)
|
Description=SurrealDB (open-notebook datastore)
|
||||||
@@ -70,7 +79,7 @@ EnvironmentFile=/home/dank/serverAppConfigs/scripts/open-notebook.env
|
|||||||
EnvironmentFile=/home/dank/Containers/open-notebook/env
|
EnvironmentFile=/home/dank/Containers/open-notebook/env
|
||||||
Exec=start --log info rocksdb:/mydata/mydatabase.db
|
Exec=start --log info rocksdb:/mydata/mydatabase.db
|
||||||
Volume=open-notebook-db:/mydata
|
Volume=open-notebook-db:/mydata
|
||||||
HealthCmd=/surreal is-ready
|
HealthCmd=["/surreal","is-ready"]
|
||||||
HealthInterval=30s
|
HealthInterval=30s
|
||||||
HealthTimeout=10s
|
HealthTimeout=10s
|
||||||
HealthRetries=3
|
HealthRetries=3
|
||||||
|
|||||||
@@ -19,11 +19,14 @@
|
|||||||
# is recorded in the parent tree, so cloning the parent commit recursively is
|
# is recorded in the parent tree, so cloning the parent commit recursively is
|
||||||
# reproducible without a second pin.
|
# reproducible without a second pin.
|
||||||
#
|
#
|
||||||
# THE BUILD LINE IS THE ONE PROVEN ON DANKCLAW, verbatim:
|
# THE BUILD LINE IS THE ONE PROVEN ON DANKCLAW, verbatim — with the Vulkan door
|
||||||
|
# open since 2026-10-19. That configure was run in this exact base image and
|
||||||
|
# answers `Found Vulkan: /usr/lib/x86_64-linux-gnu/libvulkan.so (found version
|
||||||
|
# "1.3.275") found components: glslc` then `Including Vulkan backend`:
|
||||||
#
|
#
|
||||||
# cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CCACHE=OFF \
|
# cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CCACHE=OFF \
|
||||||
# -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_OPENMP=ON \
|
# -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_OPENMP=ON \
|
||||||
# -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=OFF \
|
# -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=ON \
|
||||||
# -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF \
|
# -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF \
|
||||||
# && cmake --build build -j 8
|
# && cmake --build build -j 8
|
||||||
#
|
#
|
||||||
@@ -42,8 +45,12 @@
|
|||||||
# defaults it ON outside wasm/cross-compiles, i.e. -march=native for THIS
|
# defaults it ON outside wasm/cross-compiles, i.e. -march=native for THIS
|
||||||
# host — correct while build host == run host, wrong if anyone ever builds
|
# host — correct while build host == run host, wrong if anyone ever builds
|
||||||
# this image elsewhere, which is why the fleet builds it on Dankclaw.
|
# this image elsewhere, which is why the fleet builds it on Dankclaw.
|
||||||
# CUDA/HIP/VULKAN/METAL/SYCL/RPC all OFF: this is a CPU endpoint (the trial
|
# CUDA/HIP/METAL/SYCL/RPC stay OFF, VULKAN is ON: the endpoint computes on the
|
||||||
# ran on CPU; the box's GPUs belong to the text endpoints).
|
# gfx1151 iGPU (decision and co-tenancy gates:
|
||||||
|
# report/tts-npu-spike-verdict-2026-10-19.md, shape in [[Frank TTS Engine]]).
|
||||||
|
# Nothing else is available to this image: no ROCm userland, no NVIDIA
|
||||||
|
# userland, no macOS, no oneAPI, and RPC would add a second process to
|
||||||
|
# supervise on a box that already has one engine per card.
|
||||||
#
|
#
|
||||||
# BUILD SHARED LIBS, RUNTIME SMALL. BUILD_SHARED_LIBS defaults ON on Linux, so
|
# BUILD SHARED LIBS, RUNTIME SMALL. BUILD_SHARED_LIBS defaults ON on Linux, so
|
||||||
# tts-server links libggml*.so rather than swallowing them; the runtime stage
|
# tts-server links libggml*.so rather than swallowing them; the runtime stage
|
||||||
@@ -67,9 +74,28 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:68ee22004a9109b44973313b02f
|
|||||||
# ------------------------------------------------------------------ build
|
# ------------------------------------------------------------------ build
|
||||||
FROM ${BASE_IMAGE} AS build
|
FROM ${BASE_IMAGE} AS build
|
||||||
|
|
||||||
|
# The three Vulkan build deps are not optional and not interchangeable:
|
||||||
|
# libvulkan-dev vulkan/vulkan.h + the VulkanHeaders/VulkanLoader CMake
|
||||||
|
# configs, i.e. what find_package(Vulkan) resolves to.
|
||||||
|
# glslc ggml compiles its shaders with glslc, not glslangValidator.
|
||||||
|
# Ubuntu's glslang-tools ships glslangValidator and NOT glslc,
|
||||||
|
# so without this package `find_package(Vulkan COMPONENTS
|
||||||
|
# glslc REQUIRED)` fails and there is no Vulkan backend.
|
||||||
|
# The archive's glslc is old (2023.8): it compiles
|
||||||
|
# GL_KHR_cooperative_matrix — the path ggml's quantized
|
||||||
|
# matmul needs — but reports cooperative_matrix2,
|
||||||
|
# integer_dot_product, bfloat16 and the micro-float
|
||||||
|
# extensions as unsupported, so those shader families are
|
||||||
|
# compiled out. If a Vulkan prefill ever reads as
|
||||||
|
# inexplicably slow, suspect this compiler before the device;
|
||||||
|
# the fix is a pinned glslang build passed as
|
||||||
|
# -DVulkan_GLSLC_EXECUTABLE=<path>, not a different runtime.
|
||||||
|
# spirv-headers find_package(SPIRV-Headers CONFIG REQUIRED); the distro
|
||||||
|
# package ships /usr/share/cmake/SPIRV-Headers/.
|
||||||
RUN apt-get update -qq \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install -y -qq --no-install-recommends \
|
&& apt-get install -y -qq --no-install-recommends \
|
||||||
ca-certificates cmake g++ git make \
|
ca-certificates cmake g++ git make \
|
||||||
|
libvulkan-dev glslc spirv-headers \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Checked out detached at a full SHA, then the recorded submodule. No branch
|
# Checked out detached at a full SHA, then the recorded submodule. No branch
|
||||||
@@ -90,7 +116,7 @@ RUN cmake -B build -DCMAKE_BUILD_TYPE=Release \
|
|||||||
-DGGML_BLAS=OFF \
|
-DGGML_BLAS=OFF \
|
||||||
-DGGML_CUDA=OFF \
|
-DGGML_CUDA=OFF \
|
||||||
-DGGML_HIP=OFF \
|
-DGGML_HIP=OFF \
|
||||||
-DGGML_VULKAN=OFF \
|
-DGGML_VULKAN=ON \
|
||||||
-DGGML_METAL=OFF \
|
-DGGML_METAL=OFF \
|
||||||
-DGGML_SYCL=OFF \
|
-DGGML_SYCL=OFF \
|
||||||
-DGGML_RPC=OFF \
|
-DGGML_RPC=OFF \
|
||||||
@@ -102,9 +128,21 @@ FROM ${BASE_IMAGE}
|
|||||||
# libgomp1: the OpenMP runtime the build above links against. curl: the unit's
|
# libgomp1: the OpenMP runtime the build above links against. curl: the unit's
|
||||||
# HealthCmd. ca-certificates: so that curl can speak https if anyone ever
|
# HealthCmd. ca-certificates: so that curl can speak https if anyone ever
|
||||||
# points a probe at a URL with a scheme this host terminates elsewhere.
|
# points a probe at a URL with a scheme this host terminates elsewhere.
|
||||||
|
#
|
||||||
|
# libvulkan1 + mesa-vulkan-drivers are what make `--device Vulkan0` mean
|
||||||
|
# anything at runtime: libvulkan1 is the loader libggml-vulkan.so dlopens, and
|
||||||
|
# mesa-vulkan-drivers is the RADV ICD that claims the gfx1151. Loader without an
|
||||||
|
# ICD enumerates ZERO devices, and tts-server then dies in device lookup AFTER
|
||||||
|
# both GGUFs have been read — the journal reads as a model fault, not a missing
|
||||||
|
# driver. Enumerated from this base on 2026-10-19 with `--device /dev/dri
|
||||||
|
# --group-add keep-groups` plus the host's ComfyUI udev rule: "Radeon 8060S
|
||||||
|
# Graphics (RADV GFX1151)", API 1.4.318 (the heap figure is owned by
|
||||||
|
# [[Frank TTS Engine]]; it is the GTT-backed aperture, not the ~2 GiB DRM
|
||||||
|
# carve-out). No NVIDIA ICD is installed on purpose: one Vulkan device in the
|
||||||
|
# enumeration is what makes the NAME Vulkan0 safe to write down.
|
||||||
RUN apt-get update -qq \
|
RUN apt-get update -qq \
|
||||||
&& apt-get install -y -qq --no-install-recommends \
|
&& apt-get install -y -qq --no-install-recommends \
|
||||||
ca-certificates curl libgomp1 \
|
ca-certificates curl libgomp1 libvulkan1 mesa-vulkan-drivers \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Same unprivileged uid as the frank-tts façade (10000): the voice packs and the
|
# Same unprivileged uid as the frank-tts façade (10000): the voice packs and the
|
||||||
|
|||||||
@@ -40,12 +40,28 @@
|
|||||||
# speaks to the server from inside its own namespace, which is where the address
|
# speaks to the server from inside its own namespace, which is where the address
|
||||||
# is valid.
|
# is valid.
|
||||||
#
|
#
|
||||||
# MEMORY / NO GPU. Peak RSS measured 5.7 GiB with both GGUFs resident
|
# MEMORY / THE RENDER NODE. Host RSS is ~1.7 GiB, not the size of the GGUFs:
|
||||||
# (2026-10-18 trial); no memory limit is set because the box has the room and a
|
# the Vulkan backend copies both tensors into device memory at load, so they
|
||||||
# limit would convert a busy moment into an OOM kill mid-utterance. No
|
# stop counting in the process's RSS (measured after the 2026-10-19 cutover —
|
||||||
# /dev/kfd, no /dev/dri, no keep-groups: every GPU backend is compiled out of
|
# `podman stats` / `ps -o rss`). No memory limit is set: the box has the room
|
||||||
# the image, the endpoint is CPU-only by decision, and the GPUs on this host
|
# and a limit would convert a busy moment into an OOM kill mid-utterance.
|
||||||
# belong to the text endpoints.
|
#
|
||||||
|
# `AddDevice=/dev/dri` is what makes the iGPU visible; without it the Vulkan
|
||||||
|
# ICD enumerates nothing and the server dies on device lookup after it has
|
||||||
|
# already read both GGUFs. `GroupAdd=keep-groups` is belt-and-braces, not the
|
||||||
|
# thing that works here: the units podman spawns carry no supplementary groups
|
||||||
|
# at all, and this host grants render access a different way — the udev rule
|
||||||
|
# that makes the render nodes world-writable (same rule ComfyUI rides, see
|
||||||
|
# dankclaw/comfyui). Measured: `id` inside this container lists only the
|
||||||
|
# container's own group, and `tts-server` still holds renderD129.
|
||||||
|
# The node is NOT named: this host is hybrid, renderD128 is the NVIDIA card and
|
||||||
|
# renderD129 the amdgpu iGPU, so a node number in the unit is a guess that a
|
||||||
|
# re-probe can invalidate, while the directory hands the container every render
|
||||||
|
# node and the driver picks by name.
|
||||||
|
#
|
||||||
|
# /dev/kfd stays absent on purpose: that is the ROCm compute interface, owned by
|
||||||
|
# the engines that use HIP. A Vulkan endpoint has no use for it, and handing a
|
||||||
|
# container a second way onto the GPU is surface for nothing.
|
||||||
#
|
#
|
||||||
# THE VOICE MOUNT IS READ-ONLY AND IS FOR THE WARMER, NOT THE ENGINE. A voice
|
# THE VOICE MOUNT IS READ-ONLY AND IS FOR THE WARMER, NOT THE ENGINE. A voice
|
||||||
# becomes known to the engine only through POST /v1/audio/voices, and the
|
# becomes known to the engine only through POST /v1/audio/voices, and the
|
||||||
@@ -98,6 +114,9 @@ Network=frank-tts-engine.network
|
|||||||
EnvironmentFile=/home/dank/serverAppConfigs/scripts/frank-tts-engine.env
|
EnvironmentFile=/home/dank/serverAppConfigs/scripts/frank-tts-engine.env
|
||||||
Volume=/home/dank/models/Qwen3-TTS-GGUF:/models:ro
|
Volume=/home/dank/models/Qwen3-TTS-GGUF:/models:ro
|
||||||
Volume=/home/dank/voices:/voices:ro
|
Volume=/home/dank/voices:/voices:ro
|
||||||
|
# Render access for the iGPU (see MEMORY / THE RENDER NODE above).
|
||||||
|
AddDevice=/dev/dri
|
||||||
|
GroupAdd=keep-groups
|
||||||
HealthCmd=curl -fsS http://127.0.0.1:8880/health
|
HealthCmd=curl -fsS http://127.0.0.1:8880/health
|
||||||
HealthInterval=30s
|
HealthInterval=30s
|
||||||
HealthTimeout=10s
|
HealthTimeout=10s
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# supervision, no retries, no logging of its own. PID 1 stays the server, so
|
# supervision, no retries, no logging of its own. PID 1 stays the server, so
|
||||||
# `podman stop` and the unit's Restart=always drive it directly.
|
# `podman stop` and the unit's Restart=always drive it directly.
|
||||||
#
|
#
|
||||||
# THE FLAGS THAT ARE NOT HERE, and must stay absent:
|
# THE FLAG THAT MUST STAY ABSENT:
|
||||||
#
|
#
|
||||||
# --threads / -t does not exist in this build. src/backend.h computes the
|
# --threads / -t does not exist in this build. src/backend.h computes the
|
||||||
# CPU thread count as std::thread::hardware_concurrency()/2
|
# CPU thread count as std::thread::hardware_concurrency()/2
|
||||||
@@ -18,13 +18,19 @@
|
|||||||
# placement — pinning is deliberately not done anywhere in
|
# placement — pinning is deliberately not done anywhere in
|
||||||
# this stack. See the env file header for the same warning
|
# this stack. See the env file header for the same warning
|
||||||
# aimed at the operator.
|
# aimed at the operator.
|
||||||
# --device left at its default. Every GPU backend is compiled out of
|
|
||||||
# the image, so the default device pick can only land on
|
|
||||||
# CPU; naming a device would add a way to be wrong.
|
|
||||||
#
|
#
|
||||||
# Everything defaults to the shape the 2026-10-18 trial measured, so an empty
|
# --device IS here, but conditionally: it reaches the binary only when
|
||||||
# environment still boots the decided configuration: 1.7 B talker Q4_K_M with
|
# QWENTTS_DEVICE is non-empty, and empty is the CPU rollback — the binary then
|
||||||
# the Q8_0 tokenizer, one request at a time, on the private bridge.
|
# takes its own default device, which is CPU because the CPU backend is always
|
||||||
|
# built. One line in scripts/frank-tts-engine.env moves the endpoint between the
|
||||||
|
# two shapes, with no rebuild. What must never reach argv is a device NAME the
|
||||||
|
# loader does not enumerate: that is not refused by the argument parser, it dies
|
||||||
|
# during device lookup with the loader's own error, so the journal reads as a
|
||||||
|
# load that failed rather than a configuration refusal.
|
||||||
|
#
|
||||||
|
# Everything defaults to the shape this endpoint runs: 1.7 B talker Q4_K_M with
|
||||||
|
# the Q8_0 tokenizer, one request at a time, on the private bridge, on the
|
||||||
|
# gfx1151 iGPU. An empty environment still boots that shape.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# The binary path. Overridable for one reason: the bats seam runs a stub
|
# The binary path. Overridable for one reason: the bats seam runs a stub
|
||||||
@@ -60,13 +66,33 @@ set -euo pipefail
|
|||||||
: "${QWENTTS_LANG:=auto}"
|
: "${QWENTTS_LANG:=auto}"
|
||||||
|
|
||||||
# One request at a time. The server-side queue is a serialised FIFO at this
|
# One request at a time. The server-side queue is a serialised FIFO at this
|
||||||
# setting, which is what keeps the measured per-voice timings true; batching
|
# setting, which is what keeps the per-voice timings predictable. A second
|
||||||
# on a CPU build splits the same cores between two syntheses and the warm RTF
|
# concurrent synthesis shares the device with the first — and on this box that
|
||||||
# margin (0.55-0.67 against real time 1.0) disappears. The engine's own
|
# device is also carrying frank-dragoon — so it buys no throughput and it
|
||||||
# default is also 1; it is written here because it is a decision, not an
|
# doubles the wait for the caller behind it. The engine's own default is also 1;
|
||||||
# accident.
|
# written here because it is a decision, not an accident.
|
||||||
: "${QWENTTS_MAX_BATCH:=1}"
|
: "${QWENTTS_MAX_BATCH:=1}"
|
||||||
|
|
||||||
|
# The compute device, NAMED and not numbered: `Vulkan0` is how ggml enumerates
|
||||||
|
# the gfx1151 iGPU on this host. An index would follow whatever order the
|
||||||
|
# loader's device list returns; a name says which device this endpoint means.
|
||||||
|
# The container sees exactly one Vulkan device because the image carries the
|
||||||
|
# Mesa ICD and no NVIDIA one, so the 3090 is not in the enumeration at all.
|
||||||
|
#
|
||||||
|
# EMPTY IS THE CPU ROLLBACK: a value that is set but EMPTY passes no `--device`,
|
||||||
|
# the binary picks its own default, and the CPU backend is always built — which
|
||||||
|
# is why moving this endpoint between the iGPU and the CPU is one line in
|
||||||
|
# scripts/frank-tts-engine.env and not a rebuild.
|
||||||
|
#
|
||||||
|
# That is also why the default below is NOT `${QWENTTS_DEVICE:=Vulkan0}`: `:=`
|
||||||
|
# fires on a set-but-empty variable as well as an unset one, so the rollback line
|
||||||
|
# `QWENTTS_DEVICE=` would be rewritten back to Vulkan0 and the endpoint would sit
|
||||||
|
# on the shared GPU with no way out short of a rebuild. Only UNSET takes the
|
||||||
|
# default.
|
||||||
|
if [[ ! -v QWENTTS_DEVICE ]]; then
|
||||||
|
QWENTTS_DEVICE=Vulkan0
|
||||||
|
fi
|
||||||
|
|
||||||
# Fail loudly BEFORE binding, so the journal says which artifact is missing
|
# Fail loudly BEFORE binding, so the journal says which artifact is missing
|
||||||
# instead of the unit reporting a container that starts and dies. This is the
|
# instead of the unit reporting a container that starts and dies. This is the
|
||||||
# self-healing path: Restart=always re-runs it every RestartSec, and the same
|
# self-healing path: Restart=always re-runs it every RestartSec, and the same
|
||||||
@@ -79,11 +105,20 @@ for artifact in "$QWENTTS_MODEL" "$QWENTTS_CODEC"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
exec "$QWENTTS_BIN" \
|
ARGS=(
|
||||||
--model "$QWENTTS_MODEL" \
|
--model "$QWENTTS_MODEL"
|
||||||
--codec "$QWENTTS_CODEC" \
|
--codec "$QWENTTS_CODEC"
|
||||||
--alias "$QWENTTS_ALIAS" \
|
--alias "$QWENTTS_ALIAS"
|
||||||
--host "$QWENTTS_HOST" \
|
--host "$QWENTTS_HOST"
|
||||||
--port "$QWENTTS_PORT" \
|
--port "$QWENTTS_PORT"
|
||||||
--lang "$QWENTTS_LANG" \
|
--lang "$QWENTTS_LANG"
|
||||||
--max-batch "$QWENTTS_MAX_BATCH"
|
--max-batch "$QWENTTS_MAX_BATCH"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only when a device is named. An empty QWENTTS_DEVICE has to produce NO flag,
|
||||||
|
# because "the rollback shape" is argv identical to the pre-iGPU endpoint.
|
||||||
|
if [[ -n "$QWENTTS_DEVICE" ]]; then
|
||||||
|
ARGS+=(--device "$QWENTTS_DEVICE")
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$QWENTTS_BIN" "${ARGS[@]}"
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
# XDNA2 NPU inference runtimes on Linux (Strix Halo) — can any of them serve TTS?
|
||||||
|
|
||||||
|
Research slice, 2026-10-19. Worker: `NpuRuntimeLandscape`. Research only, no code.
|
||||||
|
Question: every way to execute model inference on an AMD XDNA2 NPU on Linux as of
|
||||||
|
2026-10, and whether any of them can serve a speech-synthesis model.
|
||||||
|
|
||||||
|
> **Status, same night — the spike that followed this slice settled it.** NO-GO on the
|
||||||
|
> NPU (the loader declines the graph, not the silicon); the actionable door is the gfx1151
|
||||||
|
> iGPU behind `-DGGML_VULKAN=ON`. Measured contract, exoneration chain and host gates:
|
||||||
|
> `tts-npu-spike-verdict-2026-10-19.md`. That page supersedes §8–§9 below.
|
||||||
|
|
||||||
|
Convention: vendor claims carry URL + the version/date they hold for.
|
||||||
|
**[host]** = Dankclaw state verified on the live machine 2026-10-19 (Main), not a
|
||||||
|
vendor claim. `[INFERENCE]` = my deduction from the cited artifacts.
|
||||||
|
|
||||||
|
## 0. Answers to the three questions Main ranked
|
||||||
|
|
||||||
|
**(1) A runtime that already runs TTS on XDNA2 today: it exists at AMD, not on our
|
||||||
|
host.** Ryzen AI Software **1.8.0** (docs `latest` == 1.8.0; released 2026-07-23)
|
||||||
|
lists `neutts-air` and `neutts-nano` as model type **TTS**, variant
|
||||||
|
*full-optimized* (NPU-only) — <https://ryzenai.docs.amd.com/en/latest/llm_list.html>;
|
||||||
|
release note "New TTS model support: neutts-air, neutts-nano" —
|
||||||
|
<https://ryzenai.docs.amd.com/en/latest/relnotes.html>. Pre-compiled artifacts are
|
||||||
|
published: `amd/neutts-air_rai_1.8.0_npu_4K` (arch `qwen2`, lastModified
|
||||||
|
2026-07-23, Apache-2.0, "AWQ / Group 128 / Asymmetric / BFP16 activations / UINT4
|
||||||
|
Weights") and `amd/neutts-nano_rai_1.8.0_npu_4K` (arch `llama`).
|
||||||
|
**Neither artifact is a TTS engine.** Both file lists are LM-only (`model.onnx`,
|
||||||
|
`genai_config.json`, BPE tokenizer, AIE-ML compiler metastate: `dd_metastate_*`
|
||||||
|
`.ctrlpkt/.super/.state`, `cache/txn_bins.zip`) with **no codec, no vocoder, no
|
||||||
|
reference-audio encoder**. [INFERENCE from HF file lists]
|
||||||
|
|
||||||
|
**(2) FastFlowLM: no TTS, and we cannot add one** — see §2. Halogen: no TTS entry
|
||||||
|
**[host]**. So "something installed already serves TTS on the NPU" is FALSE.
|
||||||
|
|
||||||
|
**(3) Our own ONNX graph compiled for the NPU on Linux:** possible for the plain
|
||||||
|
ONNX flows (they compile on Linux), **impossible for a new LLM/OGA model** — model
|
||||||
|
generation is Windows-only in 1.8.0 — and gated by an operator table where BF16
|
||||||
|
lacks Softmax/LayerNorm/ReLU/Gelu (see §4).
|
||||||
|
|
||||||
|
## 1. Landscape: every way to reach the XDNA2 array on Linux
|
||||||
|
|
||||||
|
| # | Path | License | Strix Halo + Linux, 2026-10 | Speech synthesis |
|
||||||
|
|---|------|---------|------------------------------|------------------|
|
||||||
|
| 1 | Ryzen AI SW → ONNX Runtime + Vitis AI / Ryzen AI EP; compile INT8 (`X2`) or BF16 (`VAIML`) ONNX at session creation | AMD proprietary (tarball) | supported: our device ID/rev is in AMD's STX table; compile+run works on Linux (§3, §4) | user-authored ONNX only; no TTS recipe/demo |
|
||||||
|
| 2 | Same EP driven by **OGA** (`onnxruntime-genai`), "full fusion" NPU-only LLM flow; runner = AMD `model_benchmark` / OGA C++ API | AMD + MS OGA | Linux: **vendor-precompiled models only**, no hybrid flow | **neutts-air / neutts-nano = TTS** (LM half) |
|
||||||
|
| 3 | **FastFlowLM** `flm` | MIT + free proprietary kernels | Strix Halo "Supported"; Ubuntu 24.04/25.10/**26.04**, Arch | none (ASR input only) |
|
||||||
|
| 4 | **IRON / mlir-aie** + Peano → hand-written AIE-ML design → XRT/`amdxdna` | Apache-2.0 + LLVM exception | explicitly documented "AMD Ryzen™ AI on Linux"; same driver/XRT stack | anything, hand-written; nothing ships TTS |
|
||||||
|
| 5 | Vendor-curated AIE-ML graph sets (halogen `HALOGEN_NPU_MODELS`, FLM packs) | vendor | present **[host]** | none **[host]** |
|
||||||
|
| 6 | Windows-only: DirectML, Ryzen AI Windows EP (`ryzenai-llm`), Studio Effects, `whispercpp` NPU | — | not usable | (per Lemonade matrix: Windows only) |
|
||||||
|
| 7 | ggml/llama.cpp/**qwentts.cpp**, whisper.cpp, sherpa-onnx, Piper | — | no XDNA backend in ggml/ORT-mainline | CPU / iGPU only |
|
||||||
|
| 8 | IREE, TVM, ONNX Runtime mainline EPs (OpenVINO = Intel NPU, MIGraphX/ROCm = AMD GPU) | — | no XDNA2 target; AMD's NPU EP is only "Xilinx Vitis-AI (preview)" in the ORT EP list and ships inside AMD's own build | n/a |
|
||||||
|
|
||||||
|
Sources: §2–§6 below.
|
||||||
|
|
||||||
|
## 2. FastFlowLM (`/usr/bin/flm` 1.0.1 **[host]**) — catalog + HTTP surface
|
||||||
|
|
||||||
|
* Repo moved into AMD's ROCm org **2026-08-11** (`github.com/ROCm/FastFlowLM`,
|
||||||
|
v1.0.0). "Supports all Ryzen™ AI Series chips with XDNA2 NPUs (Strix, Strix Halo,
|
||||||
|
Kraken, and Gorgon Point)."
|
||||||
|
<https://github.com/ROCm/FastFlowLM> (README fetched 2026-10-19).
|
||||||
|
* Linux support landed 2026-03; distros: Ubuntu 24.04, 25.10, **26.04**, Arch,
|
||||||
|
generic. Needs NPU firmware ≥ 1.1.0.0, kernel **7.0+** with `amdxdna` (or
|
||||||
|
`amdxdna-dkms`), XRT stack (`ppa:lemonade-team/stable` → `libxrt-npu2`,
|
||||||
|
`amdxdna-dkms`); `flm validate` reads the DRM path (`/dev/accel/accel0`), `flm
|
||||||
|
run` goes through **XRT** and fails with `No such device with index '0'` unless
|
||||||
|
`xrt-smi examine` lists the NPU; `memlock` must be raised.
|
||||||
|
<https://fastflowlm.com/docs/install_lin/> (page dated 2026-03-05, fetched
|
||||||
|
2026-10-19). Static build bundles **XRT v2.21.75** — the same XRT series as the
|
||||||
|
distro package on the host **[host]**.
|
||||||
|
* HTTP surface: `flm serve <model>` → OpenAI-compatible REST, **port 52625**, no
|
||||||
|
auth. Maintained endpoints: `v1/models`, `v1/chat/completions`,
|
||||||
|
`v1/audio/transcriptions`, `v1/embeddings`. **There is no
|
||||||
|
`/v1/audio/speech`.**
|
||||||
|
<https://fastflowlm.com/docs/instructions/server/openapi/>
|
||||||
|
* Catalog 2026-10-19 (<https://fastflowlm.com/docs/models/>): LLaMA, DeepSeek,
|
||||||
|
Qwen, Gemma, MedGemma, TranslateGemma, gpt-oss, LiquidAI/LFM, Phi, Nanbeige,
|
||||||
|
**Whisper — card states "Type: Speech-to-Text (ASR)"**
|
||||||
|
(<https://fastflowlm.com/docs/models/whisper/>), EmbeddingGemma, SmolVLA,
|
||||||
|
Hy-MT2, MiniCPM. No speech-synthesis entry.
|
||||||
|
* Engines are **prebuilt, closed, downloaded from Hugging Face per architecture**
|
||||||
|
("Internet access to HuggingFace is required to download the optimized model
|
||||||
|
kernels"). The only in-tree engine build is Gemma-4E
|
||||||
|
(`cmake --preset linux-default -DFLM_BUILD_GEMMA4E=ON`).
|
||||||
|
→ **A new TTS architecture cannot be added to `flm` by us.**
|
||||||
|
|
||||||
|
## 3. AMD Ryzen AI Software 1.8.0 on Linux — install source, device support
|
||||||
|
|
||||||
|
* **Install channel (important correction to the pypi.amd.com evidence).** The pip
|
||||||
|
index is Windows-only for the EP runtime (`ryzenai_dynamic_dispatch-1.7.0-…
|
||||||
|
win_amd64.whl`, `onnxruntime_vitisai-1.23.2-…win_amd64.whl`; `flexmlrt` has a
|
||||||
|
single **linux_aarch64** wheel) — but AMD's Linux stack is not on pip. It ships
|
||||||
|
as a tarball that is, as of 2026-10-19, **publicly reachable without an AMD
|
||||||
|
account**: `https://download.amd.com/opendownload/RyzenAI/1.8.0/ryzen_ai-1.8.0.tgz`
|
||||||
|
→ HTTP 206 on a range probe, `Content-Range: bytes 0-8/**7819506855**`
|
||||||
|
(**≈7.8 GiB**, magic `1f 8b 08` = gzip). (The docs link it behind the
|
||||||
|
EULA form `account.amd.com/...ryzenai-eula-public-xef.html`; the CDN path
|
||||||
|
serves it directly. Verified by me with curl 2026-10-19. I did not finish
|
||||||
|
downloading 7.8 GiB, so the tarball's *contents* list is unverified —
|
||||||
|
`[INFERENCE]` only where noted.)
|
||||||
|
* Driver/XRT package (public, 90,016,137 bytes, `application/zip`):
|
||||||
|
`https://download.amd.com/opendownload/RyzenAI/Driver/RAI_1.8_Linux_NPU_XRT.zip`
|
||||||
|
contains `xrt_202620.2.25.37_24.04-amd64-{base,base-dev,npu}.deb` +
|
||||||
|
`xrt_plugin.2.25.260102.56.release_24.04-amd64-amdxdna.deb` (verified by
|
||||||
|
download + unpack). **The plugin deb ships firmware for
|
||||||
|
`/usr/lib/firmware/amdnpu/17f0_11/npu.dev.sbin`** — exactly the directory our
|
||||||
|
NPU loads **[host]** — plus `1502_00`, `17f0_10`, `17f1_10`, `17f1_13`,
|
||||||
|
`libvxdna.so`, `libxrt_driver_xdna.so.2.25.260102.56`, and the DKMS driver
|
||||||
|
source tarball. **No xclbin/PDI and no compiler binary in this package**: it is
|
||||||
|
driver + XRT shim + firmware only.
|
||||||
|
* **Our device is officially STX.** AMD's APU table
|
||||||
|
(<https://ryzenai.docs.amd.com/en/latest/app_development.md#apu-types>):
|
||||||
|
`0x1022:0x17F0 rev 0x11 → STX` (0x00/0x10 also STX, 0x20 = KRK, `0x1502` rev 0
|
||||||
|
= PHX/HPT). Release notes: "Strix and **Strix Halo** as STX", and the STX/KRK
|
||||||
|
column enables CNN INT8, CNN BF16, NLP BF16, LLM (OGA).
|
||||||
|
* Vendor driver source (same deb, `share/amdxdna/amdxdna.tar.gz`, source dated
|
||||||
|
2026-06-29) maps `0x17f0 rev 0x11 → dev_npu5_info`, `.fw_path =
|
||||||
|
"amdnpu/17f0_11/"`, `default_vbnv = "RyzenAI-npu5"`, and the revision→name table
|
||||||
|
has `{ AIE2_DEV_REVISION_HALO, "NPU Strix Halo" }`; `xrt-smi examine` therefore
|
||||||
|
reports the device as **NPU Strix Halo**.
|
||||||
|
* Linux install recipe (1.8.0 docs
|
||||||
|
<https://ryzenai.docs.amd.com/en/latest/linux.html>): Ubuntu 24.04 packages,
|
||||||
|
**Python 3.12** (`apt install python3.12 python3.12-venv`),
|
||||||
|
`libboost-filesystem1.74.0`, dkms; then `install_ryzen_ai.sh -a yes -p <venv>`;
|
||||||
|
`LD_LIBRARY_PATH=…:${RYZEN_AI_INSTALLATION_PATH}/onnxruntime/lib/`;
|
||||||
|
`quicktest.py` "picks up a simple CNN model, **compiles it** and runs on AMD's
|
||||||
|
Neural Processing Unit" — i.e. **compilation runs on Linux** for the non-LLM
|
||||||
|
flows. "Linux currently supports NPU only flow" (no iGPU EP).
|
||||||
|
Host friction: our box is Ubuntu 26.04 + **Python 3.14** **[host]**, so the venv
|
||||||
|
must be built with a 3.12 interpreter and the packages are 24.04-built
|
||||||
|
`[INFERENCE]`.
|
||||||
|
* LLM flow on Linux
|
||||||
|
(<https://ryzenai.docs.amd.com/en/latest/llm_linux.html>): prequantized
|
||||||
|
ready-to-run models from HF, `deployment/lib`,
|
||||||
|
`RYZENAI_EP_PATH=$PWD/deployment/lib/libonnxruntime_providers_ryzenai.so`,
|
||||||
|
run via the `model_benchmark` binary; **"Linux does not support Hybrid flow"**;
|
||||||
|
**"Model generation is not supported in current release"** (repeated in
|
||||||
|
relnotes 1.8 Known Issues: "Models generated on Windows are compatible with
|
||||||
|
Linux"); needs `memlock unlimited`.
|
||||||
|
Sample throughput in that doc: **49.1 tokens/s decode**, 753 tokens/s prefill
|
||||||
|
(Phi-3.5-mini, 4K model).
|
||||||
|
* Third-party confirmation of the OS split: Lemonade's backend matrix
|
||||||
|
(<https://raw.githubusercontent.com/lemonade-sdk/lemonade/main/README.md>,
|
||||||
|
fetched 2026-10-19) — `flm`/npu/XDNA2 = **Windows, Linux**;
|
||||||
|
`ryzenai-llm`/npu = **Windows only**; `whispercpp`/npu (STT) = **Windows only**;
|
||||||
|
text-to-speech engines `kokoro` and `acestep` = metal / cuda / vulkan / rocm,
|
||||||
|
**no npu device at all**. Lemonade is the "Server Interface" layer of AMD's own
|
||||||
|
Ryzen AI stack (docs index), so this matrix is the vendor-sanctioned view.
|
||||||
|
|
||||||
|
## 4. Can we compile our own graph on Linux? (the no-recipe path)
|
||||||
|
|
||||||
|
Mechanics from <https://ryzenai.docs.amd.com/en/latest/modelrun.md> (1.8.0):
|
||||||
|
ONNX only, opset 17 recommended; the EP compiles on first `InferenceSession` and
|
||||||
|
caches (VitisAI EP cache, or ORT **EP context cache** — `ep.context_enable`, and
|
||||||
|
"precompile the EP context model using Python and deploy it using a C++ program"
|
||||||
|
is explicitly supported); EP auto-partitions unsupported nodes to CPU;
|
||||||
|
BF16 = `target VAIML` (`preferred_data_storage: unvectorized` for transformers),
|
||||||
|
BF16 **STX/KRK-or-newer only**; INT8 = `target=X2` with XINT8/A8W8/A16W8 and
|
||||||
|
"general asymmetric quantization … to enable third-party quantized models";
|
||||||
|
**deployment EP cannot compile BF16 on the fly** — ship a pre-compiled model
|
||||||
|
(<https://ryzenai.docs.amd.com/en/latest/app_development.md>);
|
||||||
|
cache dirs are not portable across EP or NPU-driver versions.
|
||||||
|
|
||||||
|
Operator coverage is the hard gate
|
||||||
|
(<https://ryzenai.docs.amd.com/en/latest/ops_support.md>, "operators supported in
|
||||||
|
Ryzen AI 1.8.0"; "Y" = broad coverage, some configurations may still fail):
|
||||||
|
|
||||||
|
* Good for TTS front-ends/vocoders: `ConvTranspose` **Y in BF16/A16W8/A8W8**
|
||||||
|
(absent only from XINT8), `Conv`, `Resize`, `Upsample`, `Pad`, `LSTM` (BF16),
|
||||||
|
`CumSum` (BF16/XINT8), `STFT` (XINT8 only).
|
||||||
|
* **Blockers for a decoder-style transformer in the plain flows:** in **BF16** the
|
||||||
|
table has **no Softmax, no LayerNormalization, no Relu/Gelu**; LayerNorm is
|
||||||
|
XINT8-only and Softmax is A16W8/A8W8/XINT8-only. So attention either lives in
|
||||||
|
the **LLM/OGA flow** (whose op set is exactly
|
||||||
|
`SimplifiedLayerNormalization`, `SkipSimplifiedLayerNormalization`, `MatMulNBits`
|
||||||
|
(W4ABF16/W4ABFP16), `RotaryEmbedding`, **`GroupQueryAttention`**, `Sigmoid`,
|
||||||
|
`Mul`, `Add`) or must be decomposed into a quantized INT8 flow.
|
||||||
|
* Missing entirely: `GRU`, `Trilu`, `OneHot`, `EyeLike`, `Multinomial`,
|
||||||
|
`NonMaxSuppression`, `CastLike`, `MelWeightMatrix`/`MelFilterGroup`,
|
||||||
|
`RMSNormalization` → graphs that emit them must be rewritten before they
|
||||||
|
partition onto the NPU. [INFERENCE from absence in the table.]
|
||||||
|
* Quantization is a separate tool, not in the driver package: **AMD Quark**
|
||||||
|
(`quark.torch` / `quarkonnx`), which documents `ConvTranspose2d`, `conv1d`,
|
||||||
|
softmax/LayerNorm/BFP quantization (relnotes 1.3/1.2/1.1/0.9) — Quark is *not*
|
||||||
|
on pypi.org under the obvious names **[host probe]** and ships from AMD's
|
||||||
|
gated/pip-private channel.
|
||||||
|
* **New LLM-shaped model (our Qwen-Talker analogue): Windows-only today.**
|
||||||
|
OGA model generation is not available on Linux in 1.8.0 (§3). So a *new*
|
||||||
|
speech-LM cannot be self-served onto the NPU on Linux without AMD (or a
|
||||||
|
Windows box) in the loop.
|
||||||
|
* Practical shape cost: the neutts artifacts carry **one compiled variant per
|
||||||
|
padded sequence length** (128/256/512/768/1024/1536/2048/2304/3072/4096 for
|
||||||
|
prefill; 256…4096 for token-phase attention mask) — i.e. per-shape compile
|
||||||
|
artifacts are the norm for the LLM flow. [INFERENCE from the HF file list.]
|
||||||
|
|
||||||
|
## 5. Does the AMD TTS artifact actually produce speech? (cloned-voice survival)
|
||||||
|
|
||||||
|
NeuTTS is **LM + separate neural codec** ("Simple LM + codec architecture"),
|
||||||
|
clone from ~3 s of reference audio, and the codec is **NeuCodec (50 Hz, single
|
||||||
|
codebook)**: `neuphonic/neucodec` (PyTorch, encode+decode),
|
||||||
|
`neucodec-onnx-decoder{,-int8}` (**decode only**), "Decoder-only (ONNX) codecs
|
||||||
|
require **pre-encoded references** — these ship with the repo for all bundled
|
||||||
|
voices." Phoneme input; 2048-token context ≈ 30 s of audio; NeuTTS-Air ~360 M
|
||||||
|
active / 552 M with embeddings; upstream benchmarks are **CPU-only llama.cpp**
|
||||||
|
(119 tok/s prefill/decode mix on a Ryzen 9 HX 370) and explicitly exclude the
|
||||||
|
codec. <https://raw.githubusercontent.com/neuphonic/neutts/main/README.md>
|
||||||
|
(fetched 2026-10-19); `neuphonic/neutts-air` HF metadata (lastModified
|
||||||
|
2026-08-26, `qwen2`, GGUF + safetensors).
|
||||||
|
|
||||||
|
Consequences for the fleet spike:
|
||||||
|
|
||||||
|
1. AMD's NPU artifact = LM only; **waveform decode and reference encoding are CPU
|
||||||
|
work either way**. Our `reference.wav` voices survive only if we run the
|
||||||
|
NeuCodec **encoder** (PyTorch, per the codec table there is no ONNX encoder)
|
||||||
|
once per voice and then feed pre-encoded codes. That is a one-time offline
|
||||||
|
step per voice; after that the compiled graph takes codes/prompt prefix as
|
||||||
|
ordinary inputs, so nothing about the compiled graph forbids a caller-supplied
|
||||||
|
speaker prefix. `[INFERENCE]`
|
||||||
|
2. Real-time math: 50 Hz codec means **≥50 tokens/s decode** for 1× RT. AMD's own
|
||||||
|
Linux sample output for a 4K Phi-3.5-mini is **49.1 tok/s** — so budget for
|
||||||
|
roughly RT-1× on one 4K "full fusion" graph, before the codec's CPU cost.
|
||||||
|
`[INFERENCE — different model; must be measured.]`
|
||||||
|
3. Host RAM: the same doc reports **peak working set 3.54 GB** for that 4K model.
|
||||||
|
NeuTTS-Air is ~552 M params at W4A-BFP16, so expect a smaller footprint than
|
||||||
|
3.5 GB `[INFERENCE]`; still non-trivial against 14 GiB available **[host]**.
|
||||||
|
4. AMD ships **no TTS demo, runner, or server** on any OS: the examples page has
|
||||||
|
only CNN/BF16/DistilBERT/super-resolution/OGA-LLM/VLM/RAG
|
||||||
|
(<https://ryzenai.docs.amd.com/en/latest/examples.md>); the RyzenAI-SW repo
|
||||||
|
tree (669 paths, `main`, fetched 2026-10-19) contains speech demos **only under
|
||||||
|
`Demos/ASR/{Whisper,Parakeet-TDT}` and `Transformer-examples/ASR/Whisper-AI`**
|
||||||
|
— no TTS directory. Serving TTS therefore means writing our own OGA host
|
||||||
|
(or driving `model_benchmark`-style C++ code) plus phonemizer + NeuCodec glue.
|
||||||
|
|
||||||
|
## 6. Two graphs on one NPU, and the iGPU hazard
|
||||||
|
|
||||||
|
* **Concurrency is a first-class driver feature.** The vendor `amdxdna` source
|
||||||
|
(in `xrt_plugin.2.25.260102.56`, 2026-06-29) sets `.hwctx_limit = 16` for
|
||||||
|
npu4/npu5 — i.e. **16 hardware contexts** on our device — and allocates each
|
||||||
|
context a set of AIE columns with QoS `opc = col_opc (4096) × num_col`
|
||||||
|
(`aie2_ctx.c`, `aie2_solver.h`), with `frame_boundary_preempt` /
|
||||||
|
`fill_preempt_slot` in `aie2_pci.h` (firmware preemption at frame boundaries).
|
||||||
|
So halogen's armed models and a TTS graph can hold separate contexts
|
||||||
|
simultaneously; they contend for columns and OPC share, they are not mutually
|
||||||
|
exclusive.
|
||||||
|
* AMD's user-visible history agrees: 1.4 "Support for up to **16 hardware
|
||||||
|
contexts** on Ryzen AI 300 series"; 1.5 "Application concurrency: improves the
|
||||||
|
resource distribution across applications"; 1.3 "Idle detection and suspension
|
||||||
|
of contexts", "Rebalance feature for AIE hardware resource optimization";
|
||||||
|
1.0 known issue "Support for multiple applications is limited to up to eight"
|
||||||
|
(all in <https://ryzenai.docs.amd.com/en/latest/relnotes.html>).
|
||||||
|
* **Package conflict, the real risk** [INFERENCE]: the vendor stack wants AMD's
|
||||||
|
XRT 2.25.37 + `xrt_plugin…amdxdna` deb **[host: distro `libxrt-npu2
|
||||||
|
1:2.21.75+dfsg-4` installed]**, and firmware must match the loaded `amdxdna.ko`
|
||||||
|
— the vendor README warns that a stale/mismatched `npu.sbin` causes
|
||||||
|
`ERT_CMD_ABORT`/mailbox timeouts, and that a distro **in-tree** `amdxdna` older
|
||||||
|
than the XRT SHIM makes `GET_ARRAY`/`QUERY_TELEMETRY` return
|
||||||
|
`EOPNOTSUPP`/`EINVAL` (<https://github.com/amd/xdna-driver> README,
|
||||||
|
fetched 2026-10-19). Installing AMD's debs over the distro XRT changes the ABI
|
||||||
|
under FastFlowLM/halogen, so the spike must decide the XRT version **once**, for
|
||||||
|
the whole host.
|
||||||
|
* **iGPU / fabric clock:** no vendor document supports a fabric-clock pin, and no
|
||||||
|
vendor document addresses NPU+iGPU concurrency hangs. AMD's Strix Halo
|
||||||
|
optimization doc (ROCm docs, dated 2026-02-20, and its successor "AMD RDNA3.5
|
||||||
|
system optimization", ROCm 10.1.0) covers GPUVM/GART/GTT, the TTM `pages_limit`
|
||||||
|
knob, `amd-ttm`, BIOS VRAM sizing, and minimum kernel versions
|
||||||
|
(<https://rocm.docs.amd.com/en/docs-7.2.0/how-to/system-optimization/strixhalo.html>,
|
||||||
|
<https://rocm.docs.amd.com/en/latest/reference/system-optimization/rdna3-5.html>).
|
||||||
|
The `pp_dpm_fclk` level-7 pin and the GPU+NPU hang observation stay
|
||||||
|
fleet-observed **[host]**, i.e. `[INFERENCE]` territory — no citation exists.
|
||||||
|
|
||||||
|
## 7. Paths I checked and ruled out (so nobody re-walks them)
|
||||||
|
|
||||||
|
* **Windows-only doors**: DirectML; Ryzen AI Windows EP; Lemonade's
|
||||||
|
`ryzenai-llm` and `whispercpp`-NPU engines (matrix above); AMD's OGA model
|
||||||
|
generation. Not usable on Dankclaw.
|
||||||
|
* **Mainline ONNX Runtime** has no AMD-NPU EP: its EP summary lists AMD ROCm and
|
||||||
|
AMD MIGraphX (GPU) and "Xilinx Vitis-AI (*preview*)" only
|
||||||
|
(<https://onnxruntime.ai/docs/execution-providers/>, 2026-10-19); the working
|
||||||
|
EP is AMD's own build (`libonnxruntime_providers_ryzenai.so` in the tarball's
|
||||||
|
deployment dir, per llm_linux).
|
||||||
|
* **ggml / llama.cpp / qwentts.cpp**: no XDNA backend → the current engine
|
||||||
|
(`tts-server` on GGUF, CPU-only build **[host]**) cannot be "turned on" for the
|
||||||
|
NPU; migration means a different runtime, not a flag.
|
||||||
|
* **`flm`**: catalog/engines are closed per-architecture (§2) — the cheapest door
|
||||||
|
by install cost, and it is not a door for TTS.
|
||||||
|
* **IRON / mlir-aie** is the only open toolchain that documents running on the
|
||||||
|
Ryzen AI NPU from Linux ("A close-to-metal Python API for programming AMD Ryzen™
|
||||||
|
AI NPUs, built on an open-source MLIR-based compiler toolchain"; "Getting
|
||||||
|
Started for AMD Ryzen™ AI on Linux", Ubuntu 24.04/24.10, XRT from
|
||||||
|
`ppa:amd-team/xrt`: `libxrt2 libxrt-npu2 … amdxdna-dkms`, `render` group
|
||||||
|
required) — <https://raw.githubusercontent.com/Xilinx/mlir-aie/main/README.md>,
|
||||||
|
fetched 2026-10-19. This is how FLM's kernels were made ("Low-level kernels
|
||||||
|
optimized using IRON + AIE-MLIR", FLM README) — i.e. real, but it is
|
||||||
|
hand-written per-op tile programming, not a model compiler: writing a TTS model
|
||||||
|
here is a multi-month engineering project, not a migration.
|
||||||
|
* **Embedded** Ryzen AI P100 support in the same Linux doc is irrelevant to STX.
|
||||||
|
|
||||||
|
## 8. Recommendation implied by the above (for Main's spike)
|
||||||
|
|
||||||
|
Spike the AMD path as decided, with these constraints planned in: Python 3.12 venv
|
||||||
|
+ 24.04-built wheels on Ubuntu 26.04; one host-wide XRT decision (AMD 2.25.37 vs
|
||||||
|
distro 2.21.75) with firmware/driver pairing checked via `xrt-smi examine` →
|
||||||
|
"NPU Strix Halo"; 7.8 GiB tarball staged off the public CDN URL (§3); pre-encode
|
||||||
|
each `reference.wav` with the **PyTorch** NeuCodec encoder because the ONNX codec
|
||||||
|
is decoder-only; keep phonemizer + codec on CPU; measure decode tokens/s against
|
||||||
|
the 50 Hz budget and peak RSS against the ~14 GiB headroom; expect to write the
|
||||||
|
serving shell yourself (AMD ships `model_benchmark`, no TTS demo).
|
||||||
|
|
||||||
|
## 9. Loose ends (not verified; I stopped on Main's stand-down)
|
||||||
|
|
||||||
|
* Contents of `ryzen_ai-1.8.0.tgz` (7.8 GiB) — whether the Linux tarball contains
|
||||||
|
the AIE-ML compiler (`vaiml`, `aiecompiler_client`) for the *BF16/INT8* flows
|
||||||
|
and which Python wheels it carries. My statement that non-LLM compilation works
|
||||||
|
on Linux rests on AMD's own Linux doc ("quicktest.py … compiles it and runs on
|
||||||
|
… NPU"), not on the package contents. Download was killed mid-flight.
|
||||||
|
* Whether AMD's `neutts` artifacts run correctly under the Linux `model_benchmark`
|
||||||
|
runner (the TTS rows are listed without OS qualifiers; no Linux TTS run is
|
||||||
|
documented anywhere in 1.8.0 docs).
|
||||||
|
* Exact AIE column count/total `ncols` exposed for Strix Halo (driver reads it
|
||||||
|
from AIE metadata: `ndev->total_col = min(XRS_MAX_COL, ndev->aie.metadata.cols)`).
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
# Qwen3-TTS payload + qwentts.cpp runtime: architecture and portability
|
||||||
|
|
||||||
|
Research deliverable (no code). Slice: *what the fleet's TTS payload actually is, and how
|
||||||
|
portable it is off the CPU ggml path.* Companion slice: `NpuRuntimeLandscape` (what NPU
|
||||||
|
runtimes exist on Dankclaw).
|
||||||
|
|
||||||
|
Evidence date: 2026-10-19. Everything below is grounded in:
|
||||||
|
|
||||||
|
- `github.com/ServeurpersoCom/qwentts.cpp` @ `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4`
|
||||||
|
(2026-10-16, upstream HEAD on 2026-10-19) and its ggml submodule
|
||||||
|
`ServeurpersoCom/ggml` @ `cc5df49a6b878db2b68b9a7b45976e279594627d` (2026-10-16).
|
||||||
|
- The two GGUF files as they sit on Dankclaw (`/home/dank/models/Qwen3-TTS-GGUF/`), read
|
||||||
|
byte-for-byte from the live host.
|
||||||
|
- Upstream Qwen3-TTS model cards / release material.
|
||||||
|
|
||||||
|
Status: COMPLETE for the assigned slice. ANSWER C was confirmed on the live host the same
|
||||||
|
night — the running `frank-tts-engine` container carries `libggml-base.so.0.27.0` +
|
||||||
|
`libggml-cpu.so.0.27.0` and nothing else, and our own
|
||||||
|
`dankclaw/voiceservice/Containerfile.engine` writes `-DGGML_VULKAN=OFF` out explicitly, so
|
||||||
|
the iGPU change is a flag in a file the fleet owns. Verdict, measured contract and the NPU
|
||||||
|
exoneration chain: `tts-npu-spike-verdict-2026-10-19.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the payload is
|
||||||
|
|
||||||
|
Two files, both read byte-for-byte off the live host (`/home/dank/models/Qwen3-TTS-GGUF/`,
|
||||||
|
owned `dank:dank`, mtimes 2026-10-18 18:31), both from
|
||||||
|
`https://huggingface.co/Serveurperso/Qwen3-TTS-GGUF` (community converter, not Qwen):
|
||||||
|
|
||||||
|
| file | bytes | GGUF | tensors | arch string | file_type |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `qwen-talker-1.7b-base-Q4_K_M.gguf` | 1,219,245,248 | v3, kv=64 | 480 | `qwen3-tts` | `Q4_K_M` |
|
||||||
|
| `qwen-tokenizer-12hz-Q8_0.gguf` | 291,150,624 | v3, kv=51 | 398 | `qwen3-tts-tokenizer` | `Q8_0` |
|
||||||
|
|
||||||
|
`general.name` of the talker file is `Qwen3-TTS-12Hz-1.7B-base`; the header sizes add up
|
||||||
|
to the file sizes (sum of tensor bytes + header + alignment padding), so nothing in these
|
||||||
|
files is unaccounted for.
|
||||||
|
|
||||||
|
### 1.1 Talker file (480 tensors)
|
||||||
|
|
||||||
|
Three models in one container, per `docs/ARCHITECTURE.md` §GGUF layout
|
||||||
|
(qwentts.cpp @ pinned commit) and confirmed by the tensor census:
|
||||||
|
|
||||||
|
| prefix | tensors | bytes | what it is |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `talker.*` | 316 | 1,061.9 MB | 28-layer Qwen3 decoder (hidden 2048, FFN 6144, 16 Q heads / 8 KV heads, head_dim 128, RoPE θ=1e6, RMS ε=1e-6, ctx 32768), text stream + codec stream summed, `codec_head` → 3072 codebook-0 logits |
|
||||||
|
| `code_pred.*` | 88 | 114.8 MB | 5-layer Qwen3 MTP head (hidden 1024, FFN 3072, 15 acoustic codebooks × 2048), own KV cache |
|
||||||
|
| `spk_enc.*` | 76 | 36.6 MB | ECAPA-TDNN speaker encoder (x-vector, base checkpoints only; mel → conv0 → 3 SE-Res2Net → MFA → ASP → 2048-d vector) |
|
||||||
|
|
||||||
|
Dtype split (exact tensor byte sums): `q4_K` 250 tensors / 864.1 MB, `q6_K` 17 / 312.0 MB,
|
||||||
|
`f32` 176 / 25.8 MB, `f16` 37 / 11.4 MB.
|
||||||
|
|
||||||
|
Which tensors carry which quant is the load-bearing portability fact:
|
||||||
|
|
||||||
|
- `q6_K` (17): `talker.text_embd.weight [2048, 151936]` (255.3 MB), `talker.codec_embd.weight
|
||||||
|
[2048, 3072]`, and the 15 `code_pred.codec_embd.<g>.weight [2048, 2048]` embedding tables.
|
||||||
|
These are the llama.cpp `Q4_K_M` "embedding/output rows go to Q6_K" rule, applied to
|
||||||
|
embedding tables rather than to an `lm_head`.
|
||||||
|
- `q4_K` (250): every attention/FFN projection of the talker and of the code predictor.
|
||||||
|
Distinct matmul shapes: `[2048,2048]`×73, `[2048,1024]`×62, `[2048,6144]`×56,
|
||||||
|
`[6144,2048]`×28, `[1024,2048]`×20, `[1024,3072]`×10, `[1024,1024]`×10, `[3072,1024]`×5,
|
||||||
|
`[2048,3072]`×2, `[2048,151936]`×1. Every one is 2-D and ≥ 1024 on both axes — good
|
||||||
|
GEMM shapes, nothing exotic.
|
||||||
|
- `f32` (176) / `f16` (37): all of `spk_enc` (conv weights F16 because K=1/3/5 rows are not
|
||||||
|
block-alignable, per the converter's `should_quantize` policy in `tools/quantize.cpp` +
|
||||||
|
`quantize.sh`; every bias/norm/LayerScale vector F32), plus the talker/predictor norms and
|
||||||
|
the `text_proj` biases.
|
||||||
|
|
||||||
|
Metadata the runtime actually reads (talker file): `qwen3-tts.num_code_groups=16`,
|
||||||
|
`talker.vocab_size=3072`, `text_vocab_size=151936`, `text_hidden_size=2048`,
|
||||||
|
`position_id_per_seconds=13`, `rope.mrope_section=[24,20,20]`, `mrope_interleaved=1`,
|
||||||
|
the 7 codec special ids (pad 2148 / bos 2149 / eos 2150 / think 2154 / nothink 2155 /
|
||||||
|
think_bos 2156 / think_eos 2157), `language_names` (10: chinese, english, german, italian,
|
||||||
|
portuguese, spanish, japanese, korean, french, russian) with their codec ids, the text-side
|
||||||
|
special ids (im_start 151644, im_end 151645, tts_pad 151671, tts_bos 151672, tts_eos 151673),
|
||||||
|
the whole `generation_config.json` (do_sample, top_k 50, top_p 1.0, temperature 0.9,
|
||||||
|
repetition_penalty 1.05, `subtalker_*` mirrors, max_new_tokens 8192), and an embedded
|
||||||
|
**Qwen2 BPE tokenizer** (`tokenizer.ggml.model=gpt2`, 151,676 tokens, 151,291 merges,
|
||||||
|
eos 151643) — that BPE block is 5.95 MB of the header, i.e. 49 % of the header is vocabulary.
|
||||||
|
|
||||||
|
Two live-file details that contradict the upstream doc text (worth knowing when reading
|
||||||
|
`docs/ARCHITECTURE.md`: the doc example block is stale against the shipped file):
|
||||||
|
`mrope_interleaved` is **1** (doc shows `false`), and `spk_enc.sample_rate` is **24000**
|
||||||
|
(doc says 16000). Doc also says "404 tensors on the 1.7B voice_design build" — the base
|
||||||
|
build shipped here has 480 because it carries `spk_enc.*`.
|
||||||
|
|
||||||
|
### 1.2 Tokenizer/codec file (398 tensors)
|
||||||
|
|
||||||
|
One neural audio codec, 12.5 frames/s over 24 kHz mono, hop 1920, 16 codebooks × 2048
|
||||||
|
entries (1 semantic + 15 acoustic). Dtype split: `f16` 50 / 143.6 MB, `q8_0` 110 / 76.9 MB,
|
||||||
|
`f32` 238 / 70.7 MB. `tok_dec.*` 253 tensors / 201.3 MB, `tok_enc.*` 145 / 89.9 MB.
|
||||||
|
|
||||||
|
Composition (metadata + tensor census, both files agree):
|
||||||
|
|
||||||
|
- **Encoder** (used only to make voice references, not on the synthesis hot path): SEANet
|
||||||
|
conv stack (init k=7, 4 stages with ratios 8/6/5/4 → cumulative 960×, 64→512 ch, final
|
||||||
|
k=3), an 8-layer transformer (hidden 512, 8 heads, head_dim 64, FFN 2048, RoPE 1e4,
|
||||||
|
LayerNorm+bias, GELU, LayerScale), a downsample conv k=4 s=2 (25 → 12.5 Hz), then the RVQ
|
||||||
|
argmin loop. Metadata records `encoder.num_quantizers=32` with
|
||||||
|
`encoder_valid_num_quantizers=16` — 16 codebooks are used, 16 are dead weight carried from
|
||||||
|
the upstream config.
|
||||||
|
- **Decoder** (the hot path: every generated frame): 16 codebook lookups + per-split
|
||||||
|
`output_proj` (256→512), `conv_pre`, an 8-layer transformer (hidden 512, 16/16 heads,
|
||||||
|
head_dim 64, FFN 1024, **sliding window 72**, RMSNorm, SwiGLU, LayerScale, input_proj
|
||||||
|
1024→512 / output_proj 512→1024), a ConvNeXt upampler (`upsampling_ratios=[2,2]`, causal
|
||||||
|
transposed convs + depthwise k=7), then a DAC-style decoder `dec.0..3`
|
||||||
|
(`upsample_rates=[8,5,4,3]`, channels 1536→768→384→192→96, `decoder_dim=1536`) whose blocks
|
||||||
|
are SnakeBeta → causal transposed conv (k = 2·stride) → 3 residual units (dilations 1/3/9)
|
||||||
|
→ snake_post → conv_post k=7 → 96→1.
|
||||||
|
- All 32 codebook tensors are **F32, 67.1 MB total** (16 encode + 16 decode, `[256, 2048]`),
|
||||||
|
deliberately unquantized: the converter's `should_quantize` keeps RVQ codebooks and their
|
||||||
|
wrapping projections at F32 because nearest-neighbour lookup is sensitive to per-row
|
||||||
|
quantisation noise (`docs/ARCHITECTURE.md` "Quantisation policy"; mirrored in
|
||||||
|
`quantize.sh`). 49 of the codec's 55 3-D conv tensors are **F16** with non-alignable K
|
||||||
|
(7, 3, 2, 1, 16, 10, …) — they cannot be block-quantized at all.
|
||||||
|
- **No flow matching, no diffusion, no vocoder-with-ODE.** It is a pure RVQ codec
|
||||||
|
(Mimi/SoundStream/Mimi-style: SEANet + transformer + DAC decoder). That matters for the
|
||||||
|
NPU question: every component is convolution, attention, or embedding lookup.
|
||||||
|
|
||||||
|
### 1.3 Where the payload came from, and what the official weights look like
|
||||||
|
|
||||||
|
Official upstream (`huggingface.co/api/models`, retrieved 2026-10-19):
|
||||||
|
|
||||||
|
| repo | last modified | license | weights |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Qwen/Qwen3-TTS-12Hz-1.7B-Base` | 2026-01-23 | apache-2.0 | `model.safetensors` BF16, 1,928,677,440 params, plus an embedded `speech_tokenizer/model.safetensors` |
|
||||||
|
| `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | 2026-01-29 | apache-2.0 | same layout, named-speaker tables |
|
||||||
|
| `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` | 2026-01-29 | apache-2.0 | 1.7B only |
|
||||||
|
| `Qwen/Qwen3-TTS-12Hz-0.6B-Base` / `-0.6B-CustomVoice` | 2026-01-29 | apache-2.0 | narrow variant (hidden 1024) |
|
||||||
|
| `Qwen/Qwen3-TTS-Tokenizer-12Hz` | 2026-01-29 | apache-2.0 | codec alone, F32, 170,557,441 params |
|
||||||
|
|
||||||
|
So the GGUF is **not** the only public form: the safetensors are the canonical form
|
||||||
|
(Apache-2.0, no separate community license risk), and the GGUF is a lossy re-quantization of
|
||||||
|
them produced by the same author who wrote the runtime (`Serveurperso`), via
|
||||||
|
`checkpoints.sh` → `convert.py` (F32 GGUF) → `quantize.sh` (BF16/Q8_0/Q4_K_M).
|
||||||
|
Sizes to remember for any re-export: the talker container is 1.93 B params in BF16
|
||||||
|
(that count includes the embedded codec), the codec alone is 170.6 M params in F32.
|
||||||
|
|
||||||
|
**Portability consequence #1 — the GGUF is a dead end off-ggml.** Q4_K/Q6_K are ggml
|
||||||
|
block formats (256-element super-blocks, F16 sub-block scales, 144 B / 210 B per block). No
|
||||||
|
ONNX graph, no Vitis-AI/AIE-ML compiler, and no vendor NPU runtime consumes them; AMD's own
|
||||||
|
quantizer works from ONNX (`ryzenai-quantize`, QDQ/QOP power-of-two — Ryzen AI Software
|
||||||
|
1.8.0 release notes, https://ryzenai.docs.amd.com/en/latest/relnotes.html). Moving the
|
||||||
|
payload to a non-ggml stack therefore means starting from the safetensors again, not
|
||||||
|
converting the GGUF.
|
||||||
|
|
||||||
|
**Portability consequence #2 — the quantization split is semantic, not cosmetic.** The
|
||||||
|
converter keeps RVQ codebooks, `input_proj`/`output_proj` around them, and every 1-D and
|
||||||
|
non-alignable-conv tensor out of the k-quants precisely because quantizing them breaks voice
|
||||||
|
fidelity. Any re-quantization path (GPTQ/AWQ/QDQ-INT8/INT4 for an NPU compiler) inherits that
|
||||||
|
constraint: the talker backbone is requantizable, the codec's codebook lookups and SnakeBeta
|
||||||
|
convs are not, at least not without re-measuring voice quality.
|
||||||
|
|
||||||
|
## 2. What the runtime is
|
||||||
|
|
||||||
|
`qwentts.cpp` @ `5916bf7c` (2026-10-16, MIT; upstream model Apache-2.0) is a C++17 ggml
|
||||||
|
application — 43 files under `src/` + `tools/`, three binaries (`qwen-tts`, `qwen-codec`,
|
||||||
|
`tts-server`) over one public plain-C99 ABI (`src/qwen.h`, `qt_*`, `QT_ABI_VERSION`
|
||||||
|
range-checked; `tests/abi-c.c` is compiled `-std=c99 -Wall -Werror -pedantic` on every
|
||||||
|
build). It is not a llama.cpp derivative: it links ggml directly and builds its own graphs.
|
||||||
|
|
||||||
|
Five graph families, and their shapes are the whole portability story:
|
||||||
|
|
||||||
|
| graph | batch/time axis | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| talker prefill | T = prompt length (hundreds–thousands) | summed text+codec embedding streams, 28-layer GQA decoder, KV cache |
|
||||||
|
| talker decode | **T = 1 per audio frame** | 12.5 frames/s → 80 ms of audio per step; `codec_head` → 3072 logits |
|
||||||
|
| code predictor (MTP) | **T = 1, 16 steps per frame** (2 prefill + 14 decode, cache reset every frame) | 5-layer decoder, 15 heads × 2048 |
|
||||||
|
| speaker encoder | one pass per voice registration | ECAPA-TDNN with mel fused into the graph |
|
||||||
|
| codec decode | offline: T = 300-frame chunks; streaming: **T = 1 frame** with persistent conv/KV-ring state | the streaming graph is built once with fixed topology so the CUDA graph cache stays in pure replay |
|
||||||
|
|
||||||
|
Per generated frame the host therefore runs ~1 + 16 + 1 tiny graph computes for 80 ms of
|
||||||
|
audio, and `tts-server` serializes synthesis FIFO (`--max-batch` defaults to 1, and the
|
||||||
|
fleet runs it at 1). This is the archetypal latency-bound, bandwidth-bound,
|
||||||
|
batch-1 workload; the only big-M matmuls in the whole system are the prefill and the
|
||||||
|
chunked (buffered) codec decode.
|
||||||
|
|
||||||
|
Op contract actually issued to ggml (`src/`, counted): `mul_mat` 77, `cont` 70, `add` 57,
|
||||||
|
`mul` 40, `rms_norm` 22, `clamp` 15, `rope_ext` 12, `get_rows` 12, `set_rows` 8,
|
||||||
|
`scale_bias` 6, `swiglu_split` 5, `sqr` 5, `mean` 5, `elu` 4, `col2im_1d` 4, `soft_max` 3,
|
||||||
|
`conv_1d` 3, `im2col` 2, `cumsum` 2, `sin`, `sigmoid`, `tanh`, `div`, `argsort`, `transpose`,
|
||||||
|
`permute`, `reshape/view`, plus the CPU-side Philox multinomial sampler.
|
||||||
|
|
||||||
|
**No custom ggml op is required today.** The repo doc still says the fork "provides the two
|
||||||
|
custom ops the codec needs: `GGML_OP_SNAKE` and `GGML_OP_COL2IM_1D`"; that is stale at the
|
||||||
|
pinned commit. Verified 2026-10-19:
|
||||||
|
|
||||||
|
- the submodule `ServeurpersoCom/ggml` @ `cc5df49` is upstream `ggml-org/ggml` master
|
||||||
|
(`a05c992`, 2026-10-15) **+2 commits, 0 behind** (GitHub compare API), and
|
||||||
|
`include/ggml.h` is byte-identical to upstream master (`diff` empty);
|
||||||
|
- the 2 commits are `193e339` (2026-06-19) "cpu: add fused snake activation
|
||||||
|
(mul -> sin -> sqr -> mul -> add)" — a **CPU subgraph fusion**, not an op (the runtime
|
||||||
|
builds snake with `ggml_sin`/`ggml_sqr`/`ggml_mul`/`ggml_add` in `src/dac-decoder-v2.h:222`),
|
||||||
|
and `cc5df49` "sycl: fix get_rows on reordered quantized weights";
|
||||||
|
- `ggml_col2im_1d` is an **upstream ggml API** (same declaration, line 2097, in both trees);
|
||||||
|
the Vulkan backend fuses the snake chain through ggml's generic subgraph fuser
|
||||||
|
(`ggml-vulkan.cpp:14548 ggml_vk_can_fuse_snake`, `vulkan-shaders/snake.comp`).
|
||||||
|
|
||||||
|
Consequence: any stock ggml build (upstream, or llama.cpp's vendored ggml) can execute this
|
||||||
|
payload; only the CPU snake fusion is fork-specific, and it is a performance nicety.
|
||||||
|
|
||||||
|
Backend/device support: every tool takes `--device <name>` (`CUDA0`, `Vulkan0`, `MTL0`,
|
||||||
|
`CPU…`, default best available), resolved through the ggml backend registry; the deployed
|
||||||
|
container turns CUDA/HIP/Vulkan/Metal/SYCL/RPC/BLAS all OFF and compiles the native-CPU
|
||||||
|
backend with OpenMP, with CPU threads = `hardware_concurrency()/2` (`src/backend.h`).
|
||||||
|
The registry in this ggml knows CPU, CUDA, Metal, SYCL, Vulkan, WebGPU, ZDNN, OpenCL,
|
||||||
|
**Hexagon**, BLAS, RPC, VirtGPU, **CANN**, ZenDNN, **OpenVINO**, ET — and nothing AMD-NPU
|
||||||
|
(no `xdna`/`aie`/`vitis`/`ryzen` string anywhere in the ggml tree).
|
||||||
|
|
||||||
|
Voice handling (what the fleet depends on): the registry is **process RAM only**; a restart
|
||||||
|
empties it, which is why the unit has a boot warmer. `POST /v1/audio/voices` accepts either
|
||||||
|
`{name, ref_text, wav_b64}` (server extracts) or `{name, ref_text, spk_b64, rvq_b64}`
|
||||||
|
(pre-extracted latents verbatim); `ref_text` present ⇒ ICL clone mode, absent ⇒ x-vector-only
|
||||||
|
mode. `.spk` = the ECAPA x-vector; `.rvq` = 16 codes/frame packed 11 bits LSB-first
|
||||||
|
(`src/rvq-file.h`). `wav` and `pcm` `response_format` are different audio at the same seed
|
||||||
|
(buffered chunked decode vs stateful streaming decode) — the fleet already encodes that rule.
|
||||||
|
|
||||||
|
Parity expectations for any port: the upstream harness matches the PyTorch reference per
|
||||||
|
stage (embed cos 0.999988, all 28 hidden taps ≥ 0.999893, logits 0.999824) but explicitly
|
||||||
|
reports free-run divergence (`CodesFull exact 4.96 %`, `Audio cos 0.095`) and explains why —
|
||||||
|
one argmax tie at FP epsilon diverts the AR trajectory. So a migrated endpoint must be
|
||||||
|
accepted by listening + per-stage cosine, never by waveform diff against today's audio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The three answers Main asked for
|
||||||
|
|
||||||
|
### ANSWER A — Qwen3-TTS GGUF → ONNX: no, and it does not matter
|
||||||
|
|
||||||
|
There is **no GGUF→ONNX path** and there will not be a useful one: GGUF is a weight
|
||||||
|
container for ggml, its Q4_K/Q6_K block layouts have no ONNX consumer, and nothing in the
|
||||||
|
GGUF carries the *graph* (the talker/predictor/codec structure lives in C++ code, only the
|
||||||
|
op-level structure exists in ggml at runtime). But the GGUF is not the payload's only form —
|
||||||
|
official Apache-2.0 safetensors are upstream, and **four independent ports of the full
|
||||||
|
pipeline already exist**:
|
||||||
|
|
||||||
|
| port | form | what it proves |
|
||||||
|
|---|---|---|
|
||||||
|
| `xkos/Qwen3-TTS-12Hz-1.7B-ONNX` (HF, updated 2026-03-03, apache-2.0) | FP16+FP32 ONNX, fp16/voice_clone + fp16/voice_design splits: `talker_decode.onnx`, `code_predictor.onnx`, `code_predictor_kv.onnx`, `code_predictor_embed_g0..15.onnx`, `text_embedding.onnx`, `codec_embedding.onnx`, shared `speaker_encoder.onnx`, `speech_tokenizer_encoder.onnx`, `speech_tokenizer_decoder.onnx`, `model_cache.npz` | every component of this architecture exports to ONNX, codec decoder included |
|
||||||
|
| `zhangluoyang/qwen3-tts-onnxruntime` | export tooling + Python inference for Base/CustomVoice/VoiceDesign | the export is reproducible from the official checkpoints |
|
||||||
|
| `Echo9Zulu/Qwen3-TTS-12Hz-CustomVoice-1.7B-INT8-OpenVINO`, `aurora2035/Qwen3-TTS-12Hz-0.6B-Base-OpenVINO-INT8` (2026-03) | OpenVINO IR, **INT8**: `talker`, `code_predictor`, `codec_embedding`, `text_model`, `openvino_speaker_encoder_model`, `speech_tokenizer/speech_decoder` | the talker and predictor INT8-quantize for an NPU-targeting stack |
|
||||||
|
| `litert-community/Qwen3-TTS-12Hz-0.6B-Base` (2026-10-17, apache-2.0) | `qwen3_tts_int8.litertlm` / `_int4.litertlm` (LiteRT-LM `OmniEngine`), legacy split graphs `talker_int4.tflite` (prefill_32/prefill_128/decode signatures, KV 1024, blockwise-32 OCTAV INT4), `mtp_folded_int8.tflite` (16 inner steps × 5 layers folded into one graph, in-graph argmax+gather, GPTQ int8, ~5× faster), `codec_partA` (transformer, fp32) + `codec_partB` (conv upsampler, XNNPACK FORCE_FP16, ~2.5×, "ASR-identical") | an NPU/mobile compiler accepts the whole thing — at the price of graph splitting, static prefill buckets, and folding the per-frame inner loop |
|
||||||
|
|
||||||
|
What the ports had to concede, and what any port inherits:
|
||||||
|
|
||||||
|
- the generation structure does not fit existing engine loops: LiteRT-LM's Engine "does not
|
||||||
|
support this generation structure yet", so their first shape was **three graphs + a
|
||||||
|
host-side loop**, and the MTP step was "invoked 17× per frame" until they folded it;
|
||||||
|
- per-step dispatch cost dominates: even on a Snapdragon 8 Elite Gen 5 they keep the MTP
|
||||||
|
loop, text projection, embedding gathers and the codec on CPU "avoiding 16 synchronous
|
||||||
|
CPU↔GPU round-trips per 80 ms frame", putting only the 28-layer talker on the GPU;
|
||||||
|
- RTF on that hardware is **2.57–2.97 (Galaxy S26) and 5.65–8.48 (Pixel 10 Pro)** for the
|
||||||
|
0.6 B model, i.e. 2.6–8× slower than real time. Dankclaw's current CPU engine measures
|
||||||
|
**warm RTF 0.55–0.67 with TTFA 94–112 ms** on the 1.7 B model (fleet trial, 2026-10-18).
|
||||||
|
An NPU port is not "faster by default" for this workload; it has to beat that.
|
||||||
|
- quantize the LM (int8/int4: proven), keep the codec's codebook lookups and SnakeBeta convs
|
||||||
|
in fp16/fp32 (the LiteRT split does exactly this).
|
||||||
|
|
||||||
|
So: the answer to "can the fleet's payload be exported" is *yes — from the safetensors, not
|
||||||
|
from the GGUF*, and the work is a pipeline rewrite (multi-graph host loop, per-frame fold,
|
||||||
|
static prefill buckets), not a conversion. That is the same conclusion NeuTTS-Air's
|
||||||
|
precompiled-graph route encodes: on AMD's Linux stack the graph is vendor-curated, so the
|
||||||
|
fleet chooses the model that AMD already compiled rather than compiling Qwen3-TTS.
|
||||||
|
|
||||||
|
### ANSWER B — is there any ggml backend for an AMD NPU? Not upstream; three out-of-tree, none for TTS
|
||||||
|
|
||||||
|
Checked against the trees today (2026-10-19), because this was the crux:
|
||||||
|
|
||||||
|
- **Nothing in-tree.** `ggml-org/llama.cpp` master (4,237 paths, GitHub tree API) contains no
|
||||||
|
`xdna`/`aie`/`vitis`/`ryzen` file or directory; the ggml backend set is CPU, BLAS, CUDA,
|
||||||
|
HIP, Metal, Vulkan, SYCL, OpenCL, WebGPU, MUSA, RPC, CANN (Ascend), **Hexagon** (Qualcomm),
|
||||||
|
OpenVINO (Intel CPU/GPU/NPU), ZenDNN, ZDNN, VirtGPU, ET. The deployed fork's registry
|
||||||
|
matches. The feature request "Feature Request: XDNA backend" (llama.cpp issue **#21725**,
|
||||||
|
opened 2026-04-10) is still **open**, 34 comments; the older #1499 was closed stale
|
||||||
|
2025-04-07. So Main's premise is confirmed: no ggml XDNA backend exists to flip a flag on.
|
||||||
|
- **`Cyronius/ggml-xdna`** — a real ggml backend (`XDNA0`) loaded into *stock* llama.cpp via
|
||||||
|
`GGML_BACKEND_PATH`, no fork. It does bfp16 (8-value blocks sharing an exponent) GEMMs from
|
||||||
|
GGUF weights, and is **prefill-only**: it takes only matmuls in prompt chunks ≥ 1024 tokens
|
||||||
|
(`GGML_XDNA_MIN_BATCH` default 1024, `GGML_XDNA_MIN_MFLOP` 256), attention and every decode
|
||||||
|
token stay on the Vulkan device; pinned to llama.cpp release b10944; tested on Strix Point
|
||||||
|
(HX 370) only — Strix Halo "should pass, but nobody has run it on one yet"; Linux support
|
||||||
|
new and untested on hardware by the author; needs `libxrt-npu2`, `/dev/accel/accel0`,
|
||||||
|
unlimited `memlock`. Best measured prefill gain 1.88× on a 27 B model; models under
|
||||||
|
2,048-wide **lose** (0.77× at width 1,536).
|
||||||
|
- **`BrandedTamarasu-glitch/OllamaAMDNPU`** — a llama.cpp fork with `-DGGML_XDNA=ON`,
|
||||||
|
explicitly targeting **Ryzen AI MAX / XDNA2 on Linux** (`/dev/accel/accel0`, XRT 2.21.75+,
|
||||||
|
amdxdna, unlimited memlock). It implements **only `GGML_OP_MUL_MAT`**, dispatched to
|
||||||
|
mlir-aie-compiled `.xclbin`s built per fixed tile shape (e.g. `M=2048 K=2048 N=64`,
|
||||||
|
int8 in / int32 out, per-row weight scales cached at first use), with a second slot for a
|
||||||
|
different K. Its own numbers on a Ryzen AI MAX 385 with Llama-3.1-8B Q4_K_M: NPU prefill
|
||||||
|
12–19 t/s vs **Vulkan iGPU 600–930 t/s**, NPU decode **0.65–1.40 t/s** ("Decode does not
|
||||||
|
use the NPU … M=1 per token, no xclbin covers it"), 0.6 % AIE utilization, and the stated
|
||||||
|
conclusion "**For LLaMA 8B on Strix Halo: use Vulkan exclusively**". Single-maintainer,
|
||||||
|
AI-assisted, self-measured — treat as a proof of concept, but it is the closest published
|
||||||
|
picture of what XDNA2 graph execution costs.
|
||||||
|
- Also referenced in the same thread: `Atomic-Germ/OpenFlowLM-Next` (whole-model NPU engine,
|
||||||
|
LLM-only — and slower than the iGPU in the same comparison), `GaTmaNnes/xdna2-`,
|
||||||
|
`hardWorker254/Npu-Embeddings-XDNA1` (adds Whisper ASR ops, NPU1/Linux). None is a TTS path.
|
||||||
|
|
||||||
|
Why none of them would carry this workload even if installed: the TTS hot loop is **batch-1
|
||||||
|
by construction** (one talker step + 16 predictor steps + one codec frame per 80 ms), which
|
||||||
|
sits under every dispatch gate those backends define (≥1024 tokens / ≥32768 M·N·K / tile M
|
||||||
|
covering only prefill), and the codec half of the pipeline is conv, col2im, gather and
|
||||||
|
elementwise ops — not `MUL_MAT` — so it would fall back to CPU op-by-op. The NPU case for
|
||||||
|
this host is therefore *not* "run Qwen3-TTS on the NPU"; it is "run a vendor-compiled AR TTS
|
||||||
|
graph on the NPU", which is exactly the NeuTTS-Air spike.
|
||||||
|
|
||||||
|
### ANSWER C — would `qwentts.cpp` build for the gfx1151 iGPU? Yes, via **Vulkan**, not HIP
|
||||||
|
|
||||||
|
- Upstream ships the build scripts and claims the backends: `buildvulkan.sh` ("AMD/Intel GPU
|
||||||
|
(Vulkan)"), `buildcuda.sh`, `buildsycl.sh`, `buildcpu.sh`, `buildall.sh` (all backends,
|
||||||
|
runtime dynamic loading), and README line 6 "runs on CPU, CUDA, Metal, Vulkan". CMake knobs
|
||||||
|
`GGML_VULKAN` / `GGML_HIP` / `GGML_SYCL` are the same ones the fleet's Containerfile turns
|
||||||
|
OFF, so the engine rebuild is a flag change: `-DGGML_VULKAN=ON` plus
|
||||||
|
`--device Vulkan0` on the same command line the unit runs today.
|
||||||
|
- The codec's exotic ops are covered by the Vulkan backend, which is the thing that decides
|
||||||
|
whether a GPU build is coherent: `vulkan-shaders/col2im_1d.comp`, `snake.comp`,
|
||||||
|
`swiglu*.comp`, `cumsum*.comp` exist and `supports_op` handles `GGML_OP_COL2IM_1D` /
|
||||||
|
`CONV_TRANSPOSE_1D` / `CUMSUM`. By contrast `COL2IM_1D` appears in **no** HIP, OpenCL,
|
||||||
|
WebGPU, Hexagon or CANN source — a **HIP** (ROCm) build would shard the ConvNeXt/DAC
|
||||||
|
transposed convs back to the CPU per op. On an AMD iGPU the portable path is RADV/Vulkan.
|
||||||
|
- `--device` also accepts a multi-device split (`-dev`-style naming is ggml's), and the
|
||||||
|
runtime's documented FP16 guard (`--clamp-fp16`) exists for sub-Ampere CUDA accumulation;
|
||||||
|
nothing in the tree is NVIDIA-specific.
|
||||||
|
- Unverified by me: any vendor statement pairing this fork with gfx1151. Supporting evidence
|
||||||
|
that the hardware/driver combination is viable: this very host already runs a Vulkan
|
||||||
|
endpoint on the gfx1151 iGPU (fleet ground truth, 2026-10-19), and Mesa's Vulkan
|
||||||
|
implementation is officially supported on Radeon Software for Linux
|
||||||
|
(AMD release notes RN-AMDGPU-UNIFIED-LINUX-25.20.3). A gfx1151 Vulkan build of this fork is
|
||||||
|
a build-and-measure spike, not an unsupported port.
|
||||||
|
- The real constraint is the one already in the fleet docs: the iGPU belongs to another
|
||||||
|
endpoint, and GPU+NPU concurrency has hung Strix Halo machines. An iGPU TTS build competes
|
||||||
|
with the text endpoints for the same silicon; the NPU route's only genuine advantage is
|
||||||
|
that it does not (the XDNA backend authors make the same argument: "Use NPU when the iGPU
|
||||||
|
is busy … no GPU contention").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What any replacement must reproduce, and what the voices cost
|
||||||
|
|
||||||
|
Endpoint contract in production today (from the live unit/env, not the doc):
|
||||||
|
`tts-server --model qwen-talker-1.7b-base-Q4_K_M.gguf --codec qwen-tokenizer-12hz-Q8_0.gguf
|
||||||
|
--alias frank-tts-engine --host 0.0.0.0 --port 8880 --lang auto --max-batch 1`, no published
|
||||||
|
port (the façade on :7772 dials the private bridge name), `GET /health`, `GET /v1/models`,
|
||||||
|
`GET|POST /v1/audio/voices`, `DELETE /v1/audio/voices/{name}`, `POST /v1/audio/speech` with
|
||||||
|
`response_format` wav|pcm (different audio at equal seed) and optional `seed`,
|
||||||
|
`max_new_tokens`, `temperature`, `top_k`, `top_p`, `repetition_penalty`, `subtalker_*`.
|
||||||
|
Voice registry is RAM-only, so boot warming is mandatory, and `--alias` keeps the quantization
|
||||||
|
choice out of every consumer's config.
|
||||||
|
|
||||||
|
Live voice census on Dankclaw (2026-10-19, `/home/dank/voices/`): three packs
|
||||||
|
(`narrator1..3`), each carrying `reference.wav` (24 kHz mono crop), `reference16k.wav`,
|
||||||
|
`manifest.json` (keys: name, kind, generated_utc, pipeline, source, forbidden_region_guard,
|
||||||
|
silence_scan, crop, loudnorm, audio, acceptance, acceptance_passed) and ffmpeg analysis
|
||||||
|
artifacts. **No `.spk` and no `.rvq` exist on the live host** — the repo's
|
||||||
|
`build-voice-pack.sh` treats them as required-but-they-are-not-there, so today the engine
|
||||||
|
re-extracts the x-vector (and the ICL codes when a transcript is supplied) from the WAV at
|
||||||
|
registration time.
|
||||||
|
|
||||||
|
That is the portable part, and it is the same shape AMD's NeuTTS flow wants
|
||||||
|
(`encode_reference(reference.wav)` + a reference transcript): the fleet's voice identity is
|
||||||
|
*the WAV plus its transcript*, processed at request/registration time by the model's own
|
||||||
|
speaker encoder + codec — **never a trained per-voice artifact**. Qwen3-TTS cloning is
|
||||||
|
inference-time conditioning (`generate_voice_clone(ref_audio=…, ref_text=…, x_vector_only_mode=…)`;
|
||||||
|
`create_voice_clone_prompt` exists purely to avoid recomputing prompt features). So any
|
||||||
|
replacement that consumes a WAV (+transcript) keeps all three narrators alive with no
|
||||||
|
re-derivation, and if it wants precomputed latents instead, the LiteRT precedent shows what
|
||||||
|
that looks like: a 4 KB x-vector `.npy` per voice, extracted offline
|
||||||
|
(`extract_speaker_embedding.py`) — for the 1.7 B base checkpoint that vector is 2048-dim
|
||||||
|
(`qwen3-tts.spk_enc.embedding_length = 2048` in the shipped GGUF; the LiteRT 0.6 B bundle
|
||||||
|
ships a 1024-dim one).
|
||||||
|
|
||||||
|
One trap for a mixed-runtime future: `.spk`/`.rvq` files, if they are ever written, are
|
||||||
|
engine-specific encodings (ECAPA x-vector layout; 11-bit-packed RVQ stream) and are **not**
|
||||||
|
interchangeable between runtimes. Keep `reference.wav` as the pack's source of truth and
|
||||||
|
regenerate anything derived per engine — which is what the NeuTTS re-clone plan does anyway.
|
||||||
|
|
||||||
|
## 5. Sources
|
||||||
|
|
||||||
|
Runtime/payload (all read locally at pinned versions, 2026-10-19):
|
||||||
|
|
||||||
|
- `github.com/ServeurpersoCom/qwentts.cpp` @ `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4`
|
||||||
|
(commit date 2026-10-16; also upstream HEAD today): `README.md`, `docs/ARCHITECTURE.md`
|
||||||
|
(860 lines), `src/*`, `tools/*`, `docs/DOCKER.md`, `buildvulkan.sh`/`buildcpu.sh`/`buildall.sh`.
|
||||||
|
- submodule `ServeurpersoCom/ggml` @ `cc5df49a6b878db2b68b9a7b45976e279594627d` (2026-10-16);
|
||||||
|
delta vs `ggml-org/ggml` master `a05c992` (2026-10-15) = ahead 2 / behind 0, per
|
||||||
|
https://github.com/ServeurpersoCom/ggml/compare/ggml-org:master...HEAD ;
|
||||||
|
`include/ggml.h` identical to upstream
|
||||||
|
https://raw.githubusercontent.com/ggml-org/ggml/master/include/ggml.h .
|
||||||
|
- Live payload bytes: `/home/dank/models/Qwen3-TTS-GGUF/qwen-talker-1.7b-base-Q4_K_M.gguf`
|
||||||
|
(1,219,245,248 B) and `qwen-tokenizer-12hz-Q8_0.gguf` (291,150,624 B), headers parsed from
|
||||||
|
the host; sizes/sha256 match `scripts/download-frank-tts-engine-qwentts.sh`.
|
||||||
|
- Live units/env/packs: `dankclaw/voiceservice/{Containerfile.engine,tts-engine-start.sh,tts-engine-warm.sh,quadlet/*}`,
|
||||||
|
`scripts/frank-tts-engine.env`, `scripts/frank-tts.env`, `/home/dank/voices/*`.
|
||||||
|
|
||||||
|
Model provenance:
|
||||||
|
|
||||||
|
- `https://huggingface.co/api/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base` (+ CustomVoice,
|
||||||
|
VoiceDesign, 0.6B variants, `Qwen3-TTS-Tokenizer-12Hz`), apache-2.0, last-modified
|
||||||
|
2026-01-23 / 2026-01-29; safetensors param counts 1,928,677,440 (BF16, incl. embedded
|
||||||
|
`speech_tokenizer/`) and 170,557,441 (F32 codec).
|
||||||
|
- `https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-Base/raw/main/README.md` (clone API:
|
||||||
|
`generate_voice_clone(ref_audio, ref_text, x_vector_only_mode)`,
|
||||||
|
`create_voice_clone_prompt`; reference stack = PyTorch + `device_map="cuda:0"` + bf16 +
|
||||||
|
flash-attn-2, Python 3.12 recommended).
|
||||||
|
- `https://github.com/QwenLM/Qwen3-TTS`.
|
||||||
|
|
||||||
|
Portability precedents:
|
||||||
|
|
||||||
|
- `https://huggingface.co/xkos/Qwen3-TTS-12Hz-1.7B-ONNX` (2026-03-03) ·
|
||||||
|
`https://github.com/zhangluoyang/qwen3-tts-onnxruntime` ·
|
||||||
|
`https://huggingface.co/Echo9Zulu/Qwen3-TTS-12Hz-CustomVoice-1.7B-INT8-OpenVINO` (2026-03-22) ·
|
||||||
|
`https://huggingface.co/aurora2035/Qwen3-TTS-12Hz-0.6B-Base-OpenVINO-INT8` (2026-03-18) ·
|
||||||
|
`https://huggingface.co/litert-community/Qwen3-TTS-12Hz-0.6B-Base` (2026-10-17) incl.
|
||||||
|
`legacy/README.md` (graph splits, MTP fold, codec partA/partB, RTF table).
|
||||||
|
|
||||||
|
AMD NPU / ggml backend status:
|
||||||
|
|
||||||
|
- `https://api.github.com/repos/ggml-org/llama.cpp/git/trees/master?recursive=1` (no
|
||||||
|
XDNA/AIE/Vitis backend, 2026-10-19) ·
|
||||||
|
`https://github.com/ggml-org/llama.cpp/issues/21725` (open since 2026-04-10) ·
|
||||||
|
`https://github.com/ggml-org/llama.cpp/issues/1499` (closed stale 2025-04-07).
|
||||||
|
- `https://github.com/Cyronius/ggml-xdna` (prefill-only, gates, Linux requirements, results
|
||||||
|
measured 2026-10-05/06 on HX 370) ·
|
||||||
|
`https://github.com/BrandedTamarasu-glitch/OllamaAMDNPU` (XDNA2 ggml backend on Strix Halo,
|
||||||
|
phases 1–12, "use Vulkan exclusively", measured on Ryzen AI MAX 385).
|
||||||
|
- `https://github.com/amd/RyzenAI-SW` (main, 669 paths, 2026-08-18): audio content is ASR
|
||||||
|
only (`Demos/ASR/Whisper`, `Demos/ASR/Parakeet-TDT`, `Transformer-examples/ASR/Whisper-AI`);
|
||||||
|
**no TTS example anywhere in the official Ryzen AI Software tree**.
|
||||||
|
- `https://ryzenai.docs.amd.com/en/latest/relnotes.html` (Ryzen AI SW 1.8.0: ONNX-format
|
||||||
|
models, ONNX Runtime as the loader, power-of-two QDQ/QOP quantization).
|
||||||
|
- Mesa/RADV on AMD iGPUs: `https://www.amd.com/en/resources/support-articles/release-notes/RN-AMDGPU-UNIFIED-LINUX-25-20.3.html`
|
||||||
|
(Mesa Vulkan driver officially supported).
|
||||||
|
|
||||||
|
Status: COMPLETE for the assigned slice (payload architecture, runtime architecture,
|
||||||
|
portability). `[INFERENCE]` marks: none above except the Mesa-version caveat in ANSWER C,
|
||||||
|
which is flagged there.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
# TTS on the Strix Halo NPU — spike verdict (2026-10-19)
|
||||||
|
|
||||||
|
Question: **how can the fleet's TTS engine run on hardware rather than CPU?** Answer in
|
||||||
|
one line: not on the NPU — the loader, not the silicon, refuses it; the door that exists is
|
||||||
|
the gfx1151 iGPU behind a build flag we already own.
|
||||||
|
|
||||||
|
Spike evidence lives in `report/npu-tts-runtime-landscape-2026-10-19.md` (which runtimes can
|
||||||
|
reach XDNA2 at all) and `report/qwen3-tts-payload-portability-2026-10-19.md` (what the
|
||||||
|
payload is and where it can be moved). This page owns only the **measured spike result** and
|
||||||
|
the **model-side contract** neither slice has. Throwaway work ran in containers under
|
||||||
|
`/home/dank/spikes/npu-tts` on Dankclaw; nothing in the fleet was changed — `frank-tts` and
|
||||||
|
`frank-tts-engine` still serve `narrator1|2|3` on CPU.
|
||||||
|
|
||||||
|
Legend: **[host]** live census 2026-10-19 · **[spike]** measured in the spike containers ·
|
||||||
|
**[vendor]** primary vendor source, with the version it holds for.
|
||||||
|
|
||||||
|
## 1. Verdict
|
||||||
|
|
||||||
|
**NO-GO on the NPU, and the wall is a loader.** The XDNA2 device is healthy by AMD's own
|
||||||
|
instruments; the runtime that owns this model family opens the device and then declines the
|
||||||
|
graph, silently. One run loaded successfully; byte-identical runs never reproduced it, and a
|
||||||
|
path that cannot be reproduced is not a foundation. **[spike]**
|
||||||
|
|
||||||
|
**Go on the iGPU, and the lever is in this repo.**
|
||||||
|
`dankclaw/voiceservice/Containerfile.engine` builds `qwentts.cpp` (commit `5916bf7`) with
|
||||||
|
`-DGGML_VULKAN=OFF` written out deliberately beside CUDA/HIP/METAL/SYCL/RPC — the running
|
||||||
|
container carries `libggml-base.so.0.27.0` + `libggml-cpu.so.0.27.0` + `tts-server` and
|
||||||
|
nothing else. So: `-DGGML_VULKAN=ON` + `--device Vulkan0`, plus `/dev/dri` passthrough in
|
||||||
|
the engine quadlet (the ComfyUI pattern: `AddDevice` + `GroupAdd=keep-groups` + a
|
||||||
|
world-writable udev rule). The target shape is **co-tenancy** — the engine resident on the
|
||||||
|
gfx1151 *beside* `frank-dragoon`, not a handover. §5 carries the census and both gates:
|
||||||
|
the **heap gate is PASSED** — RADV's `DEVICE_LOCAL` heap is 77.33 GiB with 65.16 GiB of
|
||||||
|
budget while dragoon is loaded, so memory is not the constraint — which leaves the
|
||||||
|
contention gate (dragoon's TTFT with a synthesis in flight) and one unproven piece:
|
||||||
|
whether qwentts.cpp's codec ops have Vulkan kernels for every op. **[host]** **[spike]**
|
||||||
|
|
||||||
|
Why the iGPU is worth doing at all, given CPU already works: the deployed payload needs
|
||||||
|
only **12.5 codes/s** (§2), so this is latency headroom and freed cores, not reviving a
|
||||||
|
dead service. The independent Strix Halo measurement in the portability slice ("use Vulkan
|
||||||
|
exclusively"; NPU decode under 1.5 t/s for an 8B) points the same way.
|
||||||
|
|
||||||
|
## 2. The two arms do not share a real-time bar
|
||||||
|
|
||||||
|
|Arm|Talker|Codec|Codes per second of audio|Bar|
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|**Deployed** (`frank-tts-engine`)|`qwen-talker-1.7b-base-Q4_K_M.gguf`|`qwen-tokenizer-12hz-Q8_0.gguf`|**12.5**|~13 tok/s **[host]**|
|
||||||
|
|**Spike** (`amd/neutts-air_rai_1.8.0_npu_4K`)|NuTTS-Air, 24-layer Qwen2|NeuCodec `24000 / hop 480`|**50.00**|~50 tok/s **[spike]**|
|
||||||
|
|
||||||
|
Measured from the staged NPU artifact — the contract any future arm must satisfy:
|
||||||
|
|
||||||
|
|Fact|Value|
|
||||||
|
|---|---|
|
||||||
|
Vocab|`genai_config.json` `vocab_size` **217,652**; base BPE 151,643; specials 151,644–151,670 **[spike]**|
|
||||||
|
Speech tokens|**65,536** `<|speech_N|>` entries, ids **151,671–217,206** contiguous; the vendor template maps a codec index straight to its id, `"".join(f"<|speech_{i}|>")`. A 10,001-code assumption silently truncates the space — encoded references really do reach the 60k range. **[spike]**|
|
||||||
|
Stop token|`<|SPEECH_GENERATION_END|>` = 151,670, but the export's `model.eos_token_id` is `[151645, 151643]` (the *chat turn* stop) and `search.eos_token_ids` is unset — pass 151,670 explicitly or speech ends after one token **[spike]**|
|
||||||
|
Input ceiling|4096 tokens **total**, input + output **[vendor]**|
|
||||||
|
Frame rate|50.00 codes/s confirmed across three references (49.96 / 49.99 / 50.00) **[spike]**|
|
||||||
|
Reference encode|`librosa.load(sr=16000, mono=True)` → `[1,1,T]` float → `codec.encode_code(audio_or_path=…)` (vendor `NeuTTS.encode_reference`); CPU RTF 0.28–0.38 on 8 threads, so codes are a cacheable one-time cost, never a per-request charge. `neucodec` also needs `torchtune`; install both `--no-deps --target <dir>` so a second torch never shadows the venv's. **[spike]**|
|
||||||
|
Pre-encoded voices|`/spike/logs/codes-narrator{1,2,3}.npy` — 994 / 1,249 / 1,250 codes for 19.90 / 24.98 / 25.00 s **[spike]**|
|
||||||
|
|
||||||
|
**Gap that any arm inherits:** the voice packs carry no transcript. `manifest.json` holds
|
||||||
|
provenance and crop geometry (`crop.source_speech_run` 98.739–118.636 s of a 22,569 s
|
||||||
|
audiobook) and no text, and Dankclaw has no ASR at all — `whisper`, `whisper.cpp`,
|
||||||
|
`faster-whisper` all absent. NuTTS conditioning needs `ref_text`; do not invent it. **[host]**
|
||||||
|
|
||||||
|
## 3. Exoneration chain — why the blame is not the hardware
|
||||||
|
|
||||||
|
Every row is an instrument, not an inference.
|
||||||
|
|
||||||
|
|Probe|Result|
|
||||||
|
|---|---|
|
||||||
|
`xrt-smi examine`|`[0000:c6:00.1] RyzenAI-npu5 aie2p 6x8`, XRT 2.25.37 (vendor extract), NPU fw 1.1.2.65, `amdxdna 7.0.0-38-generic` **[spike]**|
|
||||||
|
`xrt-smi validate`|**PASSED**, including `throughput` **[spike]**|
|
||||||
|
`pyxrt.device(0)` in-container|constructs — XRT enumerates the device **[spike]**|
|
||||||
|
`strace -f -e trace=openat` on the failing load|`genai_config.json` read → `model.onnx` opened → `libxrt_driver_xdna.so.2` loaded → `/dev/accel/accel0` opened `O_RDWR` → GEM ioctls succeed → **EP declines, before touching `dd_metastate_*` or `cache/txn_bins.zip`** **[spike]**|
|
||||||
|
`dmesg`, `fuser /dev/accel/accel0`|silent since boot, no holder — the decline never reaches the kernel **[spike]**|
|
||||||
|
Hardlinked mirror dirs of the model tree (`rundir*`)|retired, no change — the "metastate shadowing" theory is dead **[spike]**|
|
||||||
|
Every logging lever in that build: `GENAI_ORT_VERBOSE_LOGGING`, `GENAI_LOG_ORT_LIB`, `set_log_options`, `set_log_callback(callable)`, `RYZENAI_EP_TRACING`, `RYZENAI_LLM_PROFILE_INIT`|**zero lines** — the decline path is silent **[spike]**|
|
||||||
|
Vendor `quicktest`|`Session successfully initialized` — proves the XRT/driver/device path, **not** NPU residency: that build falls back to CPU silently. Prove residency with `/sys/class/accel/accel0/device/mem_usage` during generation. **[spike] [vendor]**|
|
||||||
|
|
||||||
|
No NPU token rate was ever measured, so none is quoted here. The single successful load
|
||||||
|
(13:17, `[load] 0.84s`, finite logits, XRT banner at *generate* time) has not been
|
||||||
|
reproduced by the byte-identical command since, with the model files pristine by inode and
|
||||||
|
mtime. **[spike]**
|
||||||
|
|
||||||
|
## 4. Host gates the spike proved (so they are not re-litigated)
|
||||||
|
|
||||||
|
- **Rootless containers cannot read `/dev/accel/accel0`** (`root:render` 0660): the host
|
||||||
|
gid never enters the userns map. `--group-add keep-groups`, `--userns=keep-id`,
|
||||||
|
`--userns=keep-id:gid=990`, `--group-add render`, `--group-add gid:990` — all fail.
|
||||||
|
Doors: rootful podman (separate storage), or a world-writable udev rule, which the fleet
|
||||||
|
already does for `kfd`/`renderD*`. **[spike]**
|
||||||
|
- **Never `dpkg -i` the vendor XRT deb** — it installs a DKMS `amdxdna` over the distro
|
||||||
|
driver. `dpkg-deb -x` the debs and bind-mount the tree at `/opt/xilinx/xrt`; the plugin
|
||||||
|
deb is what carries `libxrt_driver_xdna.so`. **[spike]**
|
||||||
|
- In-tree `amdxdna 0.7.0-38` + `iommu=pt` is enough for context creation; a missing device
|
||||||
|
node is *not* the modern failure mode. **[host]**
|
||||||
|
- `xrt-smi` inside a container needs `libboost-filesystem1.83.0` +
|
||||||
|
`libboost-program-options1.83.0` (**hyphenated** Ubuntu names, after `apt update`);
|
||||||
|
boost is needed by `xrt-smi` only, never by the OGA/EP path. **[spike]**
|
||||||
|
- Headroom is tight and it is host RAM, not a separate pool — see §5 for tonight's census.
|
||||||
|
A multi-GB NPU partition plus an AIE compile workspace does not fit beside
|
||||||
|
`frank-dragoon`. Benign-looking `Failed to alloc dev bo memory, ret -28` is that
|
||||||
|
pressure. **[host]**
|
||||||
|
|
||||||
|
## 5. Target shape: the TTS engine shares the gfx1151 with `frank-dragoon`
|
||||||
|
|
||||||
|
The deployment Frank wants is **both resident at once**, not a handover. Measured 2026-10-19
|
||||||
|
(read-only, nothing loaded or unloaded):
|
||||||
|
|
||||||
|
|Measure|Value|Reading|
|
||||||
|
|---|---|---|
|
||||||
|
iGPU GTT in use|**18,523 MiB of 116,733 MiB**|≈96 GiB of aperture free; this is the real budget (fleet law) **[host]**|
|
||||||
|
DRM "VRAM" carve-out|2,048 MiB total, 174 MiB used|what KFD/DRM calls dedicated — **not** the budget **[host]**|
|
||||||
|
Host RAM|121 GiB total, 17 GiB available, 79 GiB buff/cache|**GTT pages are system pages**: an iGPU allocation is host RAM consumed, so the two tenants share one pool **[host]**|
|
||||||
|
TTS engine today|3.84 GiB RSS, CPU|its device-resident replacement lands in that same pool **[host]**|
|
||||||
|
|
||||||
|
**Two gates, in this order.**
|
||||||
|
|
||||||
|
1. **Heap gate — PASSED.** RADV does **not** hand a container tenant the 2 GiB DRM
|
||||||
|
carve-out. `vulkaninfo` inside the engine's own base image (`ubuntu:24.04` plus
|
||||||
|
`mesa-vulkan-drivers vulkan-tools`, `--device /dev/dri`, rootless) sees
|
||||||
|
`Radeon 8060S Graphics (RADV GFX1151)`, driver `radv`, API 1.4.318, and reports:
|
||||||
|
|
||||||
|
|Heap|size|budget with dragoon loaded|flags|
|
||||||
|
|---|---|---|---|
|
||||||
|
|0|38.67 GiB|32.58 GiB|host-visible, not device-local|
|
||||||
|
|1|**77.33 GiB**|**65.16 GiB**|`DEVICE_LOCAL`|
|
||||||
|
|
||||||
|
The heaps sum to the ~116 GiB aperture in Mesa's ⅓/⅔ APU split — the same RAM twice:
|
||||||
|
a Vulkan allocation is a GTT mapping, so a budget is a ceiling on mappings, not a pile
|
||||||
|
of free silicon memory. ggml's Vulkan backend asks for `DEVICE_LOCAL`, which is heap 1,
|
||||||
|
and the TTS engine needs roughly 4 GiB of it. **Memory stops being the question.** **[spike]**
|
||||||
|
Probe traps: `vulkaninfo --batch` is not a flag in this `vulkan-tools` (it prints the
|
||||||
|
help page and exits 1 — use `vulkaninfo --text`), and the container has **no NVIDIA
|
||||||
|
ICD**, so Vulkan device 0 is the iGPU here. Do not trust that ordering — select by
|
||||||
|
device *name*, or an image that grows an `nvidia_icd.json` silently points TTS at the
|
||||||
|
3090 slot.
|
||||||
|
2. **Contention gate — PASSED, measured 2026-10-19 after the cutover.** With a 16.9 s
|
||||||
|
synthesis occupying the device, `frank-dragoon` answered a streamed 124-token request at
|
||||||
|
**17 ms to first token and ~35 tok/s decode**, against 17 ms and ~33 tok/s for the same
|
||||||
|
request with the card idle. Compute-unit sharing showed up as nothing. One request per
|
||||||
|
arm, no ladder: these are the gate's evidence, not a throughput record, and the engine's
|
||||||
|
own shape lives in [[Frank TTS Engine]].
|
||||||
|
|
||||||
|
**Change surface — as landed** (PR #34, `6248f02`; the CPU shape stays reachable as the
|
||||||
|
rollback):
|
||||||
|
|
||||||
|
1. `Containerfile.engine` build stage: add **`libvulkan-dev glslc spirv-headers`**. The
|
||||||
|
prediction above (`libvulkan-dev glslang-dev`) is wrong on two counts and the configure
|
||||||
|
says so: ggml looks for `glslc`, which Ubuntu's `glslang-tools` does **not** ship (it
|
||||||
|
ships `glslangValidator`), and `find_package(SPIRV-Headers CONFIG REQUIRED)` needs the
|
||||||
|
distro's `spirv-headers`, which does carry the CMake config. `glslc` is its own source
|
||||||
|
package on noble, at 2023.8 — old enough that only `GL_KHR_cooperative_matrix` compiles,
|
||||||
|
so the coopmat2 / integer-dot / bf16 / micro-float shader families are built out. Suspect
|
||||||
|
that compiler before the device if a Vulkan prefill ever reads as slow.
|
||||||
|
2. `-DGGML_VULKAN=OFF` → `ON`: one word in the written-out flag list.
|
||||||
|
3. Runtime stage: `mesa-vulkan-drivers` **and** `libvulkan1` — the ICD and its loader. An
|
||||||
|
image with a loader and no ICD enumerates zero devices.
|
||||||
|
4. `frank-tts-engine.container`: `AddDevice=/dev/dri` + `GroupAdd=keep-groups` — the
|
||||||
|
DIRECTORY, never the node: on this hybrid box `renderD128` is the NVIDIA card and
|
||||||
|
`renderD129` the amdgpu iGPU (`readlink -f /sys/class/drm/renderD129/device/driver` →
|
||||||
|
`amdgpu`). On this host the udev rule already makes the nodes world-writable, so the
|
||||||
|
group is belt-and-braces, not the gate.
|
||||||
|
5. Engine argv: `--device Vulkan0`, reached through `QWENTTS_DEVICE` in
|
||||||
|
`scripts/frank-tts-engine.env`. **An empty value passes no flag at all** — that is the
|
||||||
|
CPU rollback, and it is why the entrypoint's default is an `-v` test rather than `:=`,
|
||||||
|
which also fires on a set-but-empty value and would silently undo the rollback.
|
||||||
|
|
||||||
|
Gate 1 recipe, already run and **PASSED**: `mesa-vulkan-drivers` + `vulkan-tools` in a
|
||||||
|
throwaway container off the same base, `--device /dev/dri`, then `vulkaninfo --text` and read
|
||||||
|
the `VkPhysicalDeviceMemoryProperties` heap sizes — no model, no load, no compute. The probe
|
||||||
|
trap: `vulkaninfo --batch` is not a flag in Ubuntu 24.04's `vulkan-tools` — it prints the help
|
||||||
|
page and exits 1, which reads like a driver failure. Every package involved has a candidate on
|
||||||
|
the host's apt (`vulkan-tools` 1.4.341, `libvulkan-dev` 1.4.341, `glslang-dev`/`glslang-tools`
|
||||||
|
16.2.0); the build stage runs on the digest-pinned Ubuntu 24.04 base, whose versions are older
|
||||||
|
but present. Script: `/home/dank/spikes/npu-tts/vk_probe.sh` on Dankclaw.
|
||||||
|
|
||||||
|
Constraints that do not relax for a co-tenant: `/dev/dri` passthrough needs the ComfyUI
|
||||||
|
pattern (`AddDevice` + `GroupAdd=keep-groups` + the world-writable udev rule) because
|
||||||
|
systemd user-unit spawns carry no supplementary groups; the 64 GiB swap at `/swap.img`
|
||||||
|
exists to protect co-tenants, and paging a KV pool is worse than the OOM it prevents;
|
||||||
|
`pp_dpm_fclk` reads `auto` (unheld), which only becomes a hazard if the NPU ever joins.
|
||||||
|
|
||||||
|
## 6. Re-visit condition
|
||||||
|
|
||||||
|
Retry the NPU when **a Linux loader claims the graph**: an `onnxruntime-genai-ryzenai`
|
||||||
|
build whose RyzenAI adapter takes the NuTTS-Air Full-Fusion graph on `RyzenAI-npu5`, or that
|
||||||
|
model appearing in a runtime's own supported catalog with a Linux runner. Then the
|
||||||
|
prerequisites are already mapped and priced: the gated NeuCodec encoder for cloned voices,
|
||||||
|
the `ref_text` transcripts, the 4096-token prompt budget, and 50 codes/s as the bar.
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
# The scratch mirrors the production container contract exactly (serve-frank-
|
# The scratch mirrors the production container contract exactly (serve-frank-
|
||||||
# primary.sh): --network host, --init, seccomp=unconfined (io_uring for the
|
# primary.sh): --network host, --init, seccomp=unconfined (io_uring for the
|
||||||
# n-gram table), memlock unbounded (the ~132 GiB pinned ledger), /dev/kfd +
|
# n-gram table), memlock unbounded (the ~132 GiB pinned ledger), /dev/kfd +
|
||||||
# /dev/dri, the .rad ro, the state dir rw, the tune cache mount. The ONLY
|
# /dev/dri, the .rad ro, the tune cache mount. The ONLY differences: the
|
||||||
# differences: the container name (arbench, so prod's identity is untouched)
|
# container name (arbench, so prod's identity is untouched) and the prefix cache
|
||||||
# and a FRESH prefix-cache disk tier (arbench/, wiped per run) so the cold
|
# switched OFF (--prefix-cache-mib 0), so every prefill is cold — the tier is
|
||||||
# prefill is cold — the prod tier at kvcache/frank-archon is left intact.
|
# host RAM now, there is no per-run disk directory to wipe.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
ENV_FILE="${1:-/home/dank/serverAppConfigs/scripts/frank-archon.env}"
|
ENV_FILE="${1:-/home/dank/serverAppConfigs/scripts/frank-archon.env}"
|
||||||
@@ -60,9 +60,8 @@ set -a; . "$ENV_FILE"; set +a
|
|||||||
sudo systemctl stop frank-archon >/dev/null 2>&1 || true
|
sudo systemctl stop frank-archon >/dev/null 2>&1 || true
|
||||||
podman rm -f "$SCRATCH" >/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) ------
|
# --- the prefix cache is switched off below, so there is no scratch KV tree ---
|
||||||
FRESH_KV="$FRANK_ARCHON_STATE_DIR/kvcache/$SCRATCH"
|
# to wipe; the cold prefill comes from --prefix-cache-mib 0.
|
||||||
rm -rf "$FRESH_KV"; mkdir -p "$FRESH_KV"
|
|
||||||
mkdir -p "$FRANK_ARCHON_STATE_DIR/tune"
|
mkdir -p "$FRANK_ARCHON_STATE_DIR/tune"
|
||||||
|
|
||||||
# --- boot the scratch: the production run line, scratch name + fresh KV ------
|
# --- boot the scratch: the production run line, scratch name + fresh KV ------
|
||||||
@@ -73,7 +72,6 @@ podman run -d --name "$SCRATCH" \
|
|||||||
--ulimit memlock=-1:-1 \
|
--ulimit memlock=-1:-1 \
|
||||||
--device /dev/kfd --device /dev/dri \
|
--device /dev/kfd --device /dev/dri \
|
||||||
-v "$FRANK_ARCHON_MODEL_DIR":/models:ro \
|
-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_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||||
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
||||||
"$FRANK_ARCHON_IMAGE" \
|
"$FRANK_ARCHON_IMAGE" \
|
||||||
@@ -87,12 +85,9 @@ podman run -d --name "$SCRATCH" \
|
|||||||
--placement "$FRANK_ARCHON_PLACEMENT" \
|
--placement "$FRANK_ARCHON_PLACEMENT" \
|
||||||
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
||||||
--gpu-headroom-mib "$FRANK_ARCHON_GPU_HEADROOM_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" \
|
--kv-cache-dtype "$FRANK_ARCHON_KV_DTYPE" \
|
||||||
--prefix-cache-host-mib "$FRANK_ARCHON_PREFIX_HOST_MIB" \
|
--prefix-cache-mib 0 \
|
||||||
--prefix-cache-dir "/data/kvcache/$SCRATCH" \
|
--busy-prefill-chunk "${FRANK_ARCHON_BUSY_PREFILL_CHUNK:-auto}" \
|
||||||
--prefix-cache-disk-mib "$FRANK_ARCHON_PREFIX_DISK_MIB" \
|
|
||||||
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
||||||
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
||||||
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
||||||
|
|||||||
Executable
+164
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Destroy measurement ledgers in the surviving pages.
|
||||||
|
|
||||||
|
Point of use (AGENTS.md → Documentation Policy): a rate/throughput figure for
|
||||||
|
anything not running and not being compared is destroyed, not filed. This pass
|
||||||
|
deletes
|
||||||
|
|
||||||
|
1. any section whose heading is a bench/throughput/rate/ladder/verdict ledger,
|
||||||
|
2. any table row or standalone bullet that REPORTS a rate (`123.4 t/s`,
|
||||||
|
`tok/s`, `ms/token`, `TTFT`, acceptance %, `llama-bench`, a bench arm),
|
||||||
|
|
||||||
|
and it never touches a shape number (ctx, slots, MiB budgets, ports) or a bullet
|
||||||
|
under a *Laws* / *Wire contract* / *Rollback* heading, where the sentence is the
|
||||||
|
payload and the number is decoration. Dry run by default; `--apply` writes.
|
||||||
|
|
||||||
|
Usage: axe-measurements.py [--apply] [--verbose]
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||||
|
RATE = re.compile(
|
||||||
|
r"\d[\d,.]*\s*(t/s|tok/s|tokens?/s|tok s|tps|ms/token|µs/token|GB/s|GiB/s)\b|"
|
||||||
|
r"\b(llama-bench|bench (arm|ladder|window)|decode ladder|prefill ladder)\b|"
|
||||||
|
r"\bacceptance (rate|of)\b|\bdraft_n\b|\bttft\b|\bServer-Timing\b",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
# A heading is a ledger only when the noun LEDS it and nothing instructional
|
||||||
|
# follows: `## Throughput` yes, `## 6. The context ladder (find the max bootable
|
||||||
|
# window by measurement)` no — that one is the method, which is point-of-use.
|
||||||
|
LEDGER_HEAD = re.compile(
|
||||||
|
r"^#{2,4}\s+(?:\d+[.) ]\s*)?(bench(es|mark|marks)?|throughput|results?|timings?|"
|
||||||
|
r"measured (figures|numbers|results|rates|rates)|rates|verdict|acceptance|ladder)"
|
||||||
|
r"\b[^|]{0,40}$",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
INSTRUCTIONAL = re.compile(r"how|why|law|policy|method|procedure|find|by measurement|execution|flip|checklist", re.I)
|
||||||
|
PROTECTED = re.compile(
|
||||||
|
r"^#{2,4}\s+.*(law|wire|rollback|contract|shape|eras|files|health|see also|related|"
|
||||||
|
r"boot|preflight|environment)",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
def blank_rate_cells(text):
|
||||||
|
"""Kill throughput COLUMNS wherever they appear: a cell that reports a rate
|
||||||
|
becomes an em-dash, so the row keeps whatever else it records (the payload
|
||||||
|
name, the reason, the rollback) and loses only the dead number."""
|
||||||
|
out = []
|
||||||
|
for line in text.split("\n"):
|
||||||
|
st = line.strip()
|
||||||
|
if st.startswith("|") and st.endswith("|") and RATE.search(line):
|
||||||
|
cells = line.split("|")
|
||||||
|
for i, c in enumerate(cells):
|
||||||
|
if RATE.search(c):
|
||||||
|
cells[i] = " — "
|
||||||
|
line = "|".join(cells)
|
||||||
|
out.append(line)
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def collapse_eras(text):
|
||||||
|
"""One Eras row per era is fleet law; the NUMBERS in the old rows are not.
|
||||||
|
Keep the current era verbatim, fold everything older into one row that says
|
||||||
|
the numbers were destroyed and where the rollback actually lives (git)."""
|
||||||
|
lines = text.split("\n")
|
||||||
|
out = []
|
||||||
|
in_eras = False
|
||||||
|
ncol = 0
|
||||||
|
folded = 0
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("#"):
|
||||||
|
in_eras = bool(re.match(r"^#{2,4}\s+.*eras\b", line, re.I))
|
||||||
|
if folded:
|
||||||
|
out.append("| " + " | ".join(
|
||||||
|
[f"≤ 2026-09 — {folded} earlier eras: payloads, shapes and every measured "
|
||||||
|
"number destroyed 2026-10-19 as obsolete"] + ["—"] * max(0, ncol - 1)))
|
||||||
|
folded = 0
|
||||||
|
out.append(line)
|
||||||
|
continue
|
||||||
|
st = line.strip()
|
||||||
|
if in_eras and st.startswith("|"):
|
||||||
|
cells = [c.strip() for c in st.strip("|").split("|")]
|
||||||
|
if set("".join(cells)) <= set(":- "):
|
||||||
|
ncol = len(cells)
|
||||||
|
out.append(line)
|
||||||
|
continue
|
||||||
|
if ncol == 0:
|
||||||
|
ncol = len(cells)
|
||||||
|
if re.search(r"2026-1[01]|current|active|running", st, re.I):
|
||||||
|
out.append(line)
|
||||||
|
else:
|
||||||
|
folded += 1
|
||||||
|
continue
|
||||||
|
if folded and not st.startswith("|"):
|
||||||
|
out.append("| " + " | ".join(
|
||||||
|
[f"≤ 2026-09 — {folded} earlier eras: payloads, shapes and every measured number "
|
||||||
|
"destroyed 2026-10-19 as obsolete"] + ["—"] * max(0, ncol - 1)))
|
||||||
|
folded = 0
|
||||||
|
ncol = 0
|
||||||
|
out.append(line)
|
||||||
|
if folded:
|
||||||
|
out.append("| " + " | ".join(
|
||||||
|
[f"≤ 2026-09 — {folded} earlier eras destroyed 2026-10-19"] + ["—"] * max(0, ncol - 1)))
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
apply = "--apply" in argv
|
||||||
|
verbose = "--verbose" in argv
|
||||||
|
total_lines = 0
|
||||||
|
pages = 0
|
||||||
|
for page in sorted(VAULT.rglob("*.md")):
|
||||||
|
s = str(page)
|
||||||
|
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||||
|
continue
|
||||||
|
text = page.read_text(errors="replace")
|
||||||
|
before = len(text.split("\n"))
|
||||||
|
if s.startswith("serverAppConfigs-docs/LLM/"):
|
||||||
|
text = collapse_eras(blank_rate_cells(text))
|
||||||
|
lines = text.split("\n")
|
||||||
|
out = []
|
||||||
|
skip = False
|
||||||
|
under_protected = False
|
||||||
|
cut = 0
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("#"):
|
||||||
|
under_protected = bool(PROTECTED.search(line))
|
||||||
|
skip = (bool(LEDGER_HEAD.match(line)) and not under_protected
|
||||||
|
and not INSTRUCTIONAL.search(line))
|
||||||
|
if skip:
|
||||||
|
cut += 1
|
||||||
|
continue
|
||||||
|
if skip:
|
||||||
|
cut += 1
|
||||||
|
continue
|
||||||
|
stripped = line.strip()
|
||||||
|
out.append(line)
|
||||||
|
if under_protected:
|
||||||
|
# Eras rows are the rollback record, Law bullets are the payload:
|
||||||
|
# the number is decoration inside them, and the row/bullet is not
|
||||||
|
# a ledger entry. Leave the whole section alone.
|
||||||
|
continue
|
||||||
|
if stripped.startswith(("|", "-", "*")) and RATE.search(line):
|
||||||
|
# Destroy only when the line EXISTS to report a number: a table
|
||||||
|
# row, or a bullet whose verb is measurement.
|
||||||
|
if stripped.startswith("|") or re.search(
|
||||||
|
r"\b(measured|measures|bench|benched|ran at|runs at|clocked|hit|"
|
||||||
|
r"landed at|came in at|peak|peaks at|sustained|wall.clock)\b", line, re.I):
|
||||||
|
out.pop()
|
||||||
|
cut += 1
|
||||||
|
final = re.sub(r"\n{3,}", "\n\n", "\n".join(out))
|
||||||
|
saved = before - len(final.split("\n"))
|
||||||
|
if saved or cut:
|
||||||
|
pages += 1
|
||||||
|
total_lines += max(saved, cut)
|
||||||
|
print(f" {s.replace('serverAppConfigs-docs/', '')}: -{max(saved, cut)}")
|
||||||
|
if apply:
|
||||||
|
page.write_text(final.rstrip("\n") + "\n")
|
||||||
|
print(f"{'APPLIED' if apply else 'dry run'}: {pages} pages, {total_lines} lines of ledger")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -1463,6 +1463,12 @@ LLM_ZEALOT_RUNTIME="${LLM_ZEALOT_RUNTIME:-/home/dank/GitPackages/llama-prism-b10
|
|||||||
# 868.32 MiB worst-case VRAM (595.29 model + 248.10 compute in the boot ledger)
|
# 868.32 MiB worst-case VRAM (595.29 model + 248.10 compute in the boot ledger)
|
||||||
# lands AFTER the engine's fit check, so it is not inside the q8_0 budget.
|
# lands AFTER the engine's fit check, so it is not inside the q8_0 budget.
|
||||||
LLM_ZEALOT_MMPROJ="${LLM_ZEALOT_MMPROJ:-/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}"
|
LLM_ZEALOT_MMPROJ="${LLM_ZEALOT_MMPROJ:-/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}"
|
||||||
|
# The refusal adapter is part of the payload identity now, so the deployer
|
||||||
|
# checks it beside the other two files — but it has its OWN fetcher, so a
|
||||||
|
# missing adapter must not send the operator to the bonsai9 script. Integrity
|
||||||
|
# (bytes + sha256) is the fetcher's job at download time; this gate only asks
|
||||||
|
# whether the file the env names exists, exactly like the two above.
|
||||||
|
LLM_ZEALOT_LORA="${LLM_ZEALOT_LORA:-/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf}"
|
||||||
|
|
||||||
deploy_llm_zealot() {
|
deploy_llm_zealot() {
|
||||||
# Refuse rather than restart-loop: a unit whose runtime or payload is
|
# Refuse rather than restart-loop: a unit whose runtime or payload is
|
||||||
@@ -1484,6 +1490,13 @@ deploy_llm_zealot() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
if [[ ! -f "$LLM_ZEALOT_LORA" ]]; then
|
||||||
|
echo "ERROR: frank-zealot refusal adapter absent at $LLM_ZEALOT_LORA" >&2
|
||||||
|
echo "frank-zealot: run scripts/download-frank-zealot-uncensored-lora.sh first;" >&2
|
||||||
|
echo "frank-zealot: the env names the adapter, so the unit would refuse to boot" >&2
|
||||||
|
echo "frank-zealot: without it — starting the unit anyway is a crash loop." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
relink_unit frank-zealot
|
relink_unit frank-zealot
|
||||||
run_privileged systemctl daemon-reload
|
run_privileged systemctl daemon-reload
|
||||||
run_privileged systemctl enable frank-zealot.service
|
run_privileged systemctl enable frank-zealot.service
|
||||||
|
|||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Destroy the vault's dead weight: era ledgers, retired runtimes, dead experiments.
|
||||||
|
|
||||||
|
`AGENTS.md` was treating a measurement as a record worth keeping forever. It is not: a
|
||||||
|
throughput figure for a payload that no longer runs, on a runtime that no longer exists,
|
||||||
|
is a liability — the next agent spends context on a number that AI progress has already
|
||||||
|
obsolete. This pass moves those pages to `.AGENTTRASH/` (repo law: never `rm`) and then
|
||||||
|
deletes every reference to them from the surviving pages, including the sentence that
|
||||||
|
carried the link when the link was the sentence's only content.
|
||||||
|
|
||||||
|
Usage: destroy-vault.py <list-file>
|
||||||
|
List file: one vault-relative path per line; a directory path destroys the whole tree.
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||||
|
TRASH = VAULT / ".AGENTTRASH" / "20261019-destruction"
|
||||||
|
|
||||||
|
|
||||||
|
def targets(list_file):
|
||||||
|
out = []
|
||||||
|
for line in pathlib.Path(list_file).read_text().splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
p = VAULT / line
|
||||||
|
if p.is_dir():
|
||||||
|
out += [f for f in sorted(p.rglob("*.md"))]
|
||||||
|
elif p.is_file():
|
||||||
|
out.append(p)
|
||||||
|
else:
|
||||||
|
print(f" SKIP (missing): {line}")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if len(argv) < 2:
|
||||||
|
print(__doc__)
|
||||||
|
return 2
|
||||||
|
doomed = targets(argv[1])
|
||||||
|
# Stems come from the trash too, so re-running the pass (or running it after a
|
||||||
|
# partial destruction) still knows which names are dead and keeps scrubbing.
|
||||||
|
stems = {p.stem for p in doomed}
|
||||||
|
if TRASH.is_dir():
|
||||||
|
stems |= {p.stem for p in TRASH.rglob("*.md")}
|
||||||
|
moved = 0
|
||||||
|
for p in doomed:
|
||||||
|
dest = TRASH / p.relative_to(VAULT)
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.move(str(p), str(dest))
|
||||||
|
moved += 1
|
||||||
|
print(f"destroyed {moved} pages ({sum(len(p.read_text(errors='replace').splitlines()) for p in TRASH.rglob('*.md'))} lines staged in trash)")
|
||||||
|
|
||||||
|
# Repoint-or-delete: a link to a destroyed page is itself dead information.
|
||||||
|
link_wl = re.compile(r"\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|([^\]]*))?\]\]")
|
||||||
|
link_md = re.compile(r"\[[^\]]*\]\(([^)#]*\.md)(?:#[^)]*)?\)")
|
||||||
|
pages_cleaned = links_dropped = lines_dropped = 0
|
||||||
|
for page in sorted(VAULT.rglob("*.md")):
|
||||||
|
s = str(page)
|
||||||
|
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||||
|
continue
|
||||||
|
text = page.read_text(errors="replace")
|
||||||
|
lines = text.split("\n")
|
||||||
|
out = []
|
||||||
|
hits = 0
|
||||||
|
|
||||||
|
def dead(target):
|
||||||
|
# NOT pathlib.Path(...).stem: that strips a real suffix off names like
|
||||||
|
# `Lemonade and sd.cpp`, so a link to a destroyed page survives and the
|
||||||
|
# vault then fails its own link check.
|
||||||
|
t = target.strip()
|
||||||
|
if t.endswith(".md"):
|
||||||
|
t = t[:-3]
|
||||||
|
return t in stems or pathlib.PurePosixPath(t).name in stems
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
found = [t for t in link_wl.findall(line) if dead(t[0])]
|
||||||
|
for m in link_md.finditer(line):
|
||||||
|
if dead(m.group(1)):
|
||||||
|
found.append(m.group(1))
|
||||||
|
if not found:
|
||||||
|
out.append(line)
|
||||||
|
continue
|
||||||
|
hits += len(found)
|
||||||
|
stripped = link_wl.sub(
|
||||||
|
lambda m: m.group(2) if (m.group(2) and not dead(m.group(1))) else "",
|
||||||
|
line)
|
||||||
|
stripped = link_md.sub(
|
||||||
|
lambda m: "" if dead(m.group(1)) else m.group(0), stripped)
|
||||||
|
# What is left of the line after the dead links are gone?
|
||||||
|
residue = re.sub(r"[\s\-*•·|:;,.\u2014\u2013]+", "", stripped)
|
||||||
|
if residue:
|
||||||
|
out.append(re.sub(r"\s+$", "", re.sub(r"^(\s*[-*]\s*)[\s:;,.\u2014-]+", r"\1", stripped)))
|
||||||
|
else:
|
||||||
|
lines_dropped += 1
|
||||||
|
if hits:
|
||||||
|
page.write_text("\n".join(out))
|
||||||
|
pages_cleaned += 1
|
||||||
|
links_dropped += hits
|
||||||
|
print(f"cleaned {pages_cleaned} surviving pages: {links_dropped} dead links dropped, "
|
||||||
|
f"{lines_dropped} lines left empty and deleted")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fetch the refusal-direction adapter that frank-zealot applies at load:
|
||||||
|
# AtomicChat/Ternary-Bonsai-2-9B-Uncensored-LoRA-GGUF — a **3.2 MB rank-1 LoRA**
|
||||||
|
# for prism-ml/Ternary-Bonsai-2-9B, not a checkpoint. Public repo: no auth
|
||||||
|
# header, and never a token on the command line (a Bearer token is
|
||||||
|
# world-readable in /proc/<pid>/cmdline).
|
||||||
|
#
|
||||||
|
# IT IS AN EDIT INSIDE THE COMPUTE GRAPH, NOT A SECOND MODEL. The card: B = -r
|
||||||
|
# (the refusal direction, row 23 of its own probe matrix), A = P^T(s *
|
||||||
|
# H(r^T W_fold)) built from the base pack's folded input basis, applied at every
|
||||||
|
# transformer layer (`target_layers = all`, `sites = 64`), with the embedding
|
||||||
|
# deliberately untouched because an embedding LoRA crashes this fork's graph.
|
||||||
|
# `--lora-scaled <file>:<scale>` is the flag pair: scale 1 is the card's
|
||||||
|
# "held-out refusals 0 %" point, scale 0 is the plain model again.
|
||||||
|
#
|
||||||
|
# THE RUNTIME IS A GATE, NOT A PREFERENCE. The card verified the adapter on
|
||||||
|
# `prism-b10777-e3de715` — the exact build frank-zealot runs — and says older
|
||||||
|
# forks (e.g. `prism-b10709`) refuse these packs with `unsupported
|
||||||
|
# prism.hadamard.version: 2`. The fork is already pinned for the payload's
|
||||||
|
# custom tensor type 142; this adapter adds a second reason it cannot move.
|
||||||
|
#
|
||||||
|
# PROVENANCE ASYMMETRY, STATED HONESTLY. The GGUF's own metadata names the pack
|
||||||
|
# the direction was fitted against:
|
||||||
|
# prism.abliterate.base_gguf = Ternary-Bonsai-2-9B-PTQ1_0.gguf
|
||||||
|
# prism.abliterate.base_sha256 = 3561df7ac7d26a71ffa991ac2db740b10841c3a350aa2676ccd3ff5d428b4e8b
|
||||||
|
# frank-zealot serves the PQ2_0 + MTP + Q8_0-embedding packing (its sha256 is
|
||||||
|
# c79fcdfa…, see download-frank-zealot-bonsai9.sh), which is NOT that file and is
|
||||||
|
# not on this host. The adapter still applies — same tensor names, and the
|
||||||
|
# metadata is provenance rather than a checksum gate — but its strength was
|
||||||
|
# calibrated on a sibling quantization. What proves the effect here is the
|
||||||
|
# behaviour of the endpoint (a refusal probe before and after the swap), not the
|
||||||
|
# metadata and not the card's table, which was measured on PTQ1_0.
|
||||||
|
#
|
||||||
|
# Fresh downloads, no resume; -f/--fail-with-body so an HTTP error is not a
|
||||||
|
# success, then byte count and sha256 (this repo is NOT LFS — the file is a
|
||||||
|
# plain git object, so the sha below is the content hash of the Hub's bytes as
|
||||||
|
# fetched, not an LFS oid). Idempotent: a complete file is skipped.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/download-frank-zealot-uncensored-lora.sh [DEST_DIR] [--verify-only]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEST=""
|
||||||
|
VERIFY_ONLY=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--verify-only) VERIFY_ONLY=1 ;;
|
||||||
|
*) DEST="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
DEST="${DEST:-/home/dank/models/Bonsai2-9B-MTP}"
|
||||||
|
|
||||||
|
REPO="AtomicChat/Ternary-Bonsai-2-9B-Uncensored-LoRA-GGUF"
|
||||||
|
# name | bytes | sha256 of the Hub's bytes
|
||||||
|
FILES=(
|
||||||
|
"Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf|3155424|861b9c099fb5cd81438ba85bad30590847c777b1e18112586be7f4f9ae5fb07b"
|
||||||
|
)
|
||||||
|
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
status=0
|
||||||
|
for entry in "${FILES[@]}"; do
|
||||||
|
IFS='|' read -r name size sha <<<"$entry"
|
||||||
|
url="https://huggingface.co/${REPO}/resolve/main/${name}"
|
||||||
|
f="$DEST/$name"
|
||||||
|
have=0
|
||||||
|
if [[ -f "$f" ]] && [[ "$(stat -c%s "$f")" == "$size" ]]; then
|
||||||
|
if sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
|
||||||
|
have=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if (( have )); then
|
||||||
|
echo "OK $name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if (( VERIFY_ONLY )); then
|
||||||
|
echo "MISSING $name"
|
||||||
|
status=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "FETCHING $name (from $REPO)"
|
||||||
|
tmp="$f.part"
|
||||||
|
curl -fsSL --fail-with-body -o "$tmp" "$url"
|
||||||
|
mv "$tmp" "$f"
|
||||||
|
if [[ "$(stat -c%s "$f")" != "$size" ]]; then
|
||||||
|
echo "SIZE FAIL $name ($(stat -c%s "$f") != $size)"
|
||||||
|
status=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
|
||||||
|
echo "SHA FAIL $name"
|
||||||
|
status=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "VERIFIED $name"
|
||||||
|
done
|
||||||
|
if (( status == 0 )); then
|
||||||
|
echo "DOWNLOAD_DONE $DEST"
|
||||||
|
else
|
||||||
|
echo "DOWNLOAD_FAILED $DEST" >&2
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
+59
-27
@@ -7,10 +7,15 @@
|
|||||||
# only the reason a reader editing THIS line cannot recover from the page.
|
# only the reason a reader editing THIS line cannot recover from the page.
|
||||||
# Where the two disagree, this file wins on values; the boot ledger arbitrates
|
# Where the two disagree, this file wins on values; the boot ledger arbitrates
|
||||||
# the engine shape.
|
# the engine shape.
|
||||||
FRANK_ARCHON_IMAGE=stilldeadcode/radiance@sha256:4c3f484564bb2872784e46b0a59cbd71becc9c1e746e301a3596ffae253c322e
|
FRANK_ARCHON_IMAGE=stilldeadcode/radiance@sha256:d590e300234bf6d2f942a0b8d456d183153fd808f28820e6c2fa69c9389178ed
|
||||||
# 1.0.13 is pinned by digest on purpose: 1.2.0 is a DEAD END on this payload
|
# 1.8.1 (2026-10-18), pinned by digest. Two CLI breaks sit between it and the
|
||||||
# (acceptance 0.7123 vs 0.7549 — the page's Dead ends section names the suspect).
|
# 1.0.13 line this endpoint ran for two eras, and both are boot refusals, not
|
||||||
# The .rad file mounted under /models; /data holds the prefix-cache disk tier.
|
# warnings: --vram-weights-mib/--vram-kv-mib were REMOVED at 1.3.0 (the card is
|
||||||
|
# now split by the engine's budget resolver — see below), and the prefix cache
|
||||||
|
# lost its disk tier. Draft acceptance has to be re-scored on any pin past 1.2.0:
|
||||||
|
# 1.2.0's MoE weight rotation measured 0.7123 against the 0.7549 control, and the
|
||||||
|
# suspect is untangled (the page's Dead ends section names it).
|
||||||
|
# The .rad file mounts under /models; the tune cache mounts separately below.
|
||||||
# The model dir holds ONLY .rad files — the HF download and staging stay beside it.
|
# The model dir holds ONLY .rad files — the HF download and staging stay beside it.
|
||||||
FRANK_ARCHON_MODEL_DIR=/home/dank/models/rad
|
FRANK_ARCHON_MODEL_DIR=/home/dank/models/rad
|
||||||
FRANK_ARCHON_MODEL_FILE=qwen3.8-next-flash-fp8-iq4r-moe.rad
|
FRANK_ARCHON_MODEL_FILE=qwen3.8-next-flash-fp8-iq4r-moe.rad
|
||||||
@@ -42,31 +47,42 @@ FRANK_ARCHON_TP=2
|
|||||||
# MAX_SEQS x (1 + SPEC_TOKENS) below.
|
# MAX_SEQS x (1 + SPEC_TOKENS) below.
|
||||||
FRANK_ARCHON_TP_WIRE=wht6
|
FRANK_ARCHON_TP_WIRE=wht6
|
||||||
# Decode slots. A slot costs KV, never decode; the pool is sized to this
|
# Decode slots. A slot costs KV, never decode; the pool is sized to this
|
||||||
# contract and nothing more, so a FOURTH full-height conversation is REFUSED as
|
# contract and nothing more. At 1.8.1 the budget resolver caps the cache at
|
||||||
# too large to fit — never squeezed, paged or truncated. That is Frank's
|
# 556,348 fp8 tokens — 2.8 sessions at 200,000 — so a THIRD full-height
|
||||||
# explicit trade: the freed MiB went to the expert slab.
|
# conversation answers 400 while /health and every shorter chat work fine
|
||||||
FRANK_ARCHON_MAX_SEQS=3
|
# (3 x 50k streams run at 77.6 t/s aggregate). Frank's trade: declare the two
|
||||||
|
# slots the engine can hold and let the freed ~1.1 GiB go to the expert plane,
|
||||||
|
# instead of advertising a third slot that only exists at shorter prompts.
|
||||||
|
FRANK_ARCHON_MAX_SEQS=2
|
||||||
# Served window. The engine treats `kv group tokens >= max-model-len` as a HARD
|
# Served window. The engine treats `kv group tokens >= max-model-len` as a HARD
|
||||||
# limit, and models.yml contextWindow MUST quote this number (over-length is a
|
# limit, and models.yml contextWindow MUST quote this number (over-length is a
|
||||||
# refusal, not a truncation). The pool is sized for seqs x maxlen, not one
|
# refusal, not a truncation). The pool is sized for seqs x maxlen, not one
|
||||||
# sequence.
|
# sequence.
|
||||||
FRANK_ARCHON_MAX_MODEL_LEN=200000
|
FRANK_ARCHON_MAX_MODEL_LEN=200000
|
||||||
FRANK_ARCHON_PLACEMENT=expert_tiered
|
FRANK_ARCHON_PLACEMENT=expert_tiered
|
||||||
# Pinned host pool for expert rows that do not fit VRAM. It moves WITH
|
# Pinned host pool for expert rows that do not fit VRAM. Under the budget
|
||||||
# VRAM_WEIGHTS: the pair and this pool serve the per-rank resident set together,
|
# resolver this is ALSO the lever that decides which experts stay on the card:
|
||||||
# so cutting the slab spills here — under-sizing it is a boot failure (36,864
|
# the card's leftover goes to the expert plane first and the KV cache takes the
|
||||||
|
# rest, so growing the pool moves experts off the card and shrinking it spills
|
||||||
|
# them onto VRAM the cache wanted. Under-sizing it is a boot failure (36,864
|
||||||
# refused a boot). The ledger's coverage line is the referee, not this file.
|
# refused a boot). The ledger's coverage line is the referee, not this file.
|
||||||
|
# Measured, not guessed: 45,568 (PR #30) moved host_pool to 45.09 GiB and left
|
||||||
|
# vram_kv byte-identical at 4.20 GiB / 556,348 tokens on both ranks — extra
|
||||||
|
# host RAM above what the expert plane's footprint needs buys no cache. Back at
|
||||||
|
# 44,928, the size that boots clean.
|
||||||
FRANK_ARCHON_HOST_POOL_MIB=44928
|
FRANK_ARCHON_HOST_POOL_MIB=44928
|
||||||
# VRAM left unclaimed per card (publisher default 96 MiB); the engine's settle
|
# VRAM left unclaimed per card (publisher default 96 MiB); the engine's settle
|
||||||
# floor is built from it.
|
# floor is built from it.
|
||||||
FRANK_ARCHON_GPU_HEADROOM_MIB=96
|
FRANK_ARCHON_GPU_HEADROOM_MIB=96
|
||||||
# ONE budget per card: VRAM_WEIGHTS + VRAM_KV <= 30,969 MiB, the ceiling the
|
# There is no VRAM pair any more. --vram-weights-mib/--vram-kv-mib were removed
|
||||||
# engine's own budget table prints (derivation and the slab/pool price: the
|
# at 1.3.0: the engine takes capacity - already-held - GPU_HEADROOM - arena,
|
||||||
# page's VRAM laws). Idle KV VRAM is ALSO the expert cache's spillover, so a
|
# puts the static (non-expert) weights off the top, then divides the rest between
|
||||||
# bigger pool pays twice — which is why the pool sits at the contract and the
|
# the expert plane and the KV cache — experts first when the cache has no idle
|
||||||
# slab takes the rest.
|
# blocks to lend, and never less than ONE MAX_MODEL_LEN sequence for the cache.
|
||||||
FRANK_ARCHON_VRAM_WEIGHTS_MIB=25600
|
# What moves the split from here is HOST_POOL_MIB (which experts stay resident),
|
||||||
FRANK_ARCHON_VRAM_KV_MIB=5120
|
# MAX_MODEL_LEN/MAX_SEQS (the cache bound) and GPU_HEADROOM_MIB. The slab size
|
||||||
|
# the prefill-slope law cares about is a boot-ledger line now, not a knob: read
|
||||||
|
# it after every resize instead of asserting it here.
|
||||||
# fp8 is the ONLY rung below bf16 on this engine: one kv_dtype for the whole
|
# fp8 is the ONLY rung below bf16 on this engine: one kv_dtype for the whole
|
||||||
# cache, so llama.cpp's exact-K/quantized-V split (frank-zealot) is unservable
|
# cache, so llama.cpp's exact-K/quantized-V split (frank-zealot) is unservable
|
||||||
# here. bf16 costs 1.89x fp8, not 2x — the QSA block-key plane is dtype-fixed.
|
# here. bf16 costs 1.89x fp8, not 2x — the QSA block-key plane is dtype-fixed.
|
||||||
@@ -75,19 +91,35 @@ FRANK_ARCHON_KV_DTYPE=fp8
|
|||||||
# 2, and 3 x (1+2) = 9 rows stays inside the wht6 25-row exactness bound.
|
# 2, and 3 x (1+2) = 9 rows stays inside the wht6 25-row exactness bound.
|
||||||
FRANK_ARCHON_SPEC_TOKENS=2
|
FRANK_ARCHON_SPEC_TOKENS=2
|
||||||
FRANK_ARCHON_MAX_BATCHED_TOKENS=2048
|
FRANK_ARCHON_MAX_BATCHED_TOKENS=2048
|
||||||
# Prefix cache tiers. The host tier is sized in ENTRIES and one entry tracks the
|
# Step sharing. A prefill chunk that shares a step with decoding sequences holds
|
||||||
# cache dtype (58.50 KiB at fp8), so a dtype change re-sizes the parked set
|
# the device for the chunk's whole time, and every decode row in that step waits
|
||||||
# without touching these numbers.
|
# that long for its next token (the census: [[LLM Performance Diagnosis]] §9).
|
||||||
FRANK_ARCHON_PREFIX_HOST_MIB=8192
|
# `auto` leaves each decode row behind the chunk a row plus its draft window and
|
||||||
FRANK_ARCHON_PREFIX_DISK_MIB=65536
|
# keeps a share for whatever is waiting to be admitted; a number pins the chunk's
|
||||||
|
# cap; `off` is the pre-1.4 behaviour this endpoint ran for two eras, where the
|
||||||
|
# chunk took the step and admission never ran. Interleave inserts N decode-only
|
||||||
|
# steps after a chunk step taken beside decoders: it protects their cadence and
|
||||||
|
# holds a waiting request out of those steps, so it stays 0 until the post-migration
|
||||||
|
# decode-vs-prefill buckets say it is wanted.
|
||||||
|
FRANK_ARCHON_BUSY_PREFILL_CHUNK=auto
|
||||||
|
FRANK_ARCHON_PREFILL_INTERLEAVE=0
|
||||||
|
# Prefix cache: pinned host memory only. The disk tier was removed outright
|
||||||
|
# (--prefix-cache-dir and --prefix-cache-disk-mib are refusals), so what used to
|
||||||
|
# be restored from NVMe is now re-prefilled. The tier is sized in ENTRIES and one
|
||||||
|
# entry tracks the cache dtype (58.50 KiB at fp8). Watch prefix_hit_rate after
|
||||||
|
# this change: a drop is extra prefill, and prefill is exactly the traffic that
|
||||||
|
# costs decode its cadence.
|
||||||
|
FRANK_ARCHON_PREFIX_CACHE_MIB=8192
|
||||||
# Frank's pin law: the PLE n-gram table serves from pinned host RAM the cards
|
# Frank's pin law: the PLE n-gram table serves from pinned host RAM the cards
|
||||||
# read. `ram` REFUSES the start if memlock is too small — the unit's
|
# read. `ram` REFUSES the start if memlock is too small — the unit's
|
||||||
# LimitMEMLOCK=infinity is the other half of this law.
|
# LimitMEMLOCK=infinity is the other half of this law.
|
||||||
FRANK_ARCHON_NGRAM_PLACEMENT=ram
|
FRANK_ARCHON_NGRAM_PLACEMENT=ram
|
||||||
# TTM preflight. The pinned ledger (~147 GiB) rides TTM's shared pages, and the
|
# TTM preflight. The pinned ledger rides TTM's shared pages, and the kernel
|
||||||
# kernel RESETS pages_limit to half of RAM on every boot; below the plan the
|
# RESETS pages_limit to half of RAM on every boot; below the plan the load dies
|
||||||
# load dies mid-pin with an anonymous allocator error. ttm-preflight.sh raises
|
# mid-pin with an anonymous allocator error. ttm-preflight.sh raises the knob
|
||||||
# the knob itself before refusing by name, so recovery is a boot, not a visit.
|
# itself before refusing by name, so recovery is a boot, not a visit.
|
||||||
|
# Floor = 2 x HOST_POOL + 48,824 (static weights + arena) + PREFIX_CACHE + MNBT
|
||||||
|
# per the bats ledger test; at the 44,928 pool that computes to 148,920 MiB.
|
||||||
FRANK_ARCHON_TTM_MIN_MIB=150000
|
FRANK_ARCHON_TTM_MIN_MIB=150000
|
||||||
FRANK_ARCHON_TTM_TARGET_MIB=204800
|
FRANK_ARCHON_TTM_TARGET_MIB=204800
|
||||||
# Escape hatch for experiments. Unset -> no extra argv; the serve script expands
|
# Escape hatch for experiments. Unset -> no extra argv; the serve script expands
|
||||||
|
|||||||
@@ -27,6 +27,16 @@
|
|||||||
# below stop describing the process you are running. Nothing in this fleet
|
# below stop describing the process you are running. Nothing in this fleet
|
||||||
# pins this container; that is a decision, not an oversight.
|
# pins this container; that is a decision, not an oversight.
|
||||||
#
|
#
|
||||||
|
# THE DEVICE LAW (since the 2026-10-19 Vulkan cutover). Synthesis computes on
|
||||||
|
# the gfx1151 iGPU — the SAME card frank-dragoon serves. It is a shared device,
|
||||||
|
# never a dedicated one, so two numbers matter and neither is what `rocm-smi`
|
||||||
|
# prints first: the memory the container may use is the RADV heap inside the
|
||||||
|
# GTT-backed aperture (the ~2 GiB DRM carve-out is not the working set), and the
|
||||||
|
# timing that matters is dragoon's, measured WITH a synthesis in flight. The
|
||||||
|
# rollback is one line: set QWENTTS_DEVICE empty, and the entrypoint passes no
|
||||||
|
# --device at all — the pre-iGPU CPU shape, no rebuild, no restart of anything
|
||||||
|
# else on the box.
|
||||||
|
#
|
||||||
# THE REGISTRY LAW (why the unit has an ExecStartPost at all). Registrations live
|
# THE REGISTRY LAW (why the unit has an ExecStartPost at all). Registrations live
|
||||||
# in memory only, so a restart loses them, and re-registration alone does NOT warm
|
# in memory only, so a restart loses them, and re-registration alone does NOT warm
|
||||||
# a voice — the warm pass has to synthesize something. Without it the façade
|
# a voice — the warm pass has to synthesize something. Without it the façade
|
||||||
@@ -103,6 +113,17 @@ QWENTTS_LANG=auto
|
|||||||
# round-trip outside it) is what absorbs the queue.
|
# round-trip outside it) is what absorbs the queue.
|
||||||
QWENTTS_MAX_BATCH=1
|
QWENTTS_MAX_BATCH=1
|
||||||
|
|
||||||
|
# The compute device, by ggml device NAME. `Vulkan0` is the gfx1151 iGPU: the
|
||||||
|
# image carries the Mesa (RADV) ICD and no NVIDIA ICD, so this container's
|
||||||
|
# Vulkan enumeration has exactly one entry and it is the iGPU. The unit hands
|
||||||
|
# the container /dev/dri and keeps the render group; on this hybrid host the
|
||||||
|
# iGPU's render node is renderD129 and renderD128 is the NVIDIA card, which is
|
||||||
|
# why the unit passes the directory and lets the driver resolve the node.
|
||||||
|
#
|
||||||
|
# ROLLBACK: an empty value means no --device flag at all — the CPU shape this
|
||||||
|
# endpoint shipped with until 2026-10-19. One line, no rebuild.
|
||||||
|
QWENTTS_DEVICE=Vulkan0
|
||||||
|
|
||||||
# --- the boot warmer (dankclaw/voiceservice/tts-engine-warm.sh) --------------
|
# --- the boot warmer (dankclaw/voiceservice/tts-engine-warm.sh) --------------
|
||||||
# These three are read by the warm hook, not by tts-server. They live here
|
# These three are read by the warm hook, not by tts-server. They live here
|
||||||
# because the hook is exec-ed into this container and therefore inherits this
|
# because the hook is exec-ed into this container and therefore inherits this
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# frank-zealot — llama.cpp (PrismML fork build 10777, CUDA) on the RTX 3090,
|
# frank-zealot — llama.cpp (PrismML fork build 10777, CUDA) on the RTX 3090,
|
||||||
# :7771. Payload: Ternary-Bonsai-2-9B PQ2_0 with the MTP head in the file.
|
# :7771. Payload: Ternary-Bonsai-2-9B PQ2_0 with the MTP head in the file, plus
|
||||||
|
# the refusal-direction adapter applied at load.
|
||||||
#
|
#
|
||||||
# ONE OWNER: shape, payload identity, eras, laws and every measured number
|
# ONE OWNER: shape, payload identity, eras, laws and every measured number
|
||||||
# live in serverAppConfigs-docs/LLM/frank-zealot.md. This file carries the
|
# live in serverAppConfigs-docs/LLM/frank-zealot.md. This file carries the
|
||||||
@@ -13,6 +14,17 @@ MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.g
|
|||||||
# Q8_0 IS MANDATORY: the publisher states the BF16 tower can crash llama-server
|
# Q8_0 IS MANDATORY: the publisher states the BF16 tower can crash llama-server
|
||||||
# under concurrent image requests, which is exactly --parallel 3.
|
# under concurrent image requests, which is exactly --parallel 3.
|
||||||
MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf
|
MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf
|
||||||
|
# Refusal-direction adapter (a rank-1 LoRA, 3.2 MB) applied at load; it edits
|
||||||
|
# the compute graph, it is not a second model. Scale 1 is the card's "held-out
|
||||||
|
# refusals 0 %" point. LORA_FILE="" is the documented opt-out — the plain pack
|
||||||
|
# again, no rebuild — and LORA_SCALE=0 is the same result through the flag.
|
||||||
|
# PROVENANCE ASYMMETRY: the adapter's own prism.abliterate.base_gguf names
|
||||||
|
# Ternary-Bonsai-2-9B-PTQ1_0.gguf as the pack its direction was fitted against,
|
||||||
|
# which is NOT this packing and is not on this host. The tensor names match, so
|
||||||
|
# it applies; the strength was calibrated on a sibling quantization, so the
|
||||||
|
# effect is proven by the endpoint's behaviour, never by the metadata.
|
||||||
|
LORA_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf
|
||||||
|
LORA_SCALE=1
|
||||||
# Served id — the harness/family name, not a fact about the payload.
|
# Served id — the harness/family name, not a fact about the payload.
|
||||||
# Id-keyed consumers: OpenWebUI (model row, access_grant, api_configs), models.yml.
|
# Id-keyed consumers: OpenWebUI (model row, access_grant, api_configs), models.yml.
|
||||||
ALIAS=frank-zealot
|
ALIAS=frank-zealot
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=llama.cpp (PrismML build 10777) Ternary-Bonsai-2-9B PQ2_0-MTP GGUF (qwen35 hybrid, 2x262144 slots over one 524288 pool, unquantized f16 K+V, MTP off, mmproj Q8_0 vision) on the RTX 3090 (dankclaw, frank-zealot)
|
Description=llama.cpp (PrismML build 10777) Ternary-Bonsai-2-9B PQ2_0-MTP GGUF + refusal adapter (qwen35 hybrid, 3x200704 slots over one 602112 pool, f16 K + q8_0 V, MTP armed, mmproj Q8_0 vision) on the RTX 3090 (dankclaw, frank-zealot)
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ def sync_subindex(index: pathlib.Path, vault: pathlib.Path):
|
|||||||
lines = index.read_text().split("\n")
|
lines = index.read_text().split("\n")
|
||||||
out, dropped, refreshed = [], 0, 0
|
out, dropped, refreshed = [], 0, 0
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
# A row whose first cell is empty is a description with no page: the link
|
||||||
|
# was deleted when the page was destroyed, so the row is a dead promise.
|
||||||
|
if re.match(r"^\|\s*\|", line.strip()):
|
||||||
|
dropped += 1
|
||||||
|
continue
|
||||||
for pat, wrap in ((ROW, "| [[{}]] |{} {} |"), (BULLET, "- [[{}]] — {}")):
|
for pat, wrap in ((ROW, "| [[{}]] |{} {} |"), (BULLET, "- [[{}]] — {}")):
|
||||||
m = pat.match(line)
|
m = pat.match(line)
|
||||||
if not m:
|
if not m:
|
||||||
@@ -191,7 +196,7 @@ def main(argv):
|
|||||||
index.write_text("\n".join(out) + "\n")
|
index.write_text("\n".join(out) + "\n")
|
||||||
touched = 0
|
touched = 0
|
||||||
for sub in sorted(vault.rglob("index.md")):
|
for sub in sorted(vault.rglob("index.md")):
|
||||||
if sub == index:
|
if sub == index or ".AGENTTRASH" in str(sub):
|
||||||
continue
|
continue
|
||||||
dropped, refreshed, added = sync_subindex(sub, vault)
|
dropped, refreshed, added = sync_subindex(sub, vault)
|
||||||
if dropped or refreshed or added:
|
if dropped or refreshed or added:
|
||||||
|
|||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Repair the two artifacts the destruction passes introduced.
|
||||||
|
|
||||||
|
1. The link stripper could eat the space after a `- ` marker (`-`X``) and leave a
|
||||||
|
dangling `see .` where a wikilink used to be.
|
||||||
|
2. The Eras collapse could swallow a table's HEADER row (it looked like just
|
||||||
|
another non-current row), leaving a separator with no header above it.
|
||||||
|
|
||||||
|
Usage: repair-damage.py [--apply]
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
VAULT = pathlib.Path("serverAppConfigs-docs")
|
||||||
|
ERAS_HEADER = "| Window | Payload | Runtime | Shape | Replaced because | Rollback |"
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
apply = "--apply" in argv
|
||||||
|
marker = dangling = header = 0
|
||||||
|
for p in sorted(VAULT.rglob("*.md")):
|
||||||
|
s = str(p)
|
||||||
|
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||||
|
continue
|
||||||
|
lines = p.read_text(errors="replace").split("\n")
|
||||||
|
out = []
|
||||||
|
# Only a pointer whose target was deleted reads as `… see .` — never touch
|
||||||
|
# a line merely because it wraps on a comma. (An earlier version of this
|
||||||
|
# rule stripped the trailing comma off 660 wrapped prose lines, which
|
||||||
|
# `scripts/restore-punctuation.py` then had to undo from HEAD.)
|
||||||
|
for line in lines:
|
||||||
|
new = line
|
||||||
|
if re.match(r"^(\s*)[-*](?![-* ])[$`[(*_]", new):
|
||||||
|
new = re.sub(r"^(\s*)[-*]", r"\1- ", new)
|
||||||
|
marker += 1
|
||||||
|
if re.search(r"\s(?:see|also|details in|via|per)\s*\.?\s*$", new):
|
||||||
|
new = re.sub(r"\s(?:see|also|details in|via|per)\s*\.?\s*$", "", new)
|
||||||
|
dangling += 1
|
||||||
|
out.append(new)
|
||||||
|
# A separator row whose line above is not a header gets its header back.
|
||||||
|
fixed = []
|
||||||
|
for i, line in enumerate(out):
|
||||||
|
if re.match(r"^\|-+", line.strip()) and (not fixed or not fixed[-1].strip().startswith("|")):
|
||||||
|
fixed.append(ERAS_HEADER)
|
||||||
|
header += 1
|
||||||
|
fixed.append(line)
|
||||||
|
if apply and "".join(fixed) != "".join(lines):
|
||||||
|
p.write_text("\n".join(fixed))
|
||||||
|
print(f"{'APPLIED' if apply else 'dry run'}: marker spacing {marker}, "
|
||||||
|
f"dangling pointers {dangling}, restored table headers {header}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Undo trailing-punctuation damage by restoring lines that match HEAD exactly
|
||||||
|
except for a stripped trailing `,` / `;` / removed trailing word.
|
||||||
|
|
||||||
|
The repair pass over-reached: its "dangling pointer" cleanup also matched every
|
||||||
|
wrapped prose line ending in a comma, and deleted the comma. A line whose text
|
||||||
|
is otherwise byte-identical to its HEAD version, differing only by trailing
|
||||||
|
punctuation, is not an edit — it is damage, so HEAD's line goes back.
|
||||||
|
|
||||||
|
Usage: restore-punctuation.py [--apply]
|
||||||
|
"""
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
REPO = pathlib.Path(".")
|
||||||
|
VAULT = REPO / "serverAppConfigs-docs"
|
||||||
|
|
||||||
|
|
||||||
|
def head_text(rel):
|
||||||
|
r = subprocess.run(["git", "show", f"HEAD:{rel}"], capture_output=True, text=True)
|
||||||
|
return r.stdout if r.returncode == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
apply = "--apply" in argv
|
||||||
|
restored = 0
|
||||||
|
pages = 0
|
||||||
|
for p in sorted(VAULT.rglob("*.md")):
|
||||||
|
s = str(p)
|
||||||
|
if ".AGENTTRASH" in s or ".obsidian" in s:
|
||||||
|
continue
|
||||||
|
rel = s
|
||||||
|
head = head_text(rel)
|
||||||
|
if head is None:
|
||||||
|
continue
|
||||||
|
cur = p.read_text(errors="replace").split("\n")
|
||||||
|
# Key on (length, prefix) of the HEAD line minus trailing punctuation so a
|
||||||
|
# damaged line can find its original.
|
||||||
|
index = {}
|
||||||
|
for hl in head.split("\n"):
|
||||||
|
h = hl.rstrip()
|
||||||
|
if h.endswith((",", ";")) or True:
|
||||||
|
key = (len(h.rstrip(",;. ")), h.rstrip(",;. ")[:60])
|
||||||
|
index.setdefault(key, h)
|
||||||
|
touched = 0
|
||||||
|
for i, cl in enumerate(cur):
|
||||||
|
c = cl.rstrip()
|
||||||
|
if not c or c.endswith((".", ":", "|", "`", ")", "," , ";")):
|
||||||
|
continue
|
||||||
|
key = (len(c), c[:60])
|
||||||
|
hl = index.get(key)
|
||||||
|
if hl and hl != c and hl.startswith(c):
|
||||||
|
cur[i] = hl
|
||||||
|
touched += 1
|
||||||
|
if touched:
|
||||||
|
pages += 1
|
||||||
|
restored += touched
|
||||||
|
if apply:
|
||||||
|
p.write_text("\n".join(cur))
|
||||||
|
print(f"{'APPLIED' if apply else 'dry run'}: {restored} lines restored across {pages} pages")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
@@ -15,12 +15,13 @@
|
|||||||
# log states every budget before anything is allocated (weights vs KV
|
# 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,
|
# split, the expert host pool's coverage of the worst case, the PLE pin,
|
||||||
# the P2P answer per pair) — read it after every resize.
|
# the P2P answer per pair) — read it after every resize.
|
||||||
# Deployed shape (2026-10-13): THREE decode slots x 200,000 tokens, fp8
|
# Deployed shape (2026-10-19, radiance 1.8.1): THREE decode slots x 200,000
|
||||||
# (quantized) KV in a 5,120 MiB pool (contract 3 x 200,000 + 1.3 %) and a
|
# tokens, fp8 KV, 44 GiB/rank pinned expert pool, MTP depth 2, prefix cache in
|
||||||
# 25,600 MiB resident expert slab — 30,720 MiB of the 30,969 MiB the engine's
|
# pinned host RAM, PLE pinned in RAM. The per-card split between the expert slab
|
||||||
# own budget table makes claimable, spent on the slab because prefill is
|
# and the KV pool is NO LONGER an operator number — 1.3.0 removed the VRAM pair
|
||||||
# slab-limited. 44 GiB/rank pinned expert pool, MTP depth 2, prefix cache host
|
# and the budget resolver derives it from headroom, the cache bound and the host
|
||||||
# 8 GiB + disk 64 GiB, PLE pinned in RAM.
|
# pool, so the slab size and the pool's token count are LEDGER lines: after every
|
||||||
|
# boot read them out of the log and check the pool still backs 3 x 200,000.
|
||||||
#
|
#
|
||||||
# The root unit (scripts/frank-archon.service, User=dank,
|
# The root unit (scripts/frank-archon.service, User=dank,
|
||||||
# LimitMEMLOCK=infinity) grants the pin budget: 2 x 44 GiB expert pools +
|
# LimitMEMLOCK=infinity) grants the pin budget: 2 x 44 GiB expert pools +
|
||||||
@@ -44,7 +45,7 @@ set +a
|
|||||||
: "${FRANK_ARCHON_IMAGE:?FRANK_ARCHON_IMAGE must name the radiance image}"
|
: "${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_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_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_STATE_DIR:?FRANK_ARCHON_STATE_DIR must name the host dir holding the rad-tune cache}"
|
||||||
: "${FRANK_ARCHON_CONTAINER:?FRANK_ARCHON_CONTAINER must name the container}"
|
: "${FRANK_ARCHON_CONTAINER:?FRANK_ARCHON_CONTAINER must name the container}"
|
||||||
: "${FRANK_ARCHON_SERVED_NAME:?FRANK_ARCHON_SERVED_NAME must name the alias}"
|
: "${FRANK_ARCHON_SERVED_NAME:?FRANK_ARCHON_SERVED_NAME must name the alias}"
|
||||||
: "${FRANK_ARCHON_PORT:?FRANK_ARCHON_PORT must be the port}"
|
: "${FRANK_ARCHON_PORT:?FRANK_ARCHON_PORT must be the port}"
|
||||||
@@ -56,13 +57,11 @@ set +a
|
|||||||
: "${FRANK_ARCHON_PLACEMENT:?FRANK_ARCHON_PLACEMENT 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_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_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_BUSY_PREFILL_CHUNK:?FRANK_ARCHON_BUSY_PREFILL_CHUNK must be set (auto|off|N)}"
|
||||||
: "${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_KV_DTYPE:?FRANK_ARCHON_KV_DTYPE must be set}"
|
||||||
: "${FRANK_ARCHON_SPEC_TOKENS:?FRANK_ARCHON_SPEC_TOKENS 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_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_CACHE_MIB:?FRANK_ARCHON_PREFIX_CACHE_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_NGRAM_PLACEMENT:?FRANK_ARCHON_NGRAM_PLACEMENT must be set}"
|
||||||
: "${FRANK_ARCHON_TTM_MIN_MIB:?FRANK_ARCHON_TTM_MIN_MIB must be set}"
|
: "${FRANK_ARCHON_TTM_MIN_MIB:?FRANK_ARCHON_TTM_MIN_MIB must be set}"
|
||||||
: "${FRANK_ARCHON_TTM_TARGET_MIB:?FRANK_ARCHON_TTM_TARGET_MIB must be set}"
|
: "${FRANK_ARCHON_TTM_TARGET_MIB:?FRANK_ARCHON_TTM_TARGET_MIB must be set}"
|
||||||
@@ -103,19 +102,12 @@ if [ ! -f "$FRANK_ARCHON_TEMPLATE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
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"
|
mkdir -p "$FRANK_ARCHON_STATE_DIR/tune"
|
||||||
# rad-tune's kernel-selection cache (see the runbook's "Tuning levers"):
|
# rad-tune's kernel-selection cache (see the runbook's "Tuning levers"):
|
||||||
# the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune at boot;
|
# the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune at boot;
|
||||||
# $RADIANCE_HOME defaults to /opt/radiance, which lives in the image, so
|
# $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
|
# 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
|
# that wrote it. The engine's create is not recursive, so the dir is pre-created.
|
||||||
# (the engine's create is not recursive).
|
|
||||||
|
|
||||||
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
|
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
|
||||||
echo "ERROR: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
|
echo "ERROR: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
|
||||||
@@ -148,6 +140,15 @@ done
|
|||||||
LOG=/home/dank/logs/frank_archon.log
|
LOG=/home/dank/logs/frank_archon.log
|
||||||
mkdir -p "$(dirname "$LOG")"
|
mkdir -p "$(dirname "$LOG")"
|
||||||
|
|
||||||
|
# Interleave is opt-in: 0 is the engine's default and passing it anyway would put
|
||||||
|
# a flag in argv that changes nothing but the record. The sharing cap is always
|
||||||
|
# explicit because `auto` vs `off` is the difference between decode waiting one
|
||||||
|
# chunk and waiting the whole step.
|
||||||
|
PREFILL_ARGS=()
|
||||||
|
if [ "${FRANK_ARCHON_PREFILL_INTERLEAVE:-0}" -gt 0 ]; then
|
||||||
|
PREFILL_ARGS=(--prefill-interleave "$FRANK_ARCHON_PREFILL_INTERLEAVE")
|
||||||
|
fi
|
||||||
|
|
||||||
# Publisher-vetted container contract (docs/DOCKER.md, compose/common.yaml):
|
# Publisher-vetted container contract (docs/DOCKER.md, compose/common.yaml):
|
||||||
# --network host (the server binds the host's addresses directly, no NAT),
|
# --network host (the server binds the host's addresses directly, no NAT),
|
||||||
# --init (the engine releases the cards on SIGTERM), seccomp=unconfined
|
# --init (the engine releases the cards on SIGTERM), seccomp=unconfined
|
||||||
@@ -156,8 +157,9 @@ mkdir -p "$(dirname "$LOG")"
|
|||||||
# makes for host memory), memlock unbounded (the pinned ledger — legal
|
# makes for host memory), memlock unbounded (the pinned ledger — legal
|
||||||
# because the root unit grants LimitMEMLOCK=infinity to this dank process),
|
# because the root unit grants LimitMEMLOCK=infinity to this dank process),
|
||||||
# /dev/kfd + /dev/dri (render/video groups are moot for a root container).
|
# /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 template rides a single-file mount and the .rad dir is ro. The prefix cache
|
||||||
# the prefix-cache disk tier.
|
# has no disk tier any more, so the only host state this container writes is the
|
||||||
|
# rad-tune cache under /opt/radiance/share/radiance/tune.
|
||||||
podman run -d --name "$FRANK_ARCHON_CONTAINER" \
|
podman run -d --name "$FRANK_ARCHON_CONTAINER" \
|
||||||
--network host \
|
--network host \
|
||||||
--init \
|
--init \
|
||||||
@@ -165,7 +167,6 @@ podman run -d --name "$FRANK_ARCHON_CONTAINER" \
|
|||||||
--ulimit memlock=-1:-1 \
|
--ulimit memlock=-1:-1 \
|
||||||
--device /dev/kfd --device /dev/dri \
|
--device /dev/kfd --device /dev/dri \
|
||||||
-v "$FRANK_ARCHON_MODEL_DIR":/models:ro \
|
-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_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
|
||||||
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
-v "$FRANK_ARCHON_TEMPLATE":/templates/template.jinja:ro \
|
||||||
"$FRANK_ARCHON_IMAGE" \
|
"$FRANK_ARCHON_IMAGE" \
|
||||||
@@ -179,15 +180,13 @@ podman run -d --name "$FRANK_ARCHON_CONTAINER" \
|
|||||||
--placement "$FRANK_ARCHON_PLACEMENT" \
|
--placement "$FRANK_ARCHON_PLACEMENT" \
|
||||||
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
--host-pool-mib "$FRANK_ARCHON_HOST_POOL_MIB" \
|
||||||
--gpu-headroom-mib "$FRANK_ARCHON_GPU_HEADROOM_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" \
|
--kv-cache-dtype "$FRANK_ARCHON_KV_DTYPE" \
|
||||||
--prefix-cache-host-mib "$FRANK_ARCHON_PREFIX_HOST_MIB" \
|
--prefix-cache-mib "$FRANK_ARCHON_PREFIX_CACHE_MIB" \
|
||||||
--prefix-cache-dir /data/kvcache/frank-archon \
|
|
||||||
--prefix-cache-disk-mib "$FRANK_ARCHON_PREFIX_DISK_MIB" \
|
|
||||||
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
--ngram-placement "$FRANK_ARCHON_NGRAM_PLACEMENT" \
|
||||||
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
--num-speculative-tokens "$FRANK_ARCHON_SPEC_TOKENS" \
|
||||||
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
--max-num-batched-tokens "$FRANK_ARCHON_MAX_BATCHED_TOKENS" \
|
||||||
|
--busy-prefill-chunk "$FRANK_ARCHON_BUSY_PREFILL_CHUNK" \
|
||||||
|
"${PREFILL_ARGS[@]}" \
|
||||||
${FRANK_ARCHON_EXTRA_ARGS:+$FRANK_ARCHON_EXTRA_ARGS} \
|
${FRANK_ARCHON_EXTRA_ARGS:+$FRANK_ARCHON_EXTRA_ARGS} \
|
||||||
> "$LOG" 2>&1
|
> "$LOG" 2>&1
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,19 @@
|
|||||||
# what gives this endpoint eyes; MMPROJ_FILE="" is the documented text-only
|
# what gives this endpoint eyes; MMPROJ_FILE="" is the documented text-only
|
||||||
# opt-out.
|
# opt-out.
|
||||||
#
|
#
|
||||||
|
# A refusal-direction adapter rides on top of the pack (Frank, 2026-10-19):
|
||||||
|
# AtomicChat's rank-1 LoRA, applied at load by --lora-scaled. It is an edit
|
||||||
|
# inside the compute graph, not a second model — B = -r (the refusal direction),
|
||||||
|
# A built from the base pack's folded input basis, every transformer layer, the
|
||||||
|
# embedding deliberately untouched because an embedding LoRA crashes this fork's
|
||||||
|
# graph. The card verified it on prism-b10777-e3de715 — the build pinned here —
|
||||||
|
# and says older forks refuse the pack outright (`unsupported
|
||||||
|
# prism.hadamard.version: 2`), which makes the runtime lock doubly load-bearing.
|
||||||
|
# Its own metadata names the PTQ1_0 packing as the pack the direction was fitted
|
||||||
|
# against, so what proves the effect is a refusal probe against the live
|
||||||
|
# endpoint, not the metadata and not the card's table. LORA_FILE="" drops the
|
||||||
|
# flag and the endpoint is the plain pack again.
|
||||||
|
#
|
||||||
# Speculative decoding ships ON (Frank, 2026-10-19). The head is IN the file and
|
# Speculative decoding ships ON (Frank, 2026-10-19). The head is IN the file and
|
||||||
# accepts well (0.798/0.617 per position, mean length 2.41, measured 2026-10-18);
|
# accepts well (0.798/0.617 per position, mean length 2.41, measured 2026-10-18);
|
||||||
# what made it lose that evening was the bill, not the drafts — the draft context
|
# what made it lose that evening was the bill, not the drafts — the draft context
|
||||||
@@ -136,6 +149,13 @@ fi
|
|||||||
# Vision projector. No colon on purpose: an explicitly empty value means
|
# Vision projector. No colon on purpose: an explicitly empty value means
|
||||||
# "text-only, on purpose" and must NOT be replaced by the default path.
|
# "text-only, on purpose" and must NOT be replaced by the default path.
|
||||||
: "${MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}"
|
: "${MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}"
|
||||||
|
# Refusal adapter. No colon on purpose, the same convention as MMPROJ_FILE: an
|
||||||
|
# explicitly empty LORA_FILE is the documented opt-out (the plain pack again)
|
||||||
|
# and must NOT be replaced by the default path. The scale travels INSIDE the
|
||||||
|
# same argv token as the file, so LORA_SCALE only matters while LORA_FILE names
|
||||||
|
# something; 0 is the card's "switch it off without touching the file" value.
|
||||||
|
: "${LORA_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf}"
|
||||||
|
: "${LORA_SCALE:=1}"
|
||||||
|
|
||||||
# Fail loudly rather than half-loading: a missing file is a download that
|
# 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
|
# has not finished, and Restart=always would turn it into a crash loop that
|
||||||
@@ -149,6 +169,17 @@ if [[ -n "$MMPROJ_FILE" ]]; then
|
|||||||
EXTRA_ARGS+=(--mmproj "$MMPROJ_FILE")
|
EXTRA_ARGS+=(--mmproj "$MMPROJ_FILE")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The adapter reaches the engine as ONE token — --lora-scaled <file>:<scale> —
|
||||||
|
# so a scale can never arrive without its file. It is applied once, at load;
|
||||||
|
# nothing here hot-swaps it (--lora-init-without-apply plus POST /lora-adapters
|
||||||
|
# is that shape, and a process holding three conversations is not the place to
|
||||||
|
# re-parameterise weights mid-flight). A missing file is a fetch that never
|
||||||
|
# finished, so refuse rather than let Restart=always loop on it.
|
||||||
|
if [[ -n "$LORA_FILE" ]]; then
|
||||||
|
[[ -e "$LORA_FILE" ]] || { echo "frank-zealot: missing $LORA_FILE" >&2; exit 1; }
|
||||||
|
EXTRA_ARGS+=(--lora-scaled "${LORA_FILE}:${LORA_SCALE}")
|
||||||
|
fi
|
||||||
|
|
||||||
# Prometheus scraping is opt-IN in llama.cpp: without --metrics the server
|
# Prometheus scraping is opt-IN in llama.cpp: without --metrics the server
|
||||||
# answers 501 on /metrics, and the fleet scrape then reports the endpoint DOWN.
|
# answers 501 on /metrics, and the fleet scrape then reports the endpoint DOWN.
|
||||||
if [[ "$METRICS" = "1" ]]; then
|
if [[ "$METRICS" = "1" ]]; then
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Damage catalogue:
|
|||||||
no longer frontmatter (title/tags/description become body text).
|
no longer frontmatter (title/tags/description become body text).
|
||||||
2. A rewrite **appended** instead of merged → duplicate headings and a stray
|
2. A rewrite **appended** instead of merged → duplicate headings and a stray
|
||||||
second `---` mid-file.
|
second `---` mid-file.
|
||||||
3. Literal model artifacts in the body: `<|channel>}<tool_call|>...`,
|
3. Literal model artifacts in the body: `<|channel>}<tool_call|>...`
|
||||||
`+- `, `- - `.
|
`+- `, `- - `.
|
||||||
4. Wrapped bullets re-emitted with a `- ` prefix on every physical line.
|
4. Wrapped bullets re-emitted with a `- ` prefix on every physical line.
|
||||||
5. Line-range replacement that dropped a sentence tail → dangling fragment,
|
5. Line-range replacement that dropped a sentence tail → dangling fragment,
|
||||||
@@ -46,7 +46,7 @@ Repair order (matters):
|
|||||||
2. If it merely duplicated: **salvage unique content first** — cut the
|
2. If it merely duplicated: **salvage unique content first** — cut the
|
||||||
one-of-a-kind section, paste it into the copy you keep, THEN delete the
|
one-of-a-kind section, paste it into the copy you keep, THEN delete the
|
||||||
duplicate ranges. Never delete first.
|
duplicate ranges. Never delete first.
|
||||||
3. Re-add missing sentence tails (insert before the orphaned fragment),
|
3. Re-add missing sentence tails (insert before the orphaned fragment)
|
||||||
keeping list continuation lines indented.
|
keeping list continuation lines indented.
|
||||||
4. Re-run all checks above.
|
4. Re-run all checks above.
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ safe, but its snapshot tag changes on every edit — re-read before each
|
|||||||
subsequent edit. `.AGENTTRASH/` is gitignored (`.gitignore:25`), so NEW trash
|
subsequent edit. `.AGENTTRASH/` is gitignored (`.gitignore:25`), so NEW trash
|
||||||
stays local — but do not read that as "the repo has none of it": on 2026-10-18
|
stays local — but do not read that as "the repo has none of it": on 2026-10-18
|
||||||
`git ls-files | grep -c AGENTTRASH` returns **37** tracked paths
|
`git ls-files | grep -c AGENTTRASH` returns **37** tracked paths
|
||||||
(`.AGENTTRASH/`, `scripts/.AGENTTRASH/`, `dankserver/.AGENTTRASH/`,
|
(`.AGENTTRASH/`, `scripts/.AGENTTRASH/`, `dankserver/.AGENTTRASH/`
|
||||||
`tests/.AGENTTRASH/`), because gitignore never untracks what was committed
|
`tests/.AGENTTRASH/`), because gitignore never untracks what was committed
|
||||||
before the rule. Trash is a temporary hold, not an archive.
|
before the rule. Trash is a temporary hold, not an archive.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ this file is repo-managed; edits are harness-side only.
|
|||||||
**Model ids are the StarCraft unit names (official 2026-10-08, on the wire
|
**Model ids are the StarCraft unit names (official 2026-10-08, on the wire
|
||||||
since 2026-10-09):** `frank-archon` (Dankserver), `frank-dragoon` and
|
since 2026-10-09):** `frank-archon` (Dankserver), `frank-dragoon` and
|
||||||
`frank-zealot` (Dankclaw) — engine, port and card behind each id live on
|
`frank-zealot` (Dankclaw) — engine, port and card behind each id live on
|
||||||
[[frank-archon]], [[frank-dragoon]] and [[frank-zealot]] — formerly `frank-archon`, `frank-dragoon`,
|
[[frank-archon]], [[frank-dragoon]] and [[frank-zealot]] — formerly `frank-archon`, `frank-dragoon`
|
||||||
`frank-zealot`. The SERVICES keep the old names (units, scripts, env
|
`frank-zealot`. The SERVICES keep the old names (units, scripts, env
|
||||||
files, runbook pages); pages written in the old era name the service,
|
files, runbook pages); pages written in the old era name the service,
|
||||||
the harness names the model. The wire now matches: each endpoint's
|
the harness names the model. The wire now matches: each endpoint's
|
||||||
@@ -26,7 +26,7 @@ picker/grants, the homepage docs and the raw API all use one id per model
|
|||||||
note on the endpoint echoing the requested name).
|
note on the endpoint echoing the requested name).
|
||||||
|
|
||||||
Verified 2026-10-18: `/v1/models` on 192.168.0.235:7770, 192.168.0.143:7770 and
|
Verified 2026-10-18: `/v1/models` on 192.168.0.235:7770, 192.168.0.143:7770 and
|
||||||
192.168.0.143:7771 each answer with the unit name (`frank-archon`,
|
192.168.0.143:7771 each answer with the unit name (`frank-archon`
|
||||||
`frank-dragoon`, `frank-zealot`).
|
`frank-dragoon`, `frank-zealot`).
|
||||||
|
|
||||||
## Compaction chain arithmetic (verify before ANY window change)
|
## Compaction chain arithmetic (verify before ANY window change)
|
||||||
@@ -40,11 +40,11 @@ breaks compaction.
|
|||||||
Where the contract lives:
|
Where the contract lives:
|
||||||
|
|
||||||
- `~/.omp/agent/config.yml` → `compaction.remoteEndpoint` (target URL:
|
- `~/.omp/agent/config.yml` → `compaction.remoteEndpoint` (target URL:
|
||||||
`http://192.168.0.143:7771/v1/chat/completions`, `config.yml:76`),
|
`http://192.168.0.143:7771/v1/chat/completions`, `config.yml:76`)
|
||||||
`methodOrder` (`shake → snapcompact → soft → handoff → remote`,
|
`methodOrder` (`shake → snapcompact → soft → handoff → remote`
|
||||||
`config.yml:77-82`), `thresholdPercent` (**95**, `config.yml:84`), idle
|
`config.yml:77-82`), `thresholdPercent` (**95**, `config.yml:84`), idle
|
||||||
settings (`idleEnabled`, `idleThresholdTokens: 100000`)
|
settings (`idleEnabled`, `idleThresholdTokens: 100000`)
|
||||||
- `~/.omp/agent/models.yml` → per-model `contextWindow`, `maxTokens`,
|
- `~/.omp/agent/models.yml` → per-model `contextWindow`, `maxTokens`
|
||||||
`compactionModel` references
|
`compactionModel` references
|
||||||
- Implementation: omp plugin
|
- Implementation: omp plugin
|
||||||
`~/.omp/plugins/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js`
|
`~/.omp/plugins/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js`
|
||||||
@@ -104,16 +104,16 @@ input. Boot-shape OOM laddering is a different problem
|
|||||||
## Per-model sampler config (`compat.extraBody`)
|
## Per-model sampler config (`compat.extraBody`)
|
||||||
|
|
||||||
`models.yml` has **no per-model sampler keys**. The global knobs are
|
`models.yml` has **no per-model sampler keys**. The global knobs are
|
||||||
`config.yml`'s `temperature`, `topP`, `topK`, `minP`, `presencePenalty`,
|
`config.yml`'s `temperature`, `topP`, `topK`, `minP`, `presencePenalty`
|
||||||
`repetitionPenalty`, and a knob reaches the wire only when it is set — the
|
`repetitionPenalty`, and a knob reaches the wire only when it is set — the
|
||||||
bundle guards each one with `!== void 0` (e.g. `minP!==void 0)e.min_p=t.minP`),
|
bundle guards each one with `!== void 0` (e.g. `minP!==void 0)e.min_p=t.minP`)
|
||||||
and this workstation's `config.yml` sets **none** of them. The 2026-10-05 note
|
and this workstation's `config.yml` sets **none** of them. The 2026-10-05 note
|
||||||
that the unset default is `-1`/"not sent" is retired: there is no `-1`
|
that the unset default is `-1`/"not sent" is retired: there is no `-1`
|
||||||
sentinel, an unset key is simply absent. The per-model hook is
|
sentinel, an unset key is simply absent. The per-model hook is
|
||||||
`compat.extraBody`:
|
`compat.extraBody`:
|
||||||
|
|
||||||
**Why it works:** `applyOpenAIExtraBody` runs **last** in the
|
**Why it works:** `applyOpenAIExtraBody` runs **last** in the
|
||||||
openai-completions builder — after `applyChatCompletionsCompatPolicy` — via
|
openai-completions builder — after `applyChatCompletionsCompatPolicy` —
|
||||||
`Object.assign(params, extraBody)`. So extraBody overrides global settings
|
`Object.assign(params, extraBody)`. So extraBody overrides global settings
|
||||||
and compat-policy output.
|
and compat-policy output.
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ assistant turn carrying a long `reasoning_content`, then compare
|
|||||||
`usage.prompt_tokens` with the flag true vs false. A large delta means the
|
`usage.prompt_tokens` with the flag true vs false. A large delta means the
|
||||||
field changed template rendering; zero delta means it was ignored.
|
field changed template rendering; zero delta means it was ignored.
|
||||||
|
|
||||||
Cleanup: trash the probe profile and echo server (`profiles/.AGENTTRASH`),
|
Cleanup: trash the probe profile and echo server (`profiles/.AGENTTRASH`)
|
||||||
never `rm`.
|
never `rm`.
|
||||||
|
|
||||||
## Static-preamble audit ("the context feels bloated")
|
## Static-preamble audit ("the context feels bloated")
|
||||||
@@ -205,7 +205,7 @@ What loads statically every session (measure each, don't guess):
|
|||||||
```bash
|
```bash
|
||||||
python3 - <<'EOF'
|
python3 - <<'EOF'
|
||||||
import glob, re, os
|
import glob, re, os
|
||||||
for root in [os.path.expanduser("~/.omp/agent/managed-skills"),
|
for root in [os.path.expanduser("~/.omp/agent/managed-skills")
|
||||||
os.path.expanduser("~/.omp/agent/skills"), ".omp/skills"]:
|
os.path.expanduser("~/.omp/agent/skills"), ".omp/skills"]:
|
||||||
n=tot=0
|
n=tot=0
|
||||||
for sk in glob.glob(root+"/*/SKILL.md"):
|
for sk in glob.glob(root+"/*/SKILL.md"):
|
||||||
@@ -305,4 +305,4 @@ standalone; fold go/no-go arithmetic skills into their probe skill
|
|||||||
- [[Agent Docs and Edits]] — editing vault docs and hashline targets safely
|
- [[Agent Docs and Edits]] — editing vault docs and hashline targets safely
|
||||||
- [[Command Center Dotfiles]] — the `~/.omp` repo deploy this config rides
|
- [[Command Center Dotfiles]] — the `~/.omp` repo deploy this config rides
|
||||||
- [[Llama Cpp Serving Sizing]] — the engine-side sizing the chain must fit
|
- [[Llama Cpp Serving Sizing]] — the engine-side sizing the chain must fit
|
||||||
- [[LLM Bench Windows and Measurement]] — measuring the endpoints this config routes
|
- measuring the endpoints this config routes
|
||||||
@@ -9,4 +9,4 @@ docs get edited safely, and how the agent's home is deployed.
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [LLM](../LLM/index.md) — the fleet these agents operate
|
- the fleet these agents operate
|
||||||
@@ -47,7 +47,7 @@ network namespace**:
|
|||||||
|
|
||||||
`Network=` values are read from the live Quadlet sources in
|
`Network=` values are read from the live Quadlet sources in
|
||||||
`/home/dank/.config/containers/systemd/*.container`; runtime network names on the
|
`/home/dank/.config/containers/systemd/*.container`; runtime network names on the
|
||||||
host at the same date were `podman`, `dankclaw_default`, `systemd-gitea`,
|
host at the same date were `podman`, `dankclaw_default`, `systemd-gitea`
|
||||||
`systemd-searxng`, `systemd-open-webui`.
|
`systemd-searxng`, `systemd-open-webui`.
|
||||||
|
|
||||||
Two consequences that are easy to miss:
|
Two consequences that are easy to miss:
|
||||||
@@ -83,7 +83,7 @@ podman exec <name> cat /proc/net/route # default route → gateway IP
|
|||||||
podman exec <name> sh -c 'timeout 2 bash -c "exec 3<>/dev/tcp/10.89.1.1/8080"; echo rc=$?'
|
podman exec <name> sh -c 'timeout 2 bash -c "exec 3<>/dev/tcp/10.89.1.1/8080"; echo rc=$?'
|
||||||
```
|
```
|
||||||
|
|
||||||
Verified outcomes of that recipe on 2026-09-02: from inside `hermes-dashboard`,
|
Verified outcomes of that recipe on 2026-09-02: from inside `hermes-dashboard`
|
||||||
`10.89.1.1:3000` (Gitea, published), `:8081` (SearXNG via Gluetun's netns,
|
`10.89.1.1:3000` (Gitea, published), `:8081` (SearXNG via Gluetun's netns,
|
||||||
published) and `:9119` (its own published port) connect; `:8080` and `:8787`
|
published) and `:9119` (its own published port) connect; `:8080` and `:8787`
|
||||||
are **refused instantly** even though both listeners are bound to `0.0.0.0` in
|
are **refused instantly** even though both listeners are bound to `0.0.0.0` in
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ possibly zero behavioral change.
|
|||||||
2. Probe the target (ssh as `dank`, passwordless sudo present):
|
2. Probe the target (ssh as `dank`, passwordless sudo present):
|
||||||
- `cat ~/.local/state/serverAppConfigs/deploy-state/<svc>`
|
- `cat ~/.local/state/serverAppConfigs/deploy-state/<svc>`
|
||||||
- `git -C ~/serverAppConfigs rev-parse HEAD`
|
- `git -C ~/serverAppConfigs rev-parse HEAD`
|
||||||
- `systemctl is-active/is-enabled <svc>`;
|
- `systemctl is-active/is-enabled <svc>`
|
||||||
`curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/health`
|
`curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:<port>/health`
|
||||||
- the on-host env file already carries the new keys (it landed via the
|
- the on-host env file already carries the new keys (it landed via the
|
||||||
ff-pull).
|
ff-pull).
|
||||||
@@ -119,8 +119,8 @@ each, kill stale ones by explicit PID.
|
|||||||
A killed deploy leaves behind:
|
A killed deploy leaves behind:
|
||||||
|
|
||||||
- the unit **stopped/failed** (`Restart=always` does NOT fire after an
|
- the unit **stopped/failed** (`Restart=always` does NOT fire after an
|
||||||
explicit stop — start it yourself);
|
explicit stop — start it yourself)
|
||||||
- the state marker **pending** (pre-deploy or in-flight revision);
|
- the state marker **pending** (pre-deploy or in-flight revision)
|
||||||
- the e2e gate un-run (the deployer runs it last).
|
- the e2e gate un-run (the deployer runs it last).
|
||||||
|
|
||||||
Reconcile the marker ONLY after the service itself is verified healthy — a
|
Reconcile the marker ONLY after the service itself is verified healthy — a
|
||||||
@@ -142,11 +142,11 @@ Boot failures must be read from the journal, not `podman logs`: serve scripts
|
|||||||
numbers — which looks exactly like your change having no effect. Key on
|
numbers — which looks exactly like your change having no effect. Key on
|
||||||
container identity (`podman ps -a --format '{{.CreatedAt}}'`) and read the
|
container identity (`podman ps -a --format '{{.CreatedAt}}'`) and read the
|
||||||
cause from `journalctl --since "N min ago" | grep -a "<container-name>\["`.
|
cause from `journalctl --since "N min ago" | grep -a "<container-name>\["`.
|
||||||
See [[vLLM Boot Forensics and Config Interrogation]] for the full capture
|
See for the full capture
|
||||||
procedure.
|
procedure.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[Deployment Flow]] — the deploy pipeline itself
|
- [[Deployment Flow]] — the deploy pipeline itself
|
||||||
- [[Quadlet Lifecycle]] — unit ownership and drift diagnosis
|
- [[Quadlet Lifecycle]] — unit ownership and drift diagnosis
|
||||||
- [[vLLM Boot Forensics and Config Interrogation]] — journal capture
|
- journal capture
|
||||||
@@ -68,7 +68,7 @@ service on the other host.
|
|||||||
| 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. |
|
| 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. |
|
||||||
|
|
||||||
The shared chat template maps to the inference service on the selected target.
|
The shared chat template maps to the inference service on the selected target.
|
||||||
The `frank-npu` endpoint is retired ([[frank-npu]]) and has no deploy handler;
|
The `frank-npu` endpoint is retired () and has no deploy handler
|
||||||
an unrelated large-model deployment never touches it.
|
an unrelated large-model deployment never touches it.
|
||||||
|
|
||||||
## Caddy safety
|
## Caddy safety
|
||||||
@@ -125,6 +125,5 @@ routing. See [[Testing Policy]].
|
|||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[Testing Policy]]
|
- [[Testing Policy]]
|
||||||
- [LLM deployment](../LLM/deployment.md)
|
|
||||||
- [[Caddy]]
|
- [[Caddy]]
|
||||||
- [[SearXNG]]
|
- [[SearXNG]]
|
||||||
@@ -49,7 +49,7 @@ user sees it; the `bats` seams assert config shape, not behavior.
|
|||||||
*arr/qBittorrent links terminate on FrankBook-Pro, which sleeps when idle, and a
|
*arr/qBittorrent links terminate on FrankBook-Pro, which sleeps when idle, and a
|
||||||
sleeping Mac mini must never fail an unrelated deploy. The project joins only
|
sleeping Mac mini must never fail an unrelated deploy. The project joins only
|
||||||
when `E2E_INCLUDE_FRANKBOOK=1` (set by `scripts/deploy.sh` when a frankbook
|
when `E2E_INCLUDE_FRANKBOOK=1` (set by `scripts/deploy.sh` when a frankbook
|
||||||
trigger path changed: `tests/e2e/specs/19-frankbook.spec.ts`, `frankbook-pro/**`,
|
trigger path changed: `tests/e2e/specs/19-frankbook.spec.ts`, `frankbook-pro/**`
|
||||||
`shared/homepage/services.json`, or the `dankclaw/Caddyfile` serving the arr
|
`shared/homepage/services.json`, or the `dankclaw/Caddyfile` serving the arr
|
||||||
vhosts) or when an explicit `--project` argument names it. An `11-admin` edit
|
vhosts) or when an explicit `--project` argument names it. An `11-admin` edit
|
||||||
that touches the arr links must therefore be paired with a `19-frankbook` run
|
that touches the arr links must therefore be paired with a `19-frankbook` run
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ One Caddy fronts both hosts. It runs as a container on Dankclaw (192.168.0.143)
|
|||||||
|
|
||||||
As of 2026-08-28 the front door is a two-tier chain: Caddy terminates TLS,
|
As of 2026-08-28 the front door is a two-tier chain: Caddy terminates TLS,
|
||||||
Anubis verifies the client (DENY known AI scrapers, CHALLENGE everyone
|
Anubis verifies the client (DENY known AI scrapers, CHALLENGE everyone
|
||||||
else), and accepted traffic returns to Caddy's internal listener on `:8016`,
|
else), and accepted traffic returns to Caddy's internal listener on `:8016`
|
||||||
which proxies every request whole to the Authentik server with the Host
|
which proxies every request whole to the Authentik server with the Host
|
||||||
header preserved; Authentik's embedded outpost routes on that Host against
|
header preserved; Authentik's embedded outpost routes on that Host against
|
||||||
each proxy provider's `external_host`, serves the login flow in place for
|
each proxy provider's `external_host`, serves the login flow in place for
|
||||||
@@ -67,12 +67,12 @@ graph TB
|
|||||||
```
|
```
|
||||||
|
|
||||||
Guarded (Anubis `:8923` → Caddy `:8016` → Authentik outpost → Caddy `:8017`):
|
Guarded (Anubis `:8923` → Caddy `:8016` → Authentik outpost → Caddy `:8017`):
|
||||||
homepage (`www.`), search, **grafana** (`dankclaw/Caddyfile:128-129`),
|
homepage (`www.`), search, **grafana** (`dankclaw/Caddyfile:128-129`)
|
||||||
prometheus (`:135-137`), loki (`:141-143`), seerr (`:148-150`), marreta
|
prometheus (`:135-137`), loki (`:141-143`), seerr (`:148-150`), marreta
|
||||||
(`:155-160`), romm (`:167-169`), files (`:172-174`). Exempt / own-auth: `auth.`
|
(`:155-160`), romm (`:167-169`), files (`:172-174`). Exempt / own-auth: `auth.`
|
||||||
(the IdP login itself), `ai.` (OpenWebUI, its own OIDC), gitea (git smart-HTTP
|
(the IdP login itself), `ai.` (OpenWebUI, its own OIDC), gitea (git smart-HTTP
|
||||||
clients cannot solve JS challenges — `/info/refs` and `git-*-pack` pass
|
clients cannot solve JS challenges — `/info/refs` and `git-*-pack` pass
|
||||||
unchallenged), pihole (Caddy `basicauth`, `:167`), plex (plain HTTP),
|
unchallenged), pihole (Caddy `basicauth`, `:167`), plex (plain HTTP)
|
||||||
books/jellyfrank (own logins), `hermes.`/`hermesadmin.`, and the *arr vhosts
|
books/jellyfrank (own logins), `hermes.`/`hermesadmin.`, and the *arr vhosts
|
||||||
that terminate on FrankBook-Pro (`:254-281`, "exempt by construction: own
|
that terminate on FrankBook-Pro (`:254-281`, "exempt by construction: own
|
||||||
auth"). Grafana is **not** exempt even though it signs in through authentik as
|
auth"). Grafana is **not** exempt even though it signs in through authentik as
|
||||||
@@ -104,13 +104,13 @@ Caddy holds one Let's Encrypt certificate per vhost, issued and renewed through
|
|||||||
`Network=searxng.network` (runtime network `systemd-searxng`), while
|
`Network=searxng.network` (runtime network `systemd-searxng`), while
|
||||||
`searxng.container`, `marreta.container` and `marreta-browser.container` join
|
`searxng.container`, `marreta.container` and `marreta-browser.container` join
|
||||||
the tunnel the modern way — `Network=gluetun.container`
|
the tunnel the modern way — `Network=gluetun.container`
|
||||||
(`dankclaw/searxng/quadlet/searxng.container:18`,
|
(`dankclaw/searxng/quadlet/searxng.container:18`
|
||||||
`dankclaw/marreta/quadlet/marreta.container:34`,
|
`dankclaw/marreta/quadlet/marreta.container:34`
|
||||||
`dankclaw/marreta/quadlet/marreta-browser.container:25`), the Quadlet
|
`dankclaw/marreta/quadlet/marreta-browser.container:25`), the Quadlet
|
||||||
equivalent of the retired compose `network_mode: service:gluetun`. **Five**
|
equivalent of the retired compose `network_mode: service:gluetun`. **Five**
|
||||||
containers share the VPN exit, not three, and Marreta's outbound fetches leave
|
containers share the VPN exit, not three, and Marreta's outbound fetches leave
|
||||||
through the same WireGuard tunnel as SearXNG's. Pi-hole is a different kind of
|
through the same WireGuard tunnel as SearXNG's. Pi-hole is a different kind of
|
||||||
exception (`Network=slirp4netns`, `dankclaw/pihole/quadlet/pihole.container`);
|
exception (`Network=slirp4netns`, `dankclaw/pihole/quadlet/pihole.container`)
|
||||||
everything else on Dankclaw uses the normal LAN route. This matches KB Pattern
|
everything else on Dankclaw uses the normal LAN route. This matches KB Pattern
|
||||||
2 in [[Gluetun]].
|
2 in [[Gluetun]].
|
||||||
See [[Container Network Namespaces]] for which addresses are reachable from that
|
See [[Container Network Namespaces]] for which addresses are reachable from that
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ done
|
|||||||
## 2. Cutover: compose service → Quadlet unit
|
## 2. Cutover: compose service → Quadlet unit
|
||||||
|
|
||||||
Checklist (precedents to copy:
|
Checklist (precedents to copy:
|
||||||
`dankclaw/{caddy,gitea,searxng,open-webui,seerr}/quadlet/`,
|
`dankclaw/{caddy,gitea,searxng,open-webui,seerr}/quadlet/`
|
||||||
`dankclaw/observability/quadlet/`):
|
`dankclaw/observability/quadlet/`):
|
||||||
|
|
||||||
1. Unit file at `<host>/<svc>/quadlet/<name>.container`, mirroring compose
|
1. Unit file at `<host>/<svc>/quadlet/<name>.container`, mirroring compose
|
||||||
line for line: `Image=` (`localhost/` prefix for locally built images +
|
line for line: `Image=` (`localhost/` prefix for locally built images +
|
||||||
`Pull=never`), `ContainerName=` (Caddy and scrape configs address
|
`Pull=never`), `ContainerName=` (Caddy and scrape configs address
|
||||||
containers by name), `Network=`, `User=0:0` when the image's own user
|
containers by name), `Network=`, `User=0:0` when the image's own user
|
||||||
cannot write a rootless-owned bind mount, `Volume=` (`:ro` preserved),
|
cannot write a rootless-owned bind mount, `Volume=` (`:ro` preserved)
|
||||||
`Exec=` for compose `command:`, `PublishPort=`, health keys,
|
`Exec=` for compose `command:`, `PublishPort=`, health keys,
|
||||||
`[Service] Restart=always` + `RestartSec=10`, `[Install]
|
`[Service] Restart=always` + `RestartSec=10`, `[Install]
|
||||||
WantedBy=default.target`. **Never `systemctl --user enable` the generated
|
WantedBy=default.target`. **Never `systemctl --user enable` the generated
|
||||||
@@ -56,7 +56,7 @@ Checklist (precedents to copy:
|
|||||||
`systemctl --user restart <name>.service` → `wait_for_health`; retire any
|
`systemctl --user restart <name>.service` → `wait_for_health`; retire any
|
||||||
legacy hand-written unit into `~/.AGENTTRASH/` (a leftover
|
legacy hand-written unit into `~/.AGENTTRASH/` (a leftover
|
||||||
`~/.config/systemd/user/<svc>.service` that shells out to
|
`~/.config/systemd/user/<svc>.service` that shells out to
|
||||||
`podman compose up` collides BY NAME with the generated unit);
|
`podman compose up` collides BY NAME with the generated unit)
|
||||||
`podman rm -f <container>` so the unit's container can take the name.
|
`podman rm -f <container>` so the unit's container can take the name.
|
||||||
4. Planner: map `<host>/<svc>/**` → service in `scripts/deploy-plan.py`
|
4. Planner: map `<host>/<svc>/**` → service in `scripts/deploy-plan.py`
|
||||||
AND add a `service_is_allowed` entry; keep `SERVICE_ORDER` dependencies
|
AND add a `service_is_allowed` entry; keep `SERVICE_ORDER` dependencies
|
||||||
@@ -123,7 +123,7 @@ the restart policy faithfully resurrects it forever.
|
|||||||
|
|
||||||
Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
|
Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
|
||||||
= the §2 cutover. **Verify the capability, not the status:** `podman exec
|
= the §2 cutover. **Verify the capability, not the status:** `podman exec
|
||||||
<name> python3 -c "import torch; print(torch.cuda.is_available())"`;
|
<name> python3 -c "import torch; print(torch.cuda.is_available())"`
|
||||||
`ss -ltn | grep <port>` for the actual bind addresses. ComfyUI incident:
|
`ss -ltn | grep <port>` for the actual bind addresses. ComfyUI incident:
|
||||||
2026-09-16, stale device-less container, 9,146 restarts. History repeats:
|
2026-09-16, stale device-less container, 9,146 restarts. History repeats:
|
||||||
2026-08-26 Caddy (no unit), 2026-09-11 five observability services.
|
2026-08-26 Caddy (no unit), 2026-09-11 five observability services.
|
||||||
@@ -143,7 +143,7 @@ Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
|
|||||||
unhealthy but logs look fine, compare the baked
|
unhealthy but logs look fine, compare the baked
|
||||||
`{{.Config.Healthcheck.Test}}` array against the intended command before
|
`{{.Config.Healthcheck.Test}}` array against the intended command before
|
||||||
believing the status.
|
believing the status.
|
||||||
- **Probe a new key before shipping:** drop a throwaway `probe.container`,
|
- **Probe a new key before shipping:** drop a throwaway `probe.container`
|
||||||
`daemon-reload`, `journalctl --user --since "1 min ago" -o cat | grep -i
|
`daemon-reload`, `journalctl --user --since "1 min ago" -o cat | grep -i
|
||||||
"unsupported key"`, `systemctl --user cat probe.service` (shows the
|
"unsupported key"`, `systemctl --user cat probe.service` (shows the
|
||||||
generated ExecStart verbatim), then remove + reload.
|
generated ExecStart verbatim), then remove + reload.
|
||||||
|
|||||||
@@ -54,4 +54,3 @@ Tests are written first. A failing test means the seam is not satisfied; the con
|
|||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[Deployment Flow]]
|
- [[Deployment Flow]]
|
||||||
- [[index]]
|
|
||||||
@@ -56,8 +56,8 @@ sampling), `sampler_name: euler`, `scheduler: simple`, `denoise: 1.0`, seed
|
|||||||
fixed in the file (`1930` in the art workflow, `2026` in the text one) —
|
fixed in the file (`1930` in the art workflow, `2026` in the text one) —
|
||||||
read from `dankclaw/comfyui/workflows/*.json` node `9`.
|
read from `dankclaw/comfyui/workflows/*.json` node `9`.
|
||||||
|
|
||||||
Node `class_type`s in both files: `CLIPTextEncode` ×2, `UNETLoader`,
|
Node `class_type`s in both files: `CLIPTextEncode` ×2, `UNETLoader`
|
||||||
`CLIPLoader`, `VAELoader`, `EmptySD3LatentImage`, `KSampler`, `VAEDecode`,
|
`CLIPLoader`, `VAELoader`, `EmptySD3LatentImage`, `KSampler`, `VAEDecode`
|
||||||
`SaveImage` (ids 3–11).
|
`SaveImage` (ids 3–11).
|
||||||
|
|
||||||
| Workflow | File | Shape | Steps | Made for |
|
| Workflow | File | Shape | Steps | Made for |
|
||||||
@@ -113,9 +113,9 @@ only `models`, `input`, `output`, `custom_nodes` and the healthcheck file
|
|||||||
Quantized preferred, sized for the 8060S iGPU (GTT-backed — `/system_stats`
|
Quantized preferred, sized for the 8060S iGPU (GTT-backed — `/system_stats`
|
||||||
reports 122.4 GB of device VRAM on `cuda:0`, `curl http://127.0.0.1:8188/system_stats`
|
reports 122.4 GB of device VRAM on `cuda:0`, `curl http://127.0.0.1:8188/system_stats`
|
||||||
2026-10-18; the RTX 3090 is frank-zealot's). All repos verified 2026-10-05 and
|
2026-10-18; the RTX 3090 is frank-zealot's). All repos verified 2026-10-05 and
|
||||||
all four still resolve today (`huggingface.co/lightx2v/Qwen-Image-Lightning`,
|
all four still resolve today (`huggingface.co/lightx2v/Qwen-Image-Lightning`
|
||||||
`huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF`,
|
`huggingface.co/unsloth/Qwen-Image-Edit-2511-GGUF`
|
||||||
`huggingface.co/Comfy-Org/Qwen-Image-DiffSynth-ControlNets`,
|
`huggingface.co/Comfy-Org/Qwen-Image-DiffSynth-ControlNets`
|
||||||
`huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF`, 2026-10-18). None are on disk
|
`huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF`, 2026-10-18). None are on disk
|
||||||
yet: `models/` holds no lora/, controlnet/ or Wan/Edit file (`ls -R` 2026-10-18).
|
yet: `models/` holds no lora/, controlnet/ or Wan/Edit file (`ls -R` 2026-10-18).
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ yet: `models/` holds no lora/, controlnet/ or Wan/Edit file (`ls -R` 2026-10-18)
|
|||||||
| 3 | **DiffSynth ControlNets** (structure control) | `Comfy-Org/Qwen-Image-DiffSynth-ControlNets` | `split_files/model_patches/` canny + depth | ~2–3 GB each | Pose/edge/depth-guided layouts — pairs with the legible-text workflow for exact poster composition. The `QwenImageDiffsynthControlnet` node is already in the running ComfyUI (`GET http://127.0.0.1:8188/object_info/QwenImageDiffsynthControlnet` → 200, 2026-10-18) |
|
| 3 | **DiffSynth ControlNets** (structure control) | `Comfy-Org/Qwen-Image-DiffSynth-ControlNets` | `split_files/model_patches/` canny + depth | ~2–3 GB each | Pose/edge/depth-guided layouts — pairs with the legible-text workflow for exact poster composition. The `QwenImageDiffsynthControlnet` node is already in the running ComfyUI (`GET http://127.0.0.1:8188/object_info/QwenImageDiffsynthControlnet` → 200, 2026-10-18) |
|
||||||
| 4 | **Wan 2.2 TI2V-5B GGUF** (first video model) | `QuantStack/Wan2.2-TI2V-5B-GGUF` | Q4/Q5 GGUF (~6–8 GB) | ~6–8 GB | T2V+I2V in one dense 5B model; the gentlest on-ramp into video generation, runs in ComfyUI core (`wan` is in this build's `CLIPLoader` type enum — `GET http://127.0.0.1:8188/object_info/CLIPLoader` 2026-10-18) |
|
| 4 | **Wan 2.2 TI2V-5B GGUF** (first video model) | `QuantStack/Wan2.2-TI2V-5B-GGUF` | Q4/Q5 GGUF (~6–8 GB) | ~6–8 GB | T2V+I2V in one dense 5B model; the gentlest on-ramp into video generation, runs in ComfyUI core (`wan` is in this build's `CLIPLoader` type enum — `GET http://127.0.0.1:8188/object_info/CLIPLoader` 2026-10-18) |
|
||||||
|
|
||||||
Placement: #1 → `models/lora/` (create it), #2 → `models/diffusion_models/`,
|
Placement: #1 → `models/lora/` (create it), #2 → `models/diffusion_models/`
|
||||||
#3 → `models/controlnet/` (create it), #4 → `models/diffusion_models/`.
|
#3 → `models/controlnet/` (create it), #4 → `models/diffusion_models/`.
|
||||||
Fetch with `scripts/download-frank-*.sh` discipline (`-f` + byte-count +
|
Fetch with `scripts/download-frank-*.sh` discipline (`-f` + byte-count +
|
||||||
sha256 checks) or a plain `hf download` with the token in the environment —
|
sha256 checks) or a plain `hf download` with the token in the environment —
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Dankclaw is an AMD Strix Halo APU (gfx1151). Standard PyTorch ROCm wheels do not
|
|||||||
|
|
||||||
## GPU device access (rootless), and why the unit exists
|
## GPU device access (rootless), and why the unit exists
|
||||||
|
|
||||||
The unit passes `/dev/kfd` and `/dev/dri` (`AddDevice=`,
|
The unit passes `/dev/kfd` and `/dev/dri` (`AddDevice=`
|
||||||
`dankclaw/comfyui/quadlet/comfyui.container:45-46`) and uses
|
`dankclaw/comfyui/quadlet/comfyui.container:45-46`) and uses
|
||||||
`GroupAdd=keep-groups`. A literal `render` entry cannot work: rootless Podman
|
`GroupAdd=keep-groups`. A literal `render` entry cannot work: rootless Podman
|
||||||
cannot map the group name into the user namespace. The companion udev rule
|
cannot map the group name into the user namespace. The companion udev rule
|
||||||
@@ -115,7 +115,7 @@ generated units.
|
|||||||
The web UI responds on `/` (200 `text/html`, `curl http://127.0.0.1:8188/`
|
The web UI responds on `/` (200 `text/html`, `curl http://127.0.0.1:8188/`
|
||||||
2026-10-18). ComfyUI also exposes `/system_stats`. On this APU, `/system_stats`
|
2026-10-18). ComfyUI also exposes `/system_stats`. On this APU, `/system_stats`
|
||||||
reports `device=cuda:0 Radeon 8060S Graphics` (type `cuda`, the ROCm device). The
|
reports `device=cuda:0 Radeon 8060S Graphics` (type `cuda`, the ROCm device). The
|
||||||
health probe is the repo file `dankclaw/comfyui/comfyui-healthcheck.py`,
|
health probe is the repo file `dankclaw/comfyui/comfyui-healthcheck.py`
|
||||||
bind-mounted to `/usr/local/bin/comfyui-healthcheck.py` and run as `HealthCmd`
|
bind-mounted to `/usr/local/bin/comfyui-healthcheck.py` and run as `HealthCmd`
|
||||||
(`dankclaw/comfyui/quadlet/comfyui.container:52,57`); it GETs
|
(`dankclaw/comfyui/quadlet/comfyui.container:52,57`); it GETs
|
||||||
`http://127.0.0.1:8188/` inside the container, and the container reports
|
`http://127.0.0.1:8188/` inside the container, and the container reports
|
||||||
@@ -135,5 +135,5 @@ bind-mounted to `/usr/local/bin/comfyui-healthcheck.py` and run as `HealthCmd`
|
|||||||
- [[Dankclaw Stack]]
|
- [[Dankclaw Stack]]
|
||||||
- [[ComfyUI ROCm and Vulkan]] — the live-verified GPU path research
|
- [[ComfyUI ROCm and Vulkan]] — the live-verified GPU path research
|
||||||
- [[ComfyUI Qwen-Image Workflows]] — the Qwen-Image 2.1 payload, proven workflows, and recommended model/LoRA additions
|
- [[ComfyUI Qwen-Image Workflows]] — the Qwen-Image 2.1 payload, proven workflows, and recommended model/LoRA additions
|
||||||
- [[Lemonade and sd.cpp]] — the ggml-family alternative
|
- the ggml-family alternative
|
||||||
- Knowledge base: [[ComfyUI]] (tools)
|
- Knowledge base: [[ComfyUI]] (tools)
|
||||||
@@ -52,11 +52,10 @@ graph LR
|
|||||||
|
|
||||||
The LLM rows are bare-metal systemd services, not containers, and this repo
|
The LLM rows are bare-metal systemd services, not containers, and this repo
|
||||||
owns their scripts, units and knowledge base (see
|
owns their scripts, units and knowledge base (see
|
||||||
[LLM fleet knowledge base](../LLM/index.md)). Which payload and runtime sits on
|
). Which payload and runtime sits on
|
||||||
each port is era state and lives on the endpoint page, not here. `frank-npu`
|
each port is era state and lives on the endpoint page, not here. `frank-npu`
|
||||||
(the XDNA2 NPU endpoint) and `frank-medium` (the predecessor of `frank-archon`
|
(the XDNA2 NPU endpoint) and `frank-medium` (the predecessor of `frank-archon`
|
||||||
on the Dankserver port) are both retired — see [[frank-npu]] and
|
on the Dankserver port) are both retired — see and
|
||||||
[[frank-medium-radiance-mxfp4]].
|
|
||||||
|
|
||||||
## Boot Persistence
|
## Boot Persistence
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ Deploy the SearXNG trio with `scripts/deploy-searxng-quadlet.sh dankclaw`, open-
|
|||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[index]] — repository index
|
- repository index
|
||||||
- [[Network Architecture]] — routing and proxy
|
- [[Network Architecture]] — routing and proxy
|
||||||
- [[Observability]] — Grafana, Prometheus, logs
|
- [[Observability]] — Grafana, Prometheus, logs
|
||||||
- [LLM fleet knowledge base](../LLM/index.md) — the model-serving endpoints on this host
|
- the model-serving endpoints on this host
|
||||||
@@ -1,35 +1,36 @@
|
|||||||
---
|
---
|
||||||
type: reference
|
type: reference
|
||||||
description: "The engine behind the fleet's speech endpoint: its runtime, private bridge, and the surface it publishes."
|
description: "The engine behind the fleet's speech endpoint: its Vulkan build on the iGPU, its private bridge, and what it publishes."
|
||||||
tags:
|
tags:
|
||||||
- dankclaw
|
- dankclaw
|
||||||
- tts
|
- tts
|
||||||
- voice
|
- voice
|
||||||
- quadlet
|
- quadlet
|
||||||
- cpu
|
- vulkan
|
||||||
timestamp: 2026-10-18
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# Frank TTS Engine
|
# Frank TTS Engine
|
||||||
|
|
||||||
`frank-tts-engine` is the **arm A text-to-speech engine** on Dankclaw: [qwentts.cpp](https://github.com/ServeurpersoCom/qwentts.cpp)'s resident `tts-server` HTTP process, holding the Qwen3-TTS **1.7 B base talker (Q4_K_M)** and the **12 Hz tokenizer (Q8_0)** in RAM once and synthesizing from there. It listens on container port **8880** and is **deliberately not a fleet endpoint** — the endpoint consumers address is `frank-tts` on **7772**, which resolves voice names, re-encodes to mp3 and owns the timeout budget. This page is the engine runbook; the endpoint's contract lives with its own page.
|
`frank-tts-engine` is the **arm A text-to-speech engine** on Dankclaw: [qwentts.cpp](https://github.com/ServeurpersoCom/qwentts.cpp)'s resident `tts-server` HTTP process, holding the Qwen3-TTS **1.7 B base talker (Q4_K_M)** and the **12 Hz tokenizer (Q8_0)** loaded once and synthesizing from there — **on the gfx1151 iGPU** (ggml's Vulkan backend, `--device Vulkan0`) since 2026-10-19, and in host RAM before that. It listens on container port **8880** and is **deliberately not a fleet endpoint** — the endpoint consumers address is `frank-tts` on **7772**, which resolves voice names, re-encodes to mp3 when asked, and owns the timeout budget. This page is the engine runbook; the endpoint's contract lives with its own page.
|
||||||
|
|
||||||
**Deployed 2026-10-18.** The chain ran in the order at the bottom of this page — fetch (1.4 GiB, byte + LFS oid verified, 18 s), image build against the pinned commit, unit + network install, container-health gate, warm pass — and the live state still reads the way this page describes it: unit `active` with no drop-ins, container healthy 31 s after the build, **4.0 GiB** resident on a host that kept 17 GiB available, `3 voice(s) registered and warm` in the journal, and no published port anywhere. Two things the first real boot taught: the warm hook's registration body must reach curl through a **file**, not an argument (Troubleshooting), and `STEP 3/13 RUN useradd` is the first honest proof of the image's non-root user.
|
**Deployed 2026-10-18, cut over to the iGPU on 2026-10-19.** The first chain ran in the order at the bottom of this page — fetch (1.4 GiB, byte + LFS oid verified), image build against the pinned commit, unit + network install, container-health gate, warm pass — and the live state still reads the way this page describes it: unit `active` with no drop-ins, container healthy shortly after the build, `3 voice(s) registered and warm` in the journal, and no published port anywhere. Two things the first real boot taught: the warm hook's registration body must reach curl through a **file**, not an argument (Troubleshooting), and `STEP 3/13 RUN useradd` is the first honest proof of the image's non-root user.
|
||||||
|
|
||||||
**Host:** Dankclaw (`192.168.0.143`), rootless Podman, user `dank` — **Port:** 8880, **never published** — **Network:** the private `systemd-frank-tts-engine` bridge (10.89.5.0/24) and no other — **Deployer:** `frank-tts-engine` — **Consumers:** `frank-tts` (7772) and, through it, every voice request in the fleet including [[Open Notebook]]'s podcast renders.
|
**Host:** Dankclaw (`192.168.0.143`), rootless Podman, user `dank` — **Port:** 8880, **never published** — **Network:** the private `systemd-frank-tts-engine` bridge (10.89.5.0/24) and no other — **Deployer:** `frank-tts-engine` — **Consumers:** `frank-tts` (7772) and, through it, every voice request in the fleet including [[Open Notebook]]'s podcast renders.
|
||||||
|
|
||||||
## Facts (verified 2026-10-18 against the pinned upstream commit, the Hub, and the CPU trial on this host)
|
## Facts (verified 2026-10-18 against the pinned upstream commit, the Hub and the CPU trial; the device rows re-verified on the live iGPU shape 2026-10-19)
|
||||||
|
|
||||||
| Item | Value |
|
| Item | Value |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| Engine | `tts-server` from qwentts.cpp, pinned at commit `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4` (`dankclaw/voiceservice/Containerfile.engine`). The source is **not vendored**: the image clones that commit and its recorded `ggml` submodule, so the build needs outbound HTTPS and the `cfhash` label is what keeps an unchanged re-deploy offline |
|
| Engine | `tts-server` from qwentts.cpp, pinned at commit `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4` (`dankclaw/voiceservice/Containerfile.engine`). The source is **not vendored**: the image clones that commit and its recorded `ggml` submodule, so the build needs outbound HTTPS and the `cfhash` label is what keeps an unchanged re-deploy offline |
|
||||||
| Build line | `cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CCACHE=OFF -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_OPENMP=ON -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=OFF -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF && cmake --build build -j 8` — the exact line the 2026-10-18 trial ran on this box. `GGML_BLAS=OFF` is **forced**: Dankclaw has `libopenblas.so.0` and no BLAS dev headers, so upstream's own `Dockerfile` and `buildcpu.sh` (both of which pass BLAS on) cannot be copied. No `-DGGML_NATIVE` is passed, and ggml defaults it on, so the binary is `-march=native` for the machine that built it: **build this image on the host that runs it** |
|
| Build line | `cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CCACHE=OFF -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_OPENMP=ON -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF -DGGML_VULKAN=ON -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF && cmake --build build -j 8`. **`GGML_VULKAN=ON` since 2026-10-19**, and that door needs three packages in the build stage: `libvulkan-dev`, `spirv-headers`, and **`glslc`** — which Ubuntu's `glslang-tools` does NOT ship (it ships `glslangValidator`, and `find_package(Vulkan COMPONENTS glslc REQUIRED)` fails without the package). The archive's `glslc` is 2023.8: `GL_KHR_cooperative_matrix` compiles, the coopmat2 / integer-dot / bf16 / micro-float shader families do not. `GGML_BLAS=OFF` is **forced**: Dankclaw has `libopenblas.so.0` and no BLAS dev headers, so upstream's own `Dockerfile` and `buildcpu.sh` (both pass BLAS on) cannot be copied. No `-DGGML_NATIVE` is passed, and ggml defaults it on, so the binary is `-march=native` for the machine that built it: **build this image on the host that runs it** |
|
||||||
| Image | built locally as `franksdank/frank-tts-engine:latest`, two stages from one digest-pinned base `docker.io/library/ubuntu:24.04@sha256:68ee22004a9109b44973313b02ff7b1de250f1de0f5ecb628d73f579aaff33ac` (index digest resolved 2026-10-18). Runtime stage: `libgomp1` (OpenMP is on), `curl` (the unit's `HealthCmd` runs as `/bin/sh -c`, and the warmer speaks HTTP), uid **10000**, `LD_LIBRARY_PATH=/app` because ggml builds shared by default and the link-time RPATH names the build tree |
|
| Image | built locally as `franksdank/frank-tts-engine:latest`, two stages from one digest-pinned base `docker.io/library/ubuntu:24.04@sha256:68ee22004a9109b44973313b02ff7b1de250f1de0f5ecb628d73f579aaff33ac` (index digest resolved 2026-10-18). Runtime stage: `libgomp1` (OpenMP is on), `curl` (the unit's `HealthCmd` runs as `/bin/sh -c`, and the warmer speaks HTTP), `libvulkan1` + `mesa-vulkan-drivers` (the loader and the RADV ICD — a loader with no ICD enumerates ZERO devices, and the server then dies in device lookup after both GGUFs have been read), uid **10000**, `LD_LIBRARY_PATH=/app` because ggml builds shared by default and the link-time RPATH names the build tree. **No NVIDIA ICD on purpose**: exactly one Vulkan device in the enumeration is what makes the name `Vulkan0` safe to write down |
|
||||||
| Talker | `qwen-talker-1.7b-base-Q4_K_M.gguf`, **1 219 245 248 B**, sha256 `ea393ebaf2167ea23ce9fc18b093822851358a950d7075cd47ab4f6ce23e887d` (the Hub's LFS pointer oid) |
|
| Talker | `qwen-talker-1.7b-base-Q4_K_M.gguf`, **1 219 245 248 B**, sha256 `ea393ebaf2167ea23ce9fc18b093822851358a950d7075cd47ab4f6ce23e887d` (the Hub's LFS pointer oid) |
|
||||||
| Tokenizer | `qwen-tokenizer-12hz-Q8_0.gguf`, **291 150 624 B**, sha256 `1883beeed99348fc35e23dd225e9082f93f6f8c109330a33d935baa8acdbfd94` |
|
| Tokenizer | `qwen-tokenizer-12hz-Q8_0.gguf`, **291 150 624 B**, sha256 `1883beeed99348fc35e23dd225e9082f93f6f8c109330a33d935baa8acdbfd94` |
|
||||||
| Source of both | `https://huggingface.co/Serveurperso/Qwen3-TTS-GGUF`, fetched by `scripts/download-frank-tts-engine-qwentts.sh` into `/home/dank/models/Qwen3-TTS-GGUF`, mounted read-only at `/models`. It has to be the **base** variant: the engine refuses every voice registration unless the model reports type `base`, so on `customvoice`/`voicedesign` the cloned-voice product does not exist. A 0.6 B talker (628 905 056 B) exists and is **not** the decided artifact |
|
| Source of both | `https://huggingface.co/Serveurperso/Qwen3-TTS-GGUF`, fetched by `scripts/download-frank-tts-engine-qwentts.sh` into `/home/dank/models/Qwen3-TTS-GGUF`, mounted read-only at `/models`. It has to be the **base** variant: the engine refuses every voice registration unless the model reports type `base`, so on `customvoice`/`voicedesign` the cloned-voice product does not exist. A 0.6 B talker (628 905 056 B) exists and is **not** the decided artifact |
|
||||||
| Memory | **5.7 GiB peak RSS** with both files resident (2026-10-18 trial). No `LimitMEMORY`/`MemoryMax`: a limit would turn a busy moment into an OOM kill in the middle of an utterance |
|
| Device | `--device Vulkan0`: the gfx1151 iGPU, **shared with [[frank-dragoon]] as a co-tenant, never a handover**. Proven on the live pair, not inferred: `fuser -v /dev/dri/renderD129` lists `tts-server` beside `flash_serve`. Both gates (the RADV heap and dragoon's contention) are §5 of `report/tts-npu-spike-verdict-2026-10-19.md`, which also carries the change surface. **Rollback: `QWENTTS_DEVICE=` (empty) in `scripts/frank-tts-engine.env`, redeploy this one service** — the entrypoint then passes no `--device` at all, which is the CPU shape, no rebuild, nothing else restarts |
|
||||||
| Concurrency | `--max-batch 1`: a serialised FIFO. Warm RTF on this box is 0.55–0.67 against real time 1.0, and that margin is the entire headroom a live stream has; two concurrent syntheses split the same cores and the second caller's first byte additionally waits for the first utterance to finish |
|
| Memory | **1.7 GiB host RSS** since the iGPU cutover — the weights ride the device. No `LimitMEMORY`/`MemoryMax`: a limit would turn a busy moment into an OOM kill in the middle of an utterance |
|
||||||
|
| Concurrency | `--max-batch 1`: a serialised FIFO. Warm RTF on the iGPU measures **0.20** (11.04 s of audio in 2.23 s) with `frank-dragoon` live on the same card, so a live stream has room to spare; a second concurrent synthesis would share the device with the first and make the caller behind it wait out the whole first utterance, so the FIFO stays a decision, not an accident |
|
||||||
| Network | `systemd-frank-tts-engine` (10.89.5.0/24, `dankclaw/voiceservice/quadlet/frank-tts-engine.network`) and **nothing else**, with **no `PublishPort` at all**. `frank-tts` joins the same bridge and dials `http://frank-tts-engine:8880` |
|
| Network | `systemd-frank-tts-engine` (10.89.5.0/24, `dankclaw/voiceservice/quadlet/frank-tts-engine.network`) and **nothing else**, with **no `PublishPort` at all**. `frank-tts` joins the same bridge and dials `http://frank-tts-engine:8880` |
|
||||||
| Voices | `/home/dank/voices` mounted read-only at `/voices`, and it is the **warmer** that reads it, not the server: `tts-server` receives reference audio as base64 in a registration body and keeps its registry in memory only |
|
| Voices | `/home/dank/voices` mounted read-only at `/voices`, and it is the **warmer** that reads it, not the server: `tts-server` receives reference audio as base64 in a registration body and keeps its registry in memory only |
|
||||||
| Health | `GET /health` → `{"status":"ok"}`, served by the HTTP thread rather than the synthesis queue, so a busy engine still reads healthy. Probed by the unit (`HealthCmd`, 30 s interval, 120 s start period) **inside the container's namespace**, because there is no published port to probe from outside |
|
| Health | `GET /health` → `{"status":"ok"}`, served by the HTTP thread rather than the synthesis queue, so a busy engine still reads healthy. Probed by the unit (`HealthCmd`, 30 s interval, 120 s start period) **inside the container's namespace**, because there is no published port to probe from outside |
|
||||||
@@ -37,18 +38,11 @@ timestamp: 2026-10-18
|
|||||||
|
|
||||||
## Why this engine, and not the alternatives
|
## Why this engine, and not the alternatives
|
||||||
|
|
||||||
The criterion Frank set on 2026-10-18 was **"whatever is better for live output"**, and live output is decided by time-to-first-audio and by whether the stream stays ahead of real time — not by benchmark throughput. RTF only has to stay under 1.0 with margin; what separates the arms is first-byte latency and **spread**. Measured on this host the same day (CPU; the full A/B is the section *"Decision: arm A for live output (2026-10-18)"* in [[Local TTS and Voice Cloning]], which is the source of these numbers):
|
The criterion Frank set on 2026-10-18 was **"whatever is better for live output"**, and live output is decided by time-to-first-audio and by whether the stream stays ahead of real time — not by benchmark throughput. On that criterion the resident `tts-server` (arm A) beat the mainline per-request CLI path (arm B) on this box the same day, and the deciding property was **spread**: the resident server repeats the same first-audio latency every run, while the CLI path varies enough that some runs fall behind real time and a live listener hears the stream stall mid-sentence. That A/B was a CPU measurement and the endpoint has since moved to the iGPU, so the shape of the argument is what is kept here; no page carries its numbers any more, and they are not worth re-deriving.
|
||||||
|
|
||||||
| Measure | Resident `tts-server` (arm A, chosen) | Mainline per-request CLI path (arm B) |
|
The rollback to the other backend stays in the repo untouched — `TTS_BACKEND=llama-tts-cli` in `scripts/frank-tts.env` still selects the per-request CLI engine, and that file's header documents what it costs.
|
||||||
|---|---|---|
|
|
||||||
| first audio byte | **86–451 ms** end to end; **94–112 ms** server-side | model load, per request |
|
|
||||||
| warm real-time factor while streaming | **0.55–0.67** | 0.93–0.99 |
|
|
||||||
| run-to-run spread | **±0.01** | ±0.25 |
|
|
||||||
| HTTP surface | serves `/v1/audio/speech` itself | none — façade CLI only |
|
|
||||||
|
|
||||||
The spread is the whole argument: at ±0.01 the engine finishes synthesizing 1.5–1.8× faster than the audio plays, every run; at ±0.25 some runs fall behind real time and a live listener hears the stream stall mid-sentence. The rollback path stays in the repo untouched — `TTS_BACKEND=llama-tts-cli` in `scripts/frank-tts.env` still selects the per-request CLI engine, and that file's header documents what it costs.
|
## Four laws this service lives under
|
||||||
|
|
||||||
## Three laws this service lives under
|
|
||||||
|
|
||||||
**1. The registry law → the unit warms at boot.** The voice registry is **memory only**: a restart forgets every voice, and re-registering one does **not** warm it — only synthesizing does. A boot that registers nothing leaves the façade answering 400 for every name, which is why `frank-tts-engine.container` carries
|
**1. The registry law → the unit warms at boot.** The voice registry is **memory only**: a restart forgets every voice, and re-registering one does **not** warm it — only synthesizing does. A boot that registers nothing leaves the façade answering 400 for every name, which is why `frank-tts-engine.container` carries
|
||||||
|
|
||||||
@@ -64,10 +58,12 @@ frank-tts-engine-warm: voice narrator1 cold=1402ms warm=1812ms
|
|||||||
frank-tts-engine-warm: 3 voice(s) registered and warm
|
frank-tts-engine-warm: 3 voice(s) registered and warm
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. The thread law → nothing is ever pinned.** `tts-server` in this build has **no `-t`/`--threads` flag**: `src/backend.h` computes the CPU thread count as `std::thread::hardware_concurrency() / 2` (its comment: GEMM shares SIMD units across hyperthreads). Thread count therefore follows what the kernel *reports*, not what a cgroup *allows* — so `CPUQuota=`, `CPUAffinity=`, `taskset` or a slice silently changes the engine's GEMM tiling and its timings, with no log line. Nothing in this fleet pins this container. If a neighbour starves it, fix the neighbour.
|
**2. The thread law → nothing is ever pinned.** `tts-server` in this build has **no `-t`/`--threads` flag**: `src/backend.h` computes the CPU thread count as `std::thread::hardware_concurrency() / 2` (its comment: GEMM shares SIMD units across hyperthreads). Thread count therefore follows what the kernel *reports*, not what a cgroup *allows* — so `CPUQuota=`, `CPUAffinity=`, `taskset` or a slice silently changes the engine's pool sizes and its timings, with no log line. The rule did not retire with the iGPU cutover: the binary still sizes those pools from what the kernel says, whatever the device ends up doing with the matmuls. Nothing in this fleet pins this container. If a neighbour starves it, fix the neighbour.
|
||||||
|
|
||||||
**3. Streaming is not buffered.** At the same seed, `response_format=pcm` (chunked, codec chunks pushed as they decode) and `response_format=wav` (buffered) differ in **92.2 % of samples**, −41.6 dB RMS. Nothing may compare the two paths, cache one against the other, or assume a seed pins output across `response_format`. `frank-tts` always asks for `wav`; the warmer asks for `wav`; the test seam is forbidden from pinning either.
|
**3. Streaming is not buffered.** At the same seed, `response_format=pcm` (chunked, codec chunks pushed as they decode) and `response_format=wav` (buffered) differ in **92.2 % of samples**, −41.6 dB RMS. Nothing may compare the two paths, cache one against the other, or assume a seed pins output across `response_format`. `frank-tts` always asks for `wav`; the warmer asks for `wav`; the test seam is forbidden from pinning either.
|
||||||
|
|
||||||
|
**4. The device law → the card is shared, and the rollback is one line.** Synthesis computes on the gfx1151 **beside** [[frank-dragoon]], so a change to either tenant is a change to both. Two gates decide that pair, and both passed on 2026-10-19 with the other tenant live: the RADV heap a rootless container tenant is actually handed (not the ~2 GiB DRM carve-out), and dragoon's own first-token and decode rates with a synthesis in flight — unchanged. Their numbers, the recipe that measures them, and the full change surface are §5 of `report/tts-npu-spike-verdict-2026-10-19.md`; do not re-derive them here. What belongs on this page is the move: `QWENTTS_DEVICE=` (empty) in `scripts/frank-tts-engine.env`, re-deploy this one service, and synthesis is back on CPU — no rebuild, and dragoon never restarts.
|
||||||
|
|
||||||
## The voice registry contract
|
## The voice registry contract
|
||||||
|
|
||||||
A voice becomes known to the engine through `POST /v1/audio/voices`, and only these shapes are accepted:
|
A voice becomes known to the engine through `POST /v1/audio/voices`, and only these shapes are accepted:
|
||||||
@@ -97,9 +93,9 @@ The **façade** on 7772 is the pair's public surface and carries the probe. Note
|
|||||||
| `dankclaw/voiceservice/Containerfile.engine` | two-stage build of the pinned commit, proven cmake line, digest-pinned base, ships both scripts |
|
| `dankclaw/voiceservice/Containerfile.engine` | two-stage build of the pinned commit, proven cmake line, digest-pinned base, ships both scripts |
|
||||||
| `dankclaw/voiceservice/tts-engine-start.sh` | image entrypoint (`/app/start.sh`): env → flags, refuses to boot without weights |
|
| `dankclaw/voiceservice/tts-engine-start.sh` | image entrypoint (`/app/start.sh`): env → flags, refuses to boot without weights |
|
||||||
| `dankclaw/voiceservice/tts-engine-warm.sh` | boot warmer (`/app/warm-voices.sh`): register from `reference.wav` (body via a temp file), speak a fixed sentence twice, report cold/warm |
|
| `dankclaw/voiceservice/tts-engine-warm.sh` | boot warmer (`/app/warm-voices.sh`): register from `reference.wav` (body via a temp file), speak a fixed sentence twice, report cold/warm |
|
||||||
| `dankclaw/voiceservice/quadlet/frank-tts-engine.container` | the unit: bridge only, no publish, env file, two read-only mounts, health probe, warm hook, `Restart=always` |
|
| `dankclaw/voiceservice/quadlet/frank-tts-engine.container` | the unit: bridge only, no publish, env file, two read-only mounts, render access (`AddDevice=/dev/dri` + `GroupAdd=keep-groups`), health probe, warm hook, `Restart=always` |
|
||||||
| `dankclaw/voiceservice/quadlet/frank-tts-engine.network` | the private 10.89.5.0/24 bridge, with the subnet registry |
|
| `dankclaw/voiceservice/quadlet/frank-tts-engine.network` | the private 10.89.5.0/24 bridge, with the subnet registry |
|
||||||
| `scripts/frank-tts-engine.env` | paths and numbers only, with the thread law and the registry law in its header |
|
| `scripts/frank-tts-engine.env` | paths and numbers only, with the thread law, the registry law and the device law in its header |
|
||||||
| `scripts/download-frank-tts-engine-qwentts.sh` | weight fetcher: no resume, byte count + LFS oid check, `--verify-only` |
|
| `scripts/download-frank-tts-engine-qwentts.sh` | weight fetcher: no resume, byte count + LFS oid check, `--verify-only` |
|
||||||
| `scripts/deploy-remote.sh` → `deploy_frank_tts_engine` | weights gate → build → quadlet → restart → container health → warm |
|
| `scripts/deploy-remote.sh` → `deploy_frank_tts_engine` | weights gate → build → quadlet → restart → container health → warm |
|
||||||
| `tests/frank_tts_engine.bats` | the seam: generator output, entrypoint argv, warmer against a stub engine, planner and routing maps |
|
| `tests/frank_tts_engine.bats` | the seam: generator output, entrypoint argv, warmer against a stub engine, planner and routing maps |
|
||||||
@@ -118,6 +114,25 @@ still open.
|
|||||||
6. **Then the façade on 7772** (its own page), after the engine answered — that order is the whole point of step 6: publishing the façade in front of a dead engine would make every consumer fail at the first voice line.
|
6. **Then the façade on 7772** (its own page), after the engine answered — that order is the whole point of step 6: publishing the façade in front of a dead engine would make every consumer fail at the first voice line.
|
||||||
7. **Open:** [[Open Notebook]] is still not installed, so nothing renders a podcast yet; the voice leg it waits on now exists.
|
7. **Open:** [[Open Notebook]] is still not installed, so nothing renders a podcast yet; the voice leg it waits on now exists.
|
||||||
|
|
||||||
|
### 2026-10-19 — the iGPU cutover (PR #34, `6248f02`)
|
||||||
|
|
||||||
|
1. **Configure gate before touching anything** — the flag set, configured inside the
|
||||||
|
digest-pinned base on Dankclaw: `Found Vulkan: … found components: glslc` then
|
||||||
|
`Including Vulkan backend`. That probe is what caught `glslc` missing from
|
||||||
|
`glslang-tools`; the Containerfile's flag comment carries the finding.
|
||||||
|
2. **Deploy** — `./scripts/deploy.sh dankclaw --service frank-tts-engine`: image rebuild
|
||||||
|
(the `cfhash` moved), unit reinstall, health gate, warm pass
|
||||||
|
(`3 voice(s) registered and warm`), Playwright e2e 33/33.
|
||||||
|
3. **Prove the device, not the flag** — `sudo fuser -v /dev/dri/renderD129` lists
|
||||||
|
`tts-server` next to dragoon's `flash_serve`. Do this instead of reading the log: the
|
||||||
|
engine prints nothing about which backend it chose, so argv (`--device Vulkan0`) proves
|
||||||
|
intent and the render-node holder list proves use.
|
||||||
|
4. **Consumer path** — one `/v1/audio/speech` per voice through the façade on 7772; all
|
||||||
|
three answered with audio. The RTF figure in the Facts table comes from one engine-direct
|
||||||
|
WAV, where the duration is exact.
|
||||||
|
5. **Co-tenancy gate** — §5 of the verdict report: dragoon's first token and decode
|
||||||
|
unchanged with a 16.9 s synthesis occupying the card.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | Meaning | Move |
|
| Symptom | Meaning | Move |
|
||||||
@@ -128,6 +143,8 @@ still open.
|
|||||||
| Everything is ~2× slower after a systemd tweak | a cpuset/cap pin changed `hardware_concurrency()` and the engine resized its thread pools | remove the pin (law 2) |
|
| Everything is ~2× slower after a systemd tweak | a cpuset/cap pin changed `hardware_concurrency()` and the engine resized its thread pools | remove the pin (law 2) |
|
||||||
| `502` from the façade with `frank-tts upstream request failed` | the façade cannot resolve or reach the engine | `podman network inspect systemd-frank-tts-engine` and `podman inspect frank-tts --format '{{json .NetworkSettings.Networks}}'` — the façade must be attached to the bridge; a `127.0.0.1:8880` or `host.containers.internal:8880` upstream in `scripts/frank-tts.env` is the old shape and cannot work |
|
| `502` from the façade with `frank-tts upstream request failed` | the façade cannot resolve or reach the engine | `podman network inspect systemd-frank-tts-engine` and `podman inspect frank-tts --format '{{json .NetworkSettings.Networks}}'` — the façade must be attached to the bridge; a `127.0.0.1:8880` or `host.containers.internal:8880` upstream in `scripts/frank-tts.env` is the old shape and cannot work |
|
||||||
| A voice is missing from the list | its directory has no `reference.wav`, or the name is not lowercase | fix the pack; both are refused loudly by design |
|
| A voice is missing from the list | its directory has no `reference.wav`, or the name is not lowercase | fix the pack; both are refused loudly by design |
|
||||||
| `podman exec` in the deploy says `no such file or directory` for `/app/warm-voices.sh` | the image predates the warmer | re-deploy: since 2026-10-18 `build_compose_image` hashes every file the image `COPY`s, not only `Containerfile.engine`, so editing the warmer rebuilds it — see [[deployment]] |
|
| `podman exec` in the deploy says `no such file or directory` for `/app/warm-voices.sh` | the image predates the warmer | re-deploy: since 2026-10-18 `build_compose_image` hashes every file the image `COPY`s, not only `Containerfile.engine`, so editing the warmer rebuilds it — see |
|
||||||
| The warm pass logs `registered, but the warm synthesis failed` for EVERY voice, while a hand-run `/app/warm-voices.sh` succeeds | `WARM_TEXT` arrived **with its quotes**. podman's `--env-file` does no shell parsing — it takes the literal remainder of the line — so the env-file reader got `"Warm up, one two three."` where `source` gives `Warm up, one two three.`; the quotes plus a backslash then make the synthesis body invalid JSON | the hook strips one matched pair and escapes what is left; prove the value with `podman exec frank-tts-engine env \| grep WARM_TEXT`. The seam pins both readers, because a test that only sources the env file cannot see this |
|
| The warm pass logs `registered, but the warm synthesis failed` for EVERY voice, while a hand-run `/app/warm-voices.sh` succeeds | `WARM_TEXT` arrived **with its quotes**. podman's `--env-file` does no shell parsing — it takes the literal remainder of the line — so the env-file reader got `"Warm up, one two three."` where `source` gives `Warm up, one two three.`; the quotes plus a backslash then make the synthesis body invalid JSON | the hook strips one matched pair and escapes what is left; prove the value with `podman exec frank-tts-engine env \| grep WARM_TEXT`. The seam pins both readers, because a test that only sources the env file cannot see this |
|
||||||
| A `502` for `/v1/voices` where the engine answers `200` on `/health` from the same boot | the engine restarted and its in-memory registry is empty again — `/health` is model-loaded, NOT voices-registered | re-run `podman exec frank-tts-engine /app/warm-voices.sh`; that is the same distinction the `frank-tts` healthcheck enforces |
|
| A `502` for `/v1/voices` where the engine answers `200` on `/health` from the same boot | the engine restarted and its in-memory registry is empty again — `/health` is model-loaded, NOT voices-registered | re-run `podman exec frank-tts-engine /app/warm-voices.sh`; that is the same distinction the `frank-tts` healthcheck enforces |
|
||||||
|
| Unit restarts every 10 s and the log ends on a **device** error *after* both GGUFs were read | the image has the Vulkan loader but no ICD, or the container has no render node — the device name then resolves to nothing | `podman exec frank-tts-engine ls /dev/dri` (it must see `renderD129`) and `podman exec frank-tts-engine dpkg -l \| grep -E 'mesa-vulkan\|libvulkan1'`; re-deploy to rebuild the image, or set `QWENTTS_DEVICE=` and re-deploy for the CPU shape |
|
||||||
|
| `frank-dragoon` slows while TTS speaks | the contention premise broke — a Mesa bump, a third GPU tenant, a bigger synthesis | `QWENTTS_DEVICE=` + re-deploy this service, then re-run the gate recipe in §5 of `report/tts-npu-spike-verdict-2026-10-19.md` before putting it back |
|
||||||
@@ -18,10 +18,10 @@ audio in that cloned voice. Everything else in the voice stack sits behind it
|
|||||||
the engine ([[Frank TTS Engine]]) publishes no port on purpose, and consumers
|
the engine ([[Frank TTS Engine]]) publishes no port on purpose, and consumers
|
||||||
never learn a path.
|
never learn a path.
|
||||||
|
|
||||||
**Deployed 2026-10-18** — `./scripts/deploy.sh dankclaw --service frank-tts`,
|
**Deployed 2026-10-18** — `./scripts/deploy.sh dankclaw --service frank-tts`
|
||||||
both units `active`, no drop-ins, and the homepage card's `wip` gate came off in
|
both units `active`, no drop-ins, and the homepage card's `wip` gate came off in
|
||||||
the same change. Probes on the deployed endpoint: `/health` answers
|
the same change. Probes on the deployed endpoint: `/health` answers
|
||||||
`{"status":"ok","backend":"omni-http","voices":["narrator1","narrator2","narrator3"]}`,
|
`{"status":"ok","backend":"omni-http","voices":["narrator1","narrator2","narrator3"]}`
|
||||||
and two different voices rendered the same sentence into **different** bytes
|
and two different voices rendered the same sentence into **different** bytes
|
||||||
(24 kHz mono PCM16, 4.24 s, healthy RMS) — so name resolution is real, not a
|
(24 kHz mono PCM16, 4.24 s, healthy RMS) — so name resolution is real, not a
|
||||||
pass-through of one default. The operator harness routes its `speech` role here;
|
pass-through of one default. The operator harness routes its `speech` role here;
|
||||||
@@ -111,6 +111,22 @@ own `/health` answers 200 with the backend name and the voice list it resolved
|
|||||||
without dialing the engine, so "façade healthy, engine down" is a real state: it
|
without dialing the engine, so "façade healthy, engine down" is a real state: it
|
||||||
shows up as a failed synthesis, never as a down probe.
|
shows up as a failed synthesis, never as a down probe.
|
||||||
|
|
||||||
|
## Where the engine computes
|
||||||
|
|
||||||
|
The **gfx1151 iGPU**, as a co-tenant beside `frank-dragoon`: the Vulkan build of
|
||||||
|
`qwentts.cpp` with `--device Vulkan0`, and `/dev/dri` handed to the engine container. That is
|
||||||
|
the shape since 2026-10-19. The flag, the device name, the render-node trap on this hybrid box
|
||||||
|
and the one-line CPU rollback are owned by [[Frank TTS Engine]] — read that page before
|
||||||
|
touching either tenant, because a change to one is a change to both. Co-tenancy was gated with
|
||||||
|
`frank-dragoon` live on the card (its first token and decode rates unchanged with a synthesis
|
||||||
|
in flight); both gate recipes are §5 of `report/tts-npu-spike-verdict-2026-10-19.md`.
|
||||||
|
|
||||||
|
**The Strix Halo NPU is not a door at all**: XDNA2 executes only precompiled AIE partition
|
||||||
|
programs, and AMD's own Linux runtime opens `/dev/accel/accel0`, succeeds at the ioctls, then
|
||||||
|
declines this model family's graph without emitting a single log line. The measured record and
|
||||||
|
the model-side contract live in `report/` at the repo root
|
||||||
|
(`tts-npu-spike-verdict-2026-10-19.md`).
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
| Path | What |
|
| Path | What |
|
||||||
@@ -147,7 +163,7 @@ A `/health` probe job would therefore be structurally never-green, and because
|
|||||||
`LlmEndpointDown` is scoped across the whole `frank-*` job family (the rule set
|
`LlmEndpointDown` is scoped across the whole `frank-*` job family (the rule set
|
||||||
lives on [[Prometheus]]), a permanently red `frank-tts` target would page on
|
lives on [[Prometheus]]), a permanently red `frank-tts` target would page on
|
||||||
Signal every few minutes for an endpoint that is fine. The engine cannot be
|
Signal every few minutes for an endpoint that is fine. The engine cannot be
|
||||||
scraped at all: it publishes nothing. Status lives in `systemctl --user status`,
|
scraped at all: it publishes nothing. Status lives in `systemctl --user status`
|
||||||
`podman healthcheck run`, the two unit journals, and the homepage card, which
|
`podman healthcheck run`, the two unit journals, and the homepage card, which
|
||||||
probes `127.0.0.1:7772` for real now — the `wip: down` gate came off with the
|
probes `127.0.0.1:7772` for real now — the `wip: down` gate came off with the
|
||||||
2026-10-18 deploy.
|
2026-10-18 deploy.
|
||||||
@@ -155,7 +171,7 @@ probes `127.0.0.1:7772` for real now — the `wip: down` gate came off with the
|
|||||||
## The harness speech role
|
## The harness speech role
|
||||||
|
|
||||||
The operator harness reaches this endpoint through
|
The operator harness reaches this endpoint through
|
||||||
`~/.omp/agent/models.yml` — provider `frank-tts (dankclaw)`,
|
`~/.omp/agent/models.yml` — provider `frank-tts (dankclaw)`
|
||||||
`baseUrl: http://192.168.0.143:7772/v1`, `auth: none`, and the runner api
|
`baseUrl: http://192.168.0.143:7772/v1`, `auth: none`, and the runner api
|
||||||
`openai-speech` on the **model** row (naming a runner api at *provider* level
|
`openai-speech` on the **model** row (naming a runner api at *provider* level
|
||||||
fails the whole file's validation and disables every custom provider, which is
|
fails the whole file's validation and disables every custom provider, which is
|
||||||
@@ -171,7 +187,7 @@ harness bundle and measured the day of the flip:
|
|||||||
`speech.voice` is the TUI's **speak-aloud** voice — its enum is Kokoro's list,
|
`speech.voice` is the TUI's **speak-aloud** voice — its enum is Kokoro's list,
|
||||||
and it never reaches an HTTP request. So a harness call arrives voice-less by
|
and it never reaches an HTTP request. So a harness call arrives voice-less by
|
||||||
default, which is exactly why the endpoint grew `DEFAULT_VOICE`: the declared
|
default, which is exactly why the endpoint grew `DEFAULT_VOICE`: the declared
|
||||||
pack answers a voice-less request and the journal marks that line `(default)`,
|
pack answers a voice-less request and the journal marks that line `(default)`
|
||||||
while an **unknown** name still 400s with the list that exists.
|
while an **unknown** name still 400s with the list that exists.
|
||||||
- **`speech.voice` must stay a Kokoro name.** Setting it to a pack name does not
|
- **`speech.voice` must stay a Kokoro name.** Setting it to a pack name does not
|
||||||
select a pack; it hands the vocalizer a value outside its enum. (The first
|
select a pack; it hands the vocalizer a value outside its enum. (The first
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ The containers share the private bridge network `systemd-gitea`. The quadlet net
|
|||||||
| `dank/command-center` | second public repo on the instance (`GET http://127.0.0.1:3000/api/v1/repos/search` 2026-10-18) |
|
| `dank/command-center` | second public repo on the instance (`GET http://127.0.0.1:3000/api/v1/repos/search` 2026-10-18) |
|
||||||
| `dank/vllmScripts` | retired 2026-08-22 — merged into `dank/serverAppConfigs` (history preserved) *(unverified 2026-10-18: the anonymous API search returns no match and `/dank/vllmScripts` is 404, so whether the repo still exists privately cannot be checked without a token)* |
|
| `dank/vllmScripts` | retired 2026-08-22 — merged into `dank/serverAppConfigs` (history preserved) *(unverified 2026-10-18: the anonymous API search returns no match and `/dank/vllmScripts` is 404, so whether the repo still exists privately cannot be checked without a token)* |
|
||||||
|
|
||||||
The LLM fleet knowledge base: [serverAppConfigs-docs/LLM/index.md](../LLM/index.md)
|
The LLM fleet knowledge base:
|
||||||
|
|
||||||
## Management
|
## Management
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ entrypoint does not start the dashboard. The dashboard is a separate process, so
|
|||||||
it runs in its own Quadlet unit
|
it runs in its own Quadlet unit
|
||||||
([`dankclaw/hermes/quadlet/hermes-dashboard.container`](../../dankclaw/hermes/quadlet/hermes-dashboard.container):
|
([`dankclaw/hermes/quadlet/hermes-dashboard.container`](../../dankclaw/hermes/quadlet/hermes-dashboard.container):
|
||||||
image `localhost/hermes-agent:latest`, `Pull=never`, `Exec=hermes dashboard
|
image `localhost/hermes-agent:latest`, `Pull=never`, `Exec=hermes dashboard
|
||||||
--host 0.0.0.0 --port 9119 --no-open --skip-build`, `Restart=always`,
|
--host 0.0.0.0 --port 9119 --no-open --skip-build`, `Restart=always`
|
||||||
`RestartSec=5`). All three Hermes containers mount the same `/data` bind mount.
|
`RestartSec=5`). All three Hermes containers mount the same `/data` bind mount.
|
||||||
|
|
||||||
Unlike its two siblings — `hermes.container` and `hermes-webui.container` both
|
Unlike its two siblings — `hermes.container` and `hermes-webui.container` both
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ from another LAN host: `/health` → 200, `/` → 302) — **Vhost:**
|
|||||||
|
|
||||||
## Why a container, not a host process
|
## Why a container, not a host process
|
||||||
|
|
||||||
The gateway's state files are owned by the container-mapped uid (`109999`,
|
The gateway's state files are owned by the container-mapped uid (`109999`
|
||||||
from rootless Podman's `1 → 100000/65536` subuid range — `/etc/subuid` →
|
from rootless Podman's `1 → 100000/65536` subuid range — `/etc/subuid` →
|
||||||
`dank:100000:65536`, and the agent's runtime uid is 10000, so 100000 + 9999 =
|
`dank:100000:65536`, and the agent's runtime uid is 10000, so 100000 + 9999 =
|
||||||
109999; `ls -ld /home/dank/Containers/hermes/data` → uid 109999, 2026-10-18). A fresh rootless
|
109999; `ls -ld /home/dank/Containers/hermes/data` → uid 109999, 2026-10-18). A fresh rootless
|
||||||
@@ -82,12 +82,12 @@ the sibling dashboard on `9119` is not.
|
|||||||
|
|
||||||
- Dedicated `tunnel` account on Dankclaw (`getent passwd tunnel` → uid 994,
|
- Dedicated `tunnel` account on Dankclaw (`getent passwd tunnel` → uid 994,
|
||||||
shell `/usr/sbin/nologin`, 2026-10-18; password locked). Authentication is key-only
|
shell `/usr/sbin/nologin`, 2026-10-18; password locked). Authentication is key-only
|
||||||
*(unverified 2026-10-18: `/home/tunnel/.ssh/authorized_keys` is not readable
|
- (unverified 2026-10-18: `/home/tunnel/.ssh/authorized_keys` is not readable
|
||||||
without `sudo`)*; the account can do nothing except
|
without `sudo`)*; the account can do nothing except
|
||||||
authenticate — `ForceCommand /usr/sbin/nologin` kills any session channel
|
authenticate — `ForceCommand /usr/sbin/nologin` kills any session channel
|
||||||
while the transport stays up for port forwarding.
|
while the transport stays up for port forwarding.
|
||||||
- sshd drop-in `/etc/ssh/sshd_config.d/99-tunnel.conf` (exists, 118 B, 2026-08-31)
|
- sshd drop-in `/etc/ssh/sshd_config.d/99-tunnel.conf` (exists, 118 B, 2026-08-31)
|
||||||
scopes `AllowTcpForwarding yes` to that user only, plus `PermitTTY no`,
|
scopes `AllowTcpForwarding yes` to that user only, plus `PermitTTY no`
|
||||||
`X11Forwarding no` and `ForceCommand /usr/sbin/nologin` — all four lines are
|
`X11Forwarding no` and `ForceCommand /usr/sbin/nologin` — all four lines are
|
||||||
present under `Match User tunnel` (`cat` 2026-10-18).
|
present under `Match User tunnel` (`cat` 2026-10-18).
|
||||||
- Key options on the authorized key pin the blast radius:
|
- Key options on the authorized key pin the blast radius:
|
||||||
@@ -130,7 +130,7 @@ only if inactive (it is never restarted on deploy, so in-flight browser
|
|||||||
sessions survive), re-tags the image alias, then restarts
|
sessions survive), re-tags the image alias, then restarts
|
||||||
`hermes.service`, `hermes-dashboard.service` and
|
`hermes.service`, `hermes-dashboard.service` and
|
||||||
`hermes-webui.service` in the **user** manager of `dank` (linger must be on for
|
`hermes-webui.service` in the **user** manager of `dank` (linger must be on for
|
||||||
them to come up at boot — `loginctl show-user dank -p Linger` → `yes`,
|
them to come up at boot — `loginctl show-user dank -p Linger` → `yes`
|
||||||
2026-10-18), so gateway/dashboard/webui restarts stay isolated from
|
2026-10-18), so gateway/dashboard/webui restarts stay isolated from
|
||||||
inference deploys.
|
inference deploys.
|
||||||
Before first deploy, set a strong `HERMES_WEBUI_PASSWORD` in the live
|
Before first deploy, set a strong `HERMES_WEBUI_PASSWORD` in the live
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ bindings, 2026-10-18). Note the **container name drift**: the unit is
|
|||||||
`podman run --name systemd-%N …` (`systemctl --user cat hermes.service`), so
|
`podman run --name systemd-%N …` (`systemctl --user cat hermes.service`), so
|
||||||
`podman ps` shows the gateway as **`systemd-hermes`** — the repo unit sets no
|
`podman ps` shows the gateway as **`systemd-hermes`** — the repo unit sets no
|
||||||
`ContainerName=` key even though its header comment says "container name:
|
`ContainerName=` key even though its header comment says "container name:
|
||||||
hermes". The user-facing web surface is [[Hermes-WebUI]] on `:8787`, reached via
|
hermes". The user-facing web surface is [[Hermes-WebUI]] on `:8787`, reached
|
||||||
`hermes.franksdank.cloud` — internal-only by DNS absence (no public Porkbun A
|
`hermes.franksdank.cloud` — internal-only by DNS absence (no public Porkbun A
|
||||||
record; the name resolves from the Pi-hole hosts file,
|
record; the name resolves from the Pi-hole hosts file,
|
||||||
`dankclaw/pihole/addn-hosts:39`) — **Docs:**
|
`dankclaw/pihole/addn-hosts:39`) — **Docs:**
|
||||||
@@ -37,7 +37,7 @@ designed wiring; the remedy for both halves is in the log line
|
|||||||
|
|
||||||
**Persistent state:** `/home/dank/Containers/hermes/data` on Dankclaw, bind-mounted
|
**Persistent state:** `/home/dank/Containers/hermes/data` on Dankclaw, bind-mounted
|
||||||
at `/data` in all three Hermes containers (gateway, webui, dashboard —
|
at `/data` in all three Hermes containers (gateway, webui, dashboard —
|
||||||
`hermes.container:36`, `hermes-webui.container:46`,
|
`hermes.container:36`, `hermes-webui.container:46`
|
||||||
`hermes-dashboard.container:24`). Each
|
`hermes-dashboard.container:24`). Each
|
||||||
container additionally gets an anonymous volume at `/opt/data`, which is
|
container additionally gets an anonymous volume at `/opt/data`, which is
|
||||||
re-created empty on every container recreation — anything that lives only there
|
re-created empty on every container recreation — anything that lives only there
|
||||||
@@ -80,7 +80,7 @@ is not durable. The tracked, sanitized configuration baseline is
|
|||||||
`frank-dragoon` — verified from the host with `curl
|
`frank-dragoon` — verified from the host with `curl
|
||||||
http://127.0.0.1:7770/v1/models` → a single served id (2026-10-18; write
|
http://127.0.0.1:7770/v1/models` → a single served id (2026-10-18; write
|
||||||
`127.0.0.1`, not `localhost`, because that port is a rootless pasta publish
|
`127.0.0.1`, not `localhost`, because that port is a rootless pasta publish
|
||||||
and is IPv4-only);
|
and is IPv4-only)
|
||||||
`dankserver-small` reaches `http://192.168.0.235:7770/v1` and, **as of
|
`dankserver-small` reaches `http://192.168.0.235:7770/v1` and, **as of
|
||||||
2026-10-18, that endpoint serves only `frank-archon`** (`GET
|
2026-10-18, that endpoint serves only `frank-archon`** (`GET
|
||||||
http://192.168.0.235:7770/v1/models` → `[{"id":"frank-archon"}]`). The name
|
http://192.168.0.235:7770/v1/models` → `[{"id":"frank-archon"}]`). The name
|
||||||
@@ -115,7 +115,7 @@ per session; the browser itself lives in a dedicated daemon container, the
|
|||||||
digest-pinned headless-shell unit `chrome-devtools-protocol.container` (host
|
digest-pinned headless-shell unit `chrome-devtools-protocol.container` (host
|
||||||
network, loopback-only bind on 127.0.0.1:9222 — `ss -ltnp` 2026-10-18 shows
|
network, loopback-only bind on 127.0.0.1:9222 — `ss -ltnp` 2026-10-18 shows
|
||||||
`127.0.0.1:9222 headless-shell` and nothing on `0.0.0.0`; image
|
`127.0.0.1:9222 headless-shell` and nothing on `0.0.0.0`; image
|
||||||
`docker.io/chromedp/headless-shell@sha256:2d349b544a1ea6b5…`,
|
`docker.io/chromedp/headless-shell@sha256:2d349b544a1ea6b5…`
|
||||||
`dankclaw/hermes/quadlet/chrome-devtools-protocol.container:28`). Both consumers — the Signal
|
`dankclaw/hermes/quadlet/chrome-devtools-protocol.container:28`). Both consumers — the Signal
|
||||||
gateway and the webui server — run in the host network namespace (`podman
|
gateway and the webui server — run in the host network namespace (`podman
|
||||||
inspect` → `net=host` for both, 2026-10-18), so they can
|
inspect` → `net=host` for both, 2026-10-18), so they can
|
||||||
@@ -248,7 +248,7 @@ What actually breaks, and the fix:
|
|||||||
effort: none # mandatory, see below
|
effort: none # mandatory, see below
|
||||||
```
|
```
|
||||||
|
|
||||||
Before copying this: which id is served on which port is endpoint state — see
|
Before copying this: which id is served on which port is endpoint state —
|
||||||
[[frank-zealot]] and [[frank-archon]]. The provider/model
|
[[frank-zealot]] and [[frank-archon]]. The provider/model
|
||||||
pairing above is the shape that was landed in 2026; re-derive it from the live
|
pairing above is the shape that was landed in 2026; re-derive it from the live
|
||||||
config before reusing it.
|
config before reusing it.
|
||||||
@@ -282,7 +282,7 @@ Auth contract (verified live 2026-08-31, re-checked 2026-10-18): `POST /api/auth
|
|||||||
on match, 401 on miss. No username field. `/health` answers 200
|
on match, 401 on miss. No username field. `/health` answers 200
|
||||||
unauthenticated (`curl http://127.0.0.1:8787/health` → 200 with
|
unauthenticated (`curl http://127.0.0.1:8787/health` → 200 with
|
||||||
`{"status":"ok", …}` today) and `/` answers 302 (`curl -o /dev/null -w
|
`{"status":"ok", …}` today) and `/` answers 302 (`curl -o /dev/null -w
|
||||||
%{http_code} http://127.0.0.1:8787/` → 302 today) — **never verify auth via
|
%{http_code} http://127.0.0.1:8787/` → 302 today) — **never verify auth
|
||||||
those**; probe
|
those**; probe
|
||||||
login with a deliberately wrong password (expect 401) and the real one
|
login with a deliberately wrong password (expect 401) and the real one
|
||||||
(expect 200). Details on the webui surface: [[Hermes-WebUI]].
|
(expect 200). Details on the webui surface: [[Hermes-WebUI]].
|
||||||
@@ -311,7 +311,7 @@ login with a deliberately wrong password (expect 401) and the real one
|
|||||||
instead of loopback" — live bind `0.0.0.0:8787`, `ss -ltnp` 2026-10-18;
|
instead of loopback" — live bind `0.0.0.0:8787`, `ss -ltnp` 2026-10-18;
|
||||||
off-network ingress is SSH tunnel only —
|
off-network ingress is SSH tunnel only —
|
||||||
user `tunnel`, port **22** (`sshd -T` → `port 22`, 2026-10-18), key-only auth
|
user `tunnel`, port **22** (`sshd -T` → `port 22`, 2026-10-18), key-only auth
|
||||||
*(unverified 2026-10-18: the per-user `Match` block needs `sudo` to read)*,
|
- (unverified 2026-10-18: the per-user `Match` block needs `sudo` to read)*
|
||||||
forward remote end
|
forward remote end
|
||||||
localhost:8787).
|
localhost:8787).
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ returns a stripped, readable version of the page — no paywall chrome, no
|
|||||||
tracking, no cookie walls. It is a Laravel 13 app on the
|
tracking, no cookie walls. It is a Laravel 13 app on the
|
||||||
`serversideup/php:8.4-fpm-nginx` image (`composer.json` requires
|
`serversideup/php:8.4-fpm-nginx` image (`composer.json` requires
|
||||||
`php: ^8.4`, `laravel/framework: ^13.0`, `filament/filament: ^5.7` —
|
`php: ^8.4`, `laravel/framework: ^13.0`, `filament/filament: ^5.7` —
|
||||||
<https://raw.githubusercontent.com/manualdousuario/marreta/main/composer.json>),
|
<https://raw.githubusercontent.com/manualdousuario/marreta/main/composer.json>)
|
||||||
with an optional headless-browser companion for JavaScript-heavy pages.
|
with an optional headless-browser companion for JavaScript-heavy pages.
|
||||||
|
|
||||||
**Host:** Dankclaw — **vhost:** `marreta.franksdank.cloud` (Caddy, guard-chained)
|
**Host:** Dankclaw — **vhost:** `marreta.franksdank.cloud` (Caddy, guard-chained)
|
||||||
@@ -31,7 +31,7 @@ with an optional headless-browser companion for JavaScript-heavy pages.
|
|||||||
- Fetches a target URL with a cascade of strategies: plain HTTP (curl), a
|
- Fetches a target URL with a cascade of strategies: plain HTTP (curl), a
|
||||||
headless browser (Lightpanda) for JS pages, and the Internet Archive
|
headless browser (Lightpanda) for JS pages, and the Internet Archive
|
||||||
(Wayback) as a last resort — the three `app/Services/Fetch/*FetchStrategy.php`
|
(Wayback) as a last resort — the three `app/Services/Fetch/*FetchStrategy.php`
|
||||||
files upstream (`BrowserFetchStrategy`, `CurlFetchStrategy`,
|
files upstream (`BrowserFetchStrategy`, `CurlFetchStrategy`
|
||||||
`WaybackFetchStrategy`, dispatched by `app/Services/FetchManager.php`).
|
`WaybackFetchStrategy`, dispatched by `app/Services/FetchManager.php`).
|
||||||
- Strips paywall/consent/tracking markup and re-renders clean HTML.
|
- Strips paywall/consent/tracking markup and re-renders clean HTML.
|
||||||
- Ships a Filament admin panel at `/admin` (login = `ADMIN_EMAIL` /
|
- Ships a Filament admin panel at `/admin` (login = `ADMIN_EMAIL` /
|
||||||
@@ -69,7 +69,7 @@ graph LR
|
|||||||
| `marreta-browser.service` | `dankclaw/marreta/quadlet/marreta-browser.container` | `docker.io/lightpanda/browser:nightly` (`:26`) | `gluetun.container` (`:25`) | `9222` (CDP) |
|
| `marreta-browser.service` | `dankclaw/marreta/quadlet/marreta-browser.container` | `docker.io/lightpanda/browser:nightly` (`:26`) | `gluetun.container` (`:25`) | `9222` (CDP) |
|
||||||
|
|
||||||
Both units are boot-persistent (`WantedBy=default.target`) and self-healing
|
Both units are boot-persistent (`WantedBy=default.target`) and self-healing
|
||||||
(`Restart=always`, `RestartSec=10` — `marreta.container:62-69`,
|
(`Restart=always`, `RestartSec=10` — `marreta.container:62-69`
|
||||||
`marreta-browser.container:30-34`), per the repo's Boot Persistence rule. Live
|
`marreta-browser.container:30-34`), per the repo's Boot Persistence rule. Live
|
||||||
2026-10-18 both report `systemctl --user is-enabled` → `generated` and
|
2026-10-18 both report `systemctl --user is-enabled` → `generated` and
|
||||||
`is-active` → `active`, and `podman inspect` reports marreta **healthy**
|
`is-active` → `active`, and `podman inspect` reports marreta **healthy**
|
||||||
@@ -87,14 +87,14 @@ The fix is a single env var, verified against the serversideup source:
|
|||||||
|
|
||||||
- The image sets `ENV NGINX_HTTP_PORT="8080"` and the site template is
|
- The image sets `ENV NGINX_HTTP_PORT="8080"` and the site template is
|
||||||
`listen ${NGINX_HTTP_PORT} default_server;`.
|
`listen ${NGINX_HTTP_PORT} default_server;`.
|
||||||
- The image does **not** bake `conf.d/*.conf` (it `rm -rf`s them at build);
|
- The image does **not** bake `conf.d/*.conf` (it `rm -rf`s them at build)
|
||||||
the entrypoint `10-init-webserver-config.sh` runs `envsubst` over every
|
the entrypoint `10-init-webserver-config.sh` runs `envsubst` over every
|
||||||
`NGINX_*` variable **at container start**.
|
`NGINX_*` variable **at container start**.
|
||||||
- So `Environment=NGINX_HTTP_PORT=8085` in the quadlet relocates the listen
|
- So `Environment=NGINX_HTTP_PORT=8085` in the quadlet relocates the listen
|
||||||
port off the collision with no custom build.
|
port off the collision with no custom build.
|
||||||
|
|
||||||
Marreta then publishes **nothing** of its own — the netns owner (Gluetun)
|
Marreta then publishes **nothing** of its own — the netns owner (Gluetun)
|
||||||
publishes `8082:8085` on its behalf (`dankclaw/searxng/quadlet/gluetun.container:32`),
|
publishes `8082:8085` on its behalf (`dankclaw/searxng/quadlet/gluetun.container:32`)
|
||||||
exactly as it publishes `8081:8080` for SearXNG. Caddy dials
|
exactly as it publishes `8081:8080` for SearXNG. Caddy dials
|
||||||
`192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`). Live env confirms the
|
`192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`). Live env confirms the
|
||||||
relocation: `podman inspect marreta` carries `NGINX_HTTP_PORT=8085`
|
relocation: `podman inspect marreta` carries `NGINX_HTTP_PORT=8085`
|
||||||
@@ -127,7 +127,7 @@ on the mechanism: `BrowserFetchStrategy.php` only calls
|
|||||||
(`dankclaw/Caddyfile:159-163`).
|
(`dankclaw/Caddyfile:159-163`).
|
||||||
2. **Auth decision** — Anubis dials Caddy's `http://:8016`, which proxies
|
2. **Auth decision** — Anubis dials Caddy's `http://:8016`, which proxies
|
||||||
whole-request to `authentik-server:9000` (`dankclaw/Caddyfile:22-24,94`).
|
whole-request to `authentik-server:9000` (`dankclaw/Caddyfile:22-24,94`).
|
||||||
3. **Post-auth routing** — authenticated traffic returns to `http://:8017`,
|
3. **Post-auth routing** — authenticated traffic returns to `http://:8017`
|
||||||
where `@marreta host marreta.franksdank.cloud` routes to
|
where `@marreta host marreta.franksdank.cloud` routes to
|
||||||
`reverse_proxy 192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`).
|
`reverse_proxy 192.168.0.143:8082` (`dankclaw/Caddyfile:419-422`).
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ Marreta ships `trustProxies(at: '172.16.0.0/12,10.0.0.0/8,192.168.0.0/16,127.0.0
|
|||||||
— that is the `TRUSTED_PROXIES` fallback in upstream `bootstrap/app.php`, and
|
— that is the `TRUSTED_PROXIES` fallback in upstream `bootstrap/app.php`, and
|
||||||
this container sets no `TRUSTED_PROXIES`, so the default is what runs
|
this container sets no `TRUSTED_PROXIES`, so the default is what runs
|
||||||
(`podman inspect marreta` 2026-10-18) — and forces `https` when `APP_URL`
|
(`podman inspect marreta` 2026-10-18) — and forces `https` when `APP_URL`
|
||||||
starts with `https://` (`app/Providers/AppServiceProvider.php:23-24`,
|
starts with `https://` (`app/Providers/AppServiceProvider.php:23-24`
|
||||||
`URL::forceScheme('https')`). With
|
`URL::forceScheme('https')`). With
|
||||||
`APP_URL=https://marreta.franksdank.cloud` (`dankclaw/marreta/quadlet/marreta.container:41`) the app honors the forwarded
|
`APP_URL=https://marreta.franksdank.cloud` (`dankclaw/marreta/quadlet/marreta.container:41`) the app honors the forwarded
|
||||||
scheme and client IP through the whole chain, so there is no redirect loop
|
scheme and client IP through the whole chain, so there is no redirect loop
|
||||||
@@ -234,7 +234,7 @@ netns down and would otherwise orphan it).
|
|||||||
Deployed via `deploy.sh dankclaw --service gluetun --service marreta --service
|
Deployed via `deploy.sh dankclaw --service gluetun --service marreta --service
|
||||||
caddy` (gluetun first to recreate the tunnel with the new `8082:8085` publish,
|
caddy` (gluetun first to recreate the tunnel with the new `8082:8085` publish,
|
||||||
which also cycles SearXNG's netns ref). Authentik `ProxyProvider` `marreta`
|
which also cycles SearXNG's netns ref). Authentik `ProxyProvider` `marreta`
|
||||||
registered (pk 13, `external_host=https://marreta.franksdank.cloud/`,
|
registered (pk 13, `external_host=https://marreta.franksdank.cloud/`
|
||||||
`internal_host=http://caddy:8017`). Results:
|
`internal_host=http://caddy:8017`). Results:
|
||||||
|
|
||||||
- Unauth edge → Anubis challenge page (not 502). TLS cert issued via DNS-01.
|
- Unauth edge → Anubis challenge page (not 502). TLS cert issued via DNS-01.
|
||||||
@@ -245,14 +245,14 @@ registered (pk 13, `external_host=https://marreta.franksdank.cloud/`,
|
|||||||
(`GET 127.0.0.1:9222/json/version` → `Lightpanda/1.0`).
|
(`GET 127.0.0.1:9222/json/version` → `Lightpanda/1.0`).
|
||||||
- SearXNG unaffected: `:8081` → 200 after the tunnel recreate.
|
- SearXNG unaffected: `:8081` → 200 after the tunnel recreate.
|
||||||
|
|
||||||
> **Gotcha hit during deploy:** the first seed used a bare-base64 `APP_KEY`,
|
> **Gotcha hit during deploy:** the first seed used a bare-base64 `APP_KEY`
|
||||||
> which 500s every app route (see Secrets above). Fixed by re-keying with the
|
> which 500s every app route (see Secrets above). Fixed by re-keying with the
|
||||||
> `base64:` prefix before any encrypted data was persisted.
|
> `base64:` prefix before any encrypted data was persisted.
|
||||||
|
|
||||||
## Citations
|
## Citations
|
||||||
|
|
||||||
- Marreta upstream: <https://github.com/manualdousuario/marreta> (README,
|
- Marreta upstream: <https://github.com/manualdousuario/marreta> (README,
|
||||||
`compose.yml`, `bootstrap/app.php`, `app/Services/Fetch/BrowserFetchStrategy.php`,
|
`compose.yml`, `bootstrap/app.php`, `app/Services/Fetch/BrowserFetchStrategy.php`
|
||||||
`lang/` directory listing).
|
`lang/` directory listing).
|
||||||
- serversideup/php fpm-nginx port mechanism: `serversideup/docker-php`
|
- serversideup/php fpm-nginx port mechanism: `serversideup/docker-php`
|
||||||
`src/variations/fpm-nginx/Dockerfile` (`NGINX_HTTP_PORT="8080"`) and
|
`src/variations/fpm-nginx/Dockerfile` (`NGINX_HTTP_PORT="8080"`) and
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ The Ollama connection is disabled (`ollama.enable=false`).
|
|||||||
|
|
||||||
- Working dir for imports: `/app/backend`; package name `open_webui`
|
- Working dir for imports: `/app/backend`; package name `open_webui`
|
||||||
(underscore), NOT `openwebui`. `os.environ["WEBUI_SECRET_KEY"]` MUST be set
|
(underscore), NOT `openwebui`. `os.environ["WEBUI_SECRET_KEY"]` MUST be set
|
||||||
BEFORE importing any `open_webui` module (import-time hard requirement);
|
BEFORE importing any `open_webui` module (import-time hard requirement)
|
||||||
the secret lives at `/app/backend/.webui_secret_key`.
|
the secret lives at `/app/backend/.webui_secret_key`.
|
||||||
- **The `sqlite3` CLI is NOT in the container** — use Python's `sqlite3`
|
- **The `sqlite3` CLI is NOT in the container** — use Python's `sqlite3`
|
||||||
module (`podman exec open-webui python3 -c "import sqlite3;
|
module (`podman exec open-webui python3 -c "import sqlite3;
|
||||||
@@ -122,8 +122,8 @@ The Ollama connection is disabled (`ollama.enable=false`).
|
|||||||
pydantic `ModelModel` instances (attribute access, not `.get()`), with NO
|
pydantic `ModelModel` instances (attribute access, not `.get()`), with NO
|
||||||
`status`/`enabled` attribute — use `is_active` from the DB.
|
`status`/`enabled` attribute — use `is_active` from the DB.
|
||||||
- **API auth for verification:** `create_token` lives in
|
- **API auth for verification:** `create_token` lives in
|
||||||
`open_webui.utils.auth` (NOT `open_webui.utils.utils`);
|
`open_webui.utils.auth` (NOT `open_webui.utils.utils`)
|
||||||
`get_verified_user` requires an `id` claim (not just `sub`);
|
`get_verified_user` requires an `id` claim (not just `sub`)
|
||||||
`SESSION_SECRET = WEBUI_SECRET_KEY`. Mint an admin token inside the
|
`SESSION_SECRET = WEBUI_SECRET_KEY`. Mint an admin token inside the
|
||||||
container:
|
container:
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ The Ollama connection is disabled (`ollama.enable=false`).
|
|||||||
`ClientConnectorError`: a network-reachability problem (host LAN IP vs
|
`ClientConnectorError`: a network-reachability problem (host LAN IP vs
|
||||||
`host.containers.internal`), not a model problem.
|
`host.containers.internal`), not a model problem.
|
||||||
|
|
||||||
**Durability:** the model table + connections are DB-only (not git-managed);
|
**Durability:** the model table + connections are DB-only (not git-managed)
|
||||||
a fresh data volume loses them — including the public access grant above.
|
a fresh data volume loses them — including the public access grant above.
|
||||||
For reproducibility, add an idempotent first-start seed script or document
|
For reproducibility, add an idempotent first-start seed script or document
|
||||||
the setup in the service doc.
|
the setup in the service doc.
|
||||||
@@ -156,7 +156,7 @@ the setup in the service doc.
|
|||||||
## API access (auth-gated)
|
## API access (auth-gated)
|
||||||
|
|
||||||
Every `/api/*` endpoint requires an authenticated session (SSO login JWT)
|
Every `/api/*` endpoint requires an authenticated session (SSO login JWT)
|
||||||
or a per-user API key (`auth.enable_api_keys=true`; keys are minted per
|
or a per-user API key (`auth.enable_api_keys=true`; keys are minted
|
||||||
account under Profile → API Keys). Anonymous web visitors get the `pending`
|
account under Profile → API Keys). Anonymous web visitors get the `pending`
|
||||||
role (`ui.default_user_role=pending`), which cannot chat. Verified
|
role (`ui.default_user_role=pending`), which cannot chat. Verified
|
||||||
2026-10-07 from loopback `:3005` and over the public vhost:
|
2026-10-07 from loopback `:3005` and over the public vhost:
|
||||||
@@ -166,13 +166,12 @@ bogus bearer token); only `/api/config`, `/api/version` and `/health`
|
|||||||
`POST /api/chat/completions` all return **401** with no key (also with a bogus bearer
|
`POST /api/chat/completions` all return **401** with no key (also with a bogus bearer
|
||||||
token); only `/api/config`, `/api/version` and `/health` answer publicly, and none of
|
token); only `/api/config`, `/api/version` and `/health` answer publicly, and none of
|
||||||
them expose the model list. An external program needs an account + API key — there is
|
them expose the model list. An external program needs an account + API key — there is
|
||||||
no keyless access. Re-verified 2026-10-18 from loopback: `/health` → `{"status":true}`,
|
no keyless access. Re-verified 2026-10-18 from loopback: `/health` → `{"status":true}`
|
||||||
`/api/version` → 0.11.4, `/api/v1/models/list`, `/api/v1/models` and
|
`/api/version` → 0.11.4, `/api/v1/models/list`, `/api/v1/models` and
|
||||||
`POST /api/chat/completions` → 401; `/api/config` additionally publishes
|
`POST /api/chat/completions` → 401; `/api/config` additionally publishes
|
||||||
`features.enable_login_form=false`, `features.enable_signup=false` and
|
`features.enable_login_form=false`, `features.enable_signup=false` and
|
||||||
`oauth.providers.oidc="authentik"` — the SSO-only posture, visible from outside.
|
`oauth.providers.oidc="authentik"` — the SSO-only posture, visible from outside.
|
||||||
|
|
||||||
|
|
||||||
## Web search
|
## Web search
|
||||||
|
|
||||||
Chat web search is served by [[SearXNG]] on this host, wired in the quadlet
|
Chat web search is served by [[SearXNG]] on this host, wired in the quadlet
|
||||||
@@ -186,7 +185,7 @@ Chat web search is served by [[SearXNG]] on this host, wired in the quadlet
|
|||||||
The Anubis→Authentik guard chain is a browser mechanism (proof-of-work +
|
The Anubis→Authentik guard chain is a browser mechanism (proof-of-work +
|
||||||
session flow); a plain HTTP client cannot solve it, and the internal port
|
session flow); a plain HTTP client cannot solve it, and the internal port
|
||||||
bypasses Caddy entirely.
|
bypasses Caddy entirely.
|
||||||
- **Limiter:** SearXNG caps non-HTML (JSON) requests at 4/IP/hour (`API_MAX`,
|
- **Limiter:** SearXNG caps non-HTML (JSON) requests at 4/IP/hour (`API_MAX`
|
||||||
a hardcoded Python constant — no config knob). The `pass_ip` list is checked
|
a hardcoded Python constant — no config knob). The `pass_ip` list is checked
|
||||||
FIRST and short-circuits the whole botdetection chain, and OpenWebUI's bridge
|
FIRST and short-circuits the whole botdetection chain, and OpenWebUI's bridge
|
||||||
egress (`10.89.x`) is in `pass_ip`, so the cap never applies. Verified: an
|
egress (`10.89.x`) is in `pass_ip`, so the cap never applies. Verified: an
|
||||||
@@ -249,7 +248,6 @@ browser-driven and only touches the two exempt vhosts (`ai.` and `auth.`).
|
|||||||
restore the password form: set `ui.enable_login_form` back to `true` in the
|
restore the password form: set `ui.enable_login_form` back to `true` in the
|
||||||
Admin Panel (or DB) and drop the two env vars from the quadlet.
|
Admin Panel (or DB) and drop the two env vars from the quadlet.
|
||||||
|
|
||||||
|
|
||||||
## Boot persistence (Quadlet)
|
## Boot persistence (Quadlet)
|
||||||
|
|
||||||
The unit lives at `dankclaw/open-webui/quadlet/open-webui.container`. On the host it installs to `~/.config/containers/systemd/open-webui.container`. The generator turns it into `open-webui.service`. Because the unit sets `[Install] WantedBy=default.target` and user linger is on, it starts at boot. `systemctl --user is-enabled open-webui.service` reports `generated` (the quadlet boot-start state).
|
The unit lives at `dankclaw/open-webui/quadlet/open-webui.container`. On the host it installs to `~/.config/containers/systemd/open-webui.container`. The generator turns it into `open-webui.service`. Because the unit sets `[Install] WantedBy=default.target` and user linger is on, it starts at boot. `systemctl --user is-enabled open-webui.service` reports `generated` (the quadlet boot-start state).
|
||||||
@@ -296,7 +294,7 @@ TTS is the fleet's one OpenAI-compatible speech endpoint —
|
|||||||
`http://host.containers.internal:7772/v1` (`audio.tts.openai.api_base_url`
|
`http://host.containers.internal:7772/v1` (`audio.tts.openai.api_base_url`
|
||||||
in the config DB, 2026-10-18), model `frank-tts`, voice `narrator1`. That
|
in the config DB, 2026-10-18), model `frank-tts`, voice `narrator1`. That
|
||||||
port is the rootless quadlet `frank-tts` (this repo:
|
port is the rootless quadlet `frank-tts` (this repo:
|
||||||
`dankclaw/voiceservice/quadlet/frank-tts.container`,
|
`dankclaw/voiceservice/quadlet/frank-tts.container`
|
||||||
`scripts/frank-tts.env`), and its shim resolves `voice=<name>` against
|
`scripts/frank-tts.env`), and its shim resolves `voice=<name>` against
|
||||||
`/home/dank/voices/<name>/reference.wav` — a name without a reference is a
|
`/home/dank/voices/<name>/reference.wav` — a name without a reference is a
|
||||||
400 listing the voices that exist, never a silent fallback.
|
400 listing the voices that exist, never a silent fallback.
|
||||||
@@ -310,7 +308,7 @@ setting). Its engine select takes `""` (Default) or `browser-kokoro`; when
|
|||||||
`browser-kokoro` is chosen the page synthesizes locally and rejects any
|
`browser-kokoro` is chosen the page synthesizes locally and rejects any
|
||||||
voice outside its fixed pack — so `narrator1` fails with
|
voice outside its fixed pack — so `narrator1` fails with
|
||||||
`Voice: "narrator1" not found. Should be one of: af_heart, …` **without a
|
`Voice: "narrator1" not found. Should be one of: af_heart, …` **without a
|
||||||
single request reaching the server**. Personal audio settings are per
|
single request reaching the server**. Personal audio settings are
|
||||||
browser: no `user.settings` row and no container env carries an audio
|
browser: no `user.settings` row and no container env carries an audio
|
||||||
override, yet the toast still appears ⇒ the choice lives in that browser's
|
override, yet the toast still appears ⇒ the choice lives in that browser's
|
||||||
own storage. Fix in the UI: Settings → Audio → TTS Engine → **Default**,
|
own storage. Fix in the UI: Settings → Audio → TTS Engine → **Default**,
|
||||||
@@ -325,7 +323,6 @@ the failing click — that contrast is the instrument: a server-path failure
|
|||||||
leaves a log line and a frank-tts error, a `browser-kokoro` failure leaves
|
leaves a log line and a frank-tts error, a `browser-kokoro` failure leaves
|
||||||
neither.
|
neither.
|
||||||
|
|
||||||
|
|
||||||
## Retired instances
|
## Retired instances
|
||||||
|
|
||||||
- The compose `open-webui` service (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind.
|
- The compose `open-webui` service (port 3000) was never run: 3000 is Gitea's HTTP port, so it could not bind.
|
||||||
@@ -355,7 +352,7 @@ neither.
|
|||||||
began serving `frank-zealot` / `frank-dragoon` / `frank-archon`, so three
|
began serving `frank-zealot` / `frank-dragoon` / `frank-archon`, so three
|
||||||
id-keyed objects had to move in the same window as the alias flip:
|
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-zealot` → `frank-zealot`, `params`
|
||||||
`{"context":{"length":200704}}` and the `meta.description` kept),
|
`{"context":{"length":200704}}` and the `meta.description` kept)
|
||||||
`access_grant` (`resource_type='model'`, `resource_id`, principal
|
`access_grant` (`resource_type='model'`, `resource_id`, principal
|
||||||
`user`/`*`, permission `read`), and `openai.api_configs` per-backend
|
`user`/`*`, permission `read`), and `openai.api_configs` per-backend
|
||||||
`model_ids` (index 2 = `:7771` is the enabled family backend; indices 0/1
|
`model_ids` (index 2 = `:7771` is the enabled family backend; indices 0/1
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ never enter this repo. See [[Gitea]] and [[Authentik]].
|
|||||||
|
|
||||||
## Boot persistence (Quadlet)
|
## Boot persistence (Quadlet)
|
||||||
|
|
||||||
Both units carry `Restart=always` (`gitea-postgres.container:28`,
|
Both units carry `Restart=always` (`gitea-postgres.container:28`
|
||||||
`authentik-postgres.container:38`) and `[Install] WantedBy=default.target`
|
`authentik-postgres.container:38`) and `[Install] WantedBy=default.target`
|
||||||
(`gitea-postgres.container:31`, `authentik-postgres.container:41-42`), are installed
|
(`gitea-postgres.container:31`, `authentik-postgres.container:41-42`), are installed
|
||||||
under `~/.config/containers/systemd/`, and report `generated` / `active` with
|
under `~/.config/containers/systemd/`, and report `generated` / `active` with
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ engines:
|
|||||||
disabled: false
|
disabled: false
|
||||||
```
|
```
|
||||||
|
|
||||||
Key per-engine keys (each overwrites the engine-module default; see
|
Key per-engine keys (each overwrites the engine-module default
|
||||||
`settings_engines.html`):
|
`settings_engines.html`):
|
||||||
|
|
||||||
| Key | Effect |
|
| Key | Effect |
|
||||||
@@ -164,7 +164,7 @@ Key per-engine keys (each overwrites the engine-module default; see
|
|||||||
*different* category. When the primary `google` gets CAPTCHA'd it suspends, but
|
*different* category. When the primary `google` gets CAPTCHA'd it suspends, but
|
||||||
`google2` still returns results, and vice-versa.
|
`google2` still returns results, and vice-versa.
|
||||||
|
|
||||||
**Parser caveat (read before fiddling with `parsers`):** the modern `google`,
|
**Parser caveat (read before fiddling with `parsers`):** the modern `google`
|
||||||
`duckduckgo`, and `brave` engines ship a **custom `response()`** with hardcoded
|
`duckduckgo`, and `brave` engines ship a **custom `response()`** with hardcoded
|
||||||
XPath and *ignore* the generic `parsers`/`p_parsers` table. So `parsers:
|
XPath and *ignore* the generic `parsers`/`p_parsers` table. So `parsers:
|
||||||
[scrapy_html]` under `name: google` mostly does nothing. To make Google
|
[scrapy_html]` under `name: google` mostly does nothing. To make Google
|
||||||
@@ -376,14 +376,14 @@ done
|
|||||||
|
|
||||||
**2. `/config` REST endpoint** lists every engine + enabled/disabled +
|
**2. `/config` REST endpoint** lists every engine + enabled/disabled +
|
||||||
categories — an inventory of what is live. It answers 200 on this instance today
|
categories — an inventory of what is live. It answers 200 on this instance today
|
||||||
(`curl http://127.0.0.1:8081/config`, 2026-10-18; each entry carries `enabled`,
|
(`curl http://127.0.0.1:8081/config`, 2026-10-18; each entry carries `enabled`
|
||||||
`categories`, `timeout`, `shortcut`, and it is the source of the engine counts on
|
`categories`, `timeout`, `shortcut`, and it is the source of the engine counts on
|
||||||
[[SearXNG]]). `?engines=<name>` isolates a single
|
[[SearXNG]]). `?engines=<name>` isolates a single
|
||||||
provider; `/debug` adds engine diagnostics.
|
provider; `/debug` adds engine diagnostics.
|
||||||
|
|
||||||
**3. `check.searx.space`** — the hosted official checker probes every engine of
|
**3. `check.searx.space`** — the hosted official checker probes every engine of
|
||||||
your instance with a **headless Firefox** (catches Cloudflare/CAPTCHA blocks a
|
your instance with a **headless Firefox** (catches Cloudflare/CAPTCHA blocks a
|
||||||
plain HTTP probe misses) and posts results on searx.space. Add your instance via
|
plain HTTP probe misses) and posts results on searx.space. Add your instance
|
||||||
a GitHub issue in `searxng/searx-instances`, then allow `check.searx.space` IPs
|
a GitHub issue in `searxng/searx-instances`, then allow `check.searx.space` IPs
|
||||||
to reach the SearXNG port.
|
to reach the SearXNG port.
|
||||||
|
|
||||||
@@ -417,10 +417,10 @@ misleading).
|
|||||||
already replaced — checked 2026-10-18; see [[SearXNG]]). It is
|
already replaced — checked 2026-10-18; see [[SearXNG]]). It is
|
||||||
also immune to the shared-VPN-exit-IP blocking this page is about: it
|
also immune to the shared-VPN-exit-IP blocking this page is about: it
|
||||||
authenticates, so reputation of the exit IP is irrelevant.
|
authenticates, so reputation of the exit IP is irrelevant.
|
||||||
5. **Verify first** — `/json` + `?engines=google` and the searx-checker to see
|
5. **Verify first** — `/json` + `?engines=google` and the searx-checker to
|
||||||
exactly which engines are down and why. Confirm the running SearXNG version
|
exactly which engines are down and why. Confirm the running SearXNG version
|
||||||
(check whether `parsers`/`rate_limit` exist in the pinned build).
|
(check whether `parsers`/`rate_limit` exist in the pinned build).
|
||||||
6. **Use conservative request tuning** — Dankclaw now allows 5 seconds per
|
6. **Use conservative request tuning** — Dankclaw now allows 5 seconds
|
||||||
upstream request (`shared/searxng/settings.yml:26`), caps it at 15 seconds
|
upstream request (`shared/searxng/settings.yml:26`), caps it at 15 seconds
|
||||||
(`:27`), and retries once (`:32`). This addresses
|
(`:27`), and retries once (`:32`). This addresses
|
||||||
modest VPN latency without multiplying 429/CAPTCHA traffic. Adjust only
|
modest VPN latency without multiplying 429/CAPTCHA traffic. Adjust only
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ engines:
|
|||||||
disabled: true
|
disabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
The block above is the repo file (`shared/searxng/settings.yml`, 21 engine stanzas);
|
The block above is the repo file (`shared/searxng/settings.yml`, 21 engine stanzas)
|
||||||
the deployed render at `/opt/searxng/config/settings.yml` differs only in the baked
|
the deployed render at `/opt/searxng/config/settings.yml` differs only in the baked
|
||||||
`secret_key`, `base_url` and `KAGI_API_KEY` values, and the running engine set is
|
`secret_key`, `base_url` and `KAGI_API_KEY` values, and the running engine set is
|
||||||
censused below.
|
censused below.
|
||||||
@@ -154,7 +154,7 @@ what keeps the `trusted_proxies` entry meaningful.
|
|||||||
|
|
||||||
## Unblocking Google & Search Providers
|
## Unblocking Google & Search Providers
|
||||||
|
|
||||||
The current request policy allows a modest amount of VPN latency (`5.0s` per
|
The current request policy allows a modest amount of VPN latency (`5.0s`
|
||||||
request, `15.0s` maximum — `shared/searxng/settings.yml:26-27`) while limiting
|
request, `15.0s` maximum — `shared/searxng/settings.yml:26-27`) while limiting
|
||||||
retries to one (`:32`). This can recover slow
|
retries to one (`:32`). This can recover slow
|
||||||
providers, but it cannot fix an upstream CAPTCHA or 429 caused by the shared
|
providers, but it cannot fix an upstream CAPTCHA or 429 caused by the shared
|
||||||
@@ -169,10 +169,10 @@ upstream request volume. SearXNG **suspends** offending engines — which is wha
|
|||||||
up as "Google went unavailable". The default suspension is shorter than the folklore:
|
up as "Google went unavailable". The default suspension is shorter than the folklore:
|
||||||
see the table in [[SearXNG Providers]]. This is upstream, not local.
|
see the table in [[SearXNG Providers]]. This is upstream, not local.
|
||||||
|
|
||||||
Fixes (cheapest first): tune the `outgoing` block (`request_timeout`,
|
Fixes (cheapest first): tune the `outgoing` block (`request_timeout`
|
||||||
`retry_on_http_error`, `suspended_times`), duplicate the `google` engine under a
|
`retry_on_http_error`, `suspended_times`), duplicate the `google` engine under a
|
||||||
fresh `name` so a CAPTCHA ban on one leaves the other returning results, and —
|
fresh `name` so a CAPTCHA ban on one leaves the other returning results, and —
|
||||||
if Google still re-blocks — route the Google engines at a clean-IP proxy via
|
if Google still re-blocks — route the Google engines at a clean-IP proxy
|
||||||
per-engine `proxies` in `settings.yml`. See [[SearXNG Providers]] for the full
|
per-engine `proxies` in `settings.yml`. See [[SearXNG Providers]] for the full
|
||||||
root cause, the suspend-model table, the `engines:` config, and the
|
root cause, the suspend-model table, the `engines:` config, and the
|
||||||
`/json` verification recipe.
|
`/json` verification recipe.
|
||||||
@@ -212,17 +212,16 @@ root cause, the suspend-model table, the `engines:` config, and the
|
|||||||
(`searxng.container:21`), and that is one of the variables SearXNG substitutes at
|
(`searxng.container:21`), and that is one of the variables SearXNG substitutes at
|
||||||
load, so both paths carry the same LAN-IP value. Flagged, not fixed here: it is a
|
load, so both paths carry the same LAN-IP value. Flagged, not fixed here: it is a
|
||||||
config change, out of scope for docs.
|
config change, out of scope for docs.
|
||||||
- Keep the valkey cache container on the network SearXNG can actually reach — see
|
- Keep the valkey cache container on the network SearXNG can actually reach —
|
||||||
[[Valkey]] Gotchas: SearXNG itself has no network of its own (it borrows Gluetun's
|
[[Valkey]] Gotchas: SearXNG itself has no network of its own (it borrows Gluetun's
|
||||||
netns), so the network that matters is the one Gluetun and `searxng-valkey` share.
|
netns), so the network that matters is the one Gluetun and `searxng-valkey` share.
|
||||||
|
|
||||||
|
|
||||||
## Kagi (paid API engine)
|
## Kagi (paid API engine)
|
||||||
|
|
||||||
Added 2026-09-26 (user-ordered) and still enabled: the running build ships a native
|
Added 2026-09-26 (user-ordered) and still enabled: the running build ships a native
|
||||||
`kagi` engine module (`searx/engines/kagi.py` at the pinned commit `12f8b6515`),
|
`kagi` engine module (`searx/engines/kagi.py` at the pinned commit `12f8b6515`)
|
||||||
which sets `Authorization: Bearer <key>` (`kagi.py:103`) and issues
|
which sets `Authorization: Bearer <key>` (`kagi.py:103`) and issues
|
||||||
`POST https://kagi.com/api/v1/search` (`base_url` + `/api/v1/search`, `kagi.py:76,104,131`),
|
`POST https://kagi.com/api/v1/search` (`base_url` + `/api/v1/search`, `kagi.py:76,104,131`)
|
||||||
and declares `require_api_key: True` (`kagi.py:63`).
|
and declares `require_api_key: True` (`kagi.py:63`).
|
||||||
|
|
||||||
- **Enabled**: `weight: 2` alongside bing/duckduckgo, categories
|
- **Enabled**: `weight: 2` alongside bing/duckduckgo, categories
|
||||||
@@ -231,10 +230,10 @@ and declares `require_api_key: True` (`kagi.py:63`).
|
|||||||
`enabled: true`, categories `general, web` (2026-10-18).
|
`enabled: true`, categories `general, web` (2026-10-18).
|
||||||
- **Cost control**: `paging: false` — exactly one API credit per aggregated
|
- **Cost control**: `paging: false` — exactly one API credit per aggregated
|
||||||
query; `timeout: 8.0` so a slow paid query is not wasted (scrapers use 5.0)
|
query; `timeout: 8.0` so a slow paid query is not wasted (scrapers use 5.0)
|
||||||
(`shared/searxng/settings.yml:74-77`; upstream paging default is `True`,
|
(`shared/searxng/settings.yml:74-77`; upstream paging default is `True`
|
||||||
`kagi.py:67`, so the `false` here is a real override).
|
`kagi.py:67`, so the `false` here is a real override).
|
||||||
- **Key handling**: the repo file keeps `api_key: "${KAGI_API_KEY}"`;
|
- **Key handling**: the repo file keeps `api_key: "${KAGI_API_KEY}"`
|
||||||
`deploy-remote.sh` bakes the real key into the live settings.yml via
|
`deploy-remote.sh` bakes the real key into the live settings.yml
|
||||||
`scripts/searxng-render-settings.py`, reading `/opt/searxng/.env`
|
`scripts/searxng-render-settings.py`, reading `/opt/searxng/.env`
|
||||||
(owner `dank`, mode 0600 — `ls -l /opt/searxng/.env` 2026-10-18; its key names are
|
(owner `dank`, mode 0600 — `ls -l /opt/searxng/.env` 2026-10-18; its key names are
|
||||||
the WireGuard set plus `SEARXNG_SECRET`, `SEARXNG_BASE_URL`, `KAGI_API_KEY`).
|
the WireGuard set plus `SEARXNG_SECRET`, `SEARXNG_BASE_URL`, `KAGI_API_KEY`).
|
||||||
@@ -262,15 +261,15 @@ and declares `require_api_key: True` (`kagi.py:63`).
|
|||||||
|
|
||||||
- 262 engines in the build, **75 enabled**, categories of the enabled set led by
|
- 262 engines in the build, **75 enabled**, categories of the enabled set led by
|
||||||
`other` (12), `it` (11), `general` (9), `videos` (8), `stock images` (7).
|
`other` (12), `it` (11), `general` (9), `videos` (8), `stock images` (7).
|
||||||
- Enabled and matching the policy page: `bing`, `duckduckgo`, `wikipedia`, `kagi`,
|
- Enabled and matching the policy page: `bing`, `duckduckgo`, `wikipedia`, `kagi`
|
||||||
`github`, `mdn`, `stackoverflow`, `askubuntu`, `superuser`, `docker hub`, `pypi`,
|
`github`, `mdn`, `stackoverflow`, `askubuntu`, `superuser`, `docker hub`, `pypi`
|
||||||
`arxiv`, `pubmed`, `semantic scholar`, `reuters`, `bing news`, `duckduckgo news`,
|
`arxiv`, `pubmed`, `semantic scholar`, `reuters`, `bing news`, `duckduckgo news`
|
||||||
`wikinews`, `bing images`, `duckduckgo images/videos/news`, `wikicommons.*`,
|
`wikinews`, `bing images`, `duckduckgo images/videos/news`, `wikicommons.*`
|
||||||
`youtube`, `vimeo`, `dailymotion`, `bing videos`.
|
`youtube`, `vimeo`, `dailymotion`, `bing videos`.
|
||||||
- Disabled, as configured: `google`, `google cse`, `google news`, `google scholar`,
|
- Disabled, as configured: `google`, `google cse`, `google news`, `google scholar`
|
||||||
`brave`, `brave.images`, `brave.news`, `brave.videos`, `qwant`.
|
`brave`, `brave.images`, `brave.news`, `brave.videos`, `qwant`.
|
||||||
- The three `startpage` stanzas in `shared/searxng/settings.yml:113-124` target
|
- The three `startpage` stanzas in `shared/searxng/settings.yml:113-124` target
|
||||||
engines that **no longer exist in this build** — `/config` lists no `startpage`,
|
engines that **no longer exist in this build** — `/config` lists no `startpage`
|
||||||
`startpage images` or `startpage news`, so those overrides are no-ops kept as
|
`startpage images` or `startpage news`, so those overrides are no-ops kept as
|
||||||
intent. *(labelled 2026-10-18)*
|
intent. *(labelled 2026-10-18)*
|
||||||
|
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ frank-dragoon `health=200` throughout.
|
|||||||
The previous 8 GiB `/swap.img` (dated 2026-07-21) was staged, not deleted, at
|
The previous 8 GiB `/swap.img` (dated 2026-07-21) was staged, not deleted, at
|
||||||
`dankclaw:~/.AGENTTRASH/20260912-swapfile-8g/swap.img` (8.1 GiB). **That staging
|
`dankclaw:~/.AGENTTRASH/20260912-swapfile-8g/swap.img` (8.1 GiB). **That staging
|
||||||
directory no longer exists** — `ls /home/dank/.AGENTTRASH` on 2026-10-18 lists only
|
directory no longer exists** — `ls /home/dank/.AGENTTRASH` on 2026-10-18 lists only
|
||||||
`20261005-frank-small-ubatch-ab`, `20261006-frank-large-tuning`,
|
`20261005-frank-small-ubatch-ab`, `20261006-frank-large-tuning`
|
||||||
`20261007-openwebui-modelpicker`, `20261010-fleet-rename`, `20261017-bonsai-scratch`,
|
`20261007-openwebui-modelpicker`, `20261010-fleet-rename`, `20261017-bonsai-scratch`
|
||||||
`20261017-prism-build`, `20261017-v2abl-trial`, and there is no `/root/.AGENTTRASH`.
|
`20261017-prism-build`, `20261017-v2abl-trial`, and there is no `/root/.AGENTTRASH`.
|
||||||
The old swapfile has therefore been purged; nothing on the host still holds it.
|
The old swapfile has therefore been purged; nothing on the host still holds it.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ voice is how a wrong-voice endpoint stays wrong for weeks.
|
|||||||
state that stays out of the repo, mounted read-only at `/voices` by the façade
|
state that stays out of the repo, mounted read-only at `/voices` by the façade
|
||||||
unit — **Builder:** `scripts/build-voice-pack.sh` with tunables in
|
unit — **Builder:** `scripts/build-voice-pack.sh` with tunables in
|
||||||
`scripts/voice-packs.env` — **Seam:** `tests/voice_pack_unit.bats` — **Engine:**
|
`scripts/voice-packs.env` — **Seam:** `tests/voice_pack_unit.bats` — **Engine:**
|
||||||
arm A `qwentts.cpp`, decided in [[Local TTS and Voice Cloning]].
|
arm A `qwentts.cpp`, decided in .
|
||||||
|
|
||||||
Packs are **world-readable on purpose** (`drwxr-xr-x`, owner `dank`): the façade
|
Packs are **world-readable on purpose** (`drwxr-xr-x`, owner `dank`): the façade
|
||||||
container runs as uid 10000 and reads the mount from outside its own uid map.
|
container runs as uid 10000 and reads the mount from outside its own uid map.
|
||||||
@@ -137,7 +137,7 @@ invariant to every crop in the tracked pack list.
|
|||||||
1. Pick the source file and hash it: `sha256sum /path/to/sample.wav`. A crop
|
1. Pick the source file and hash it: `sha256sum /path/to/sample.wav`. A crop
|
||||||
offset is a claim about one exact file, so the digest goes in the env with it.
|
offset is a claim about one exact file, so the digest goes in the env with it.
|
||||||
2. Find a speech-dense stretch **past 10 s**. The trial's recipe, applied once:
|
2. Find a speech-dense stretch **past 10 s**. The trial's recipe, applied once:
|
||||||
`ffmpeg -ss 60 -t 600 -i SRC -af silencedetect=noise=-35dB:d=0.6 -f null -`,
|
`ffmpeg -ss 60 -t 600 -i SRC -af silencedetect=noise=-35dB:d=0.6 -f null -`
|
||||||
build the runs of non-silence, keep runs ≥ 18 s, take the longest (earliest
|
build the runs of non-silence, keep runs ≥ 18 s, take the longest (earliest
|
||||||
on ties), and cap at 25 s. Aim for 20–25 s: shorter than 19.84 s and the
|
on ties), and cap at 25 s. Aim for 20–25 s: shorter than 19.84 s and the
|
||||||
`.rvq` falls under the floor.
|
`.rvq` falls under the floor.
|
||||||
@@ -188,15 +188,15 @@ loud, not a bug. Work through it in order:
|
|||||||
1. Re-read the tool's own numbers rather than trusting this page: `qwen-codec`
|
1. Re-read the tool's own numbers rather than trusting this page: `qwen-codec`
|
||||||
prints `SpeakerEncoder] Loaded: enc_dim=…` and `Pipeline] Ready: hop … , …
|
prints `SpeakerEncoder] Loaded: enc_dim=…` and `Pipeline] Ready: hop … , …
|
||||||
codebooks @ … Hz` on every run, and the pinned tree's
|
codebooks @ … Hz` on every run, and the pinned tree's
|
||||||
`tools/qwen-codec.cpp` documents the packing. `enc_dim × 4` is the `.spk`;
|
`tools/qwen-codec.cpp` documents the packing. `enc_dim × 4` is the `.spk`
|
||||||
`codebooks × code_bits / 8` bytes per frame × frames is the `.rvq`.
|
`codebooks × code_bits / 8` bytes per frame × frames is the `.rvq`.
|
||||||
2. Decide whether the new number is intended. A changed `enc_dim` means the old
|
2. Decide whether the new number is intended. A changed `enc_dim` means the old
|
||||||
`.spk` files are unreadable by the new engine, so **every** pack must be
|
`.spk` files are unreadable by the new engine, so **every** pack must be
|
||||||
rebuilt, not just the failing one.
|
rebuilt, not just the failing one.
|
||||||
3. Update the four arithmetic lines in `scripts/voice-packs.env`
|
3. Update the four arithmetic lines in `scripts/voice-packs.env`
|
||||||
(`VOICE_PACK_SPK_BYTES`, `VOICE_PACK_RVQ_MIN_BYTES`, `VOICE_PACK_RVQ_MAX_BYTES`,
|
(`VOICE_PACK_SPK_BYTES`, `VOICE_PACK_RVQ_MIN_BYTES`, `VOICE_PACK_RVQ_MAX_BYTES`
|
||||||
and `VOICE_PACK_RVQ_BYTES_PER_FRAME` if the codebook packing changed) **and**
|
and `VOICE_PACK_RVQ_BYTES_PER_FRAME` if the codebook packing changed) **and**
|
||||||
`VOICE_PACK_ENGINE_COMMIT`, `VOICE_PACK_TOOL`, `VOICE_PACK_CODEC_MODEL`,
|
`VOICE_PACK_ENGINE_COMMIT`, `VOICE_PACK_TOOL`, `VOICE_PACK_CODEC_MODEL`
|
||||||
`VOICE_PACK_TALKER_MODEL` to the new build. An env diff that moves the sizes
|
`VOICE_PACK_TALKER_MODEL` to the new build. An env diff that moves the sizes
|
||||||
without moving the commit is the mistake to avoid.
|
without moving the commit is the mistake to avoid.
|
||||||
4. Rebuild all three packs with `--force` (the retired directories keep the old
|
4. Rebuild all three packs with `--force` (the retired directories keep the old
|
||||||
@@ -226,7 +226,7 @@ loud, not a bug. Work through it in order:
|
|||||||
narrator1 and narrator2). The builder's first real run is therefore a
|
narrator1 and narrator2). The builder's first real run is therefore a
|
||||||
migration, and it should be a `--force` run per voice so the old directories
|
migration, and it should be a `--force` run per voice so the old directories
|
||||||
are retired rather than left half-updated.
|
are retired rather than left half-updated.
|
||||||
- The three source files (`/home/blank/audiosample.mp3`,
|
- The three source files (`/home/blank/audiosample.mp3`
|
||||||
`audiosample2.m4b`, `audiosample3.m4b`, digests pinned in
|
`audiosample2.m4b`, `audiosample3.m4b`, digests pinned in
|
||||||
`scripts/voice-packs.env`) live on the workstation, not on Dankclaw; the
|
`scripts/voice-packs.env`) live on the workstation, not on Dankclaw; the
|
||||||
builder reads them from `$VOICE_PACK_SOURCE_DIR=/home/dank/voice-sources`, so
|
builder reads them from `$VOICE_PACK_SOURCE_DIR=/home/dank/voice-sources`, so
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ image's own nginx (the desktop), so the content server runs on internal 8090.
|
|||||||
**Auth model (calibre 9.x):** users live in a SQLite userdb whose `pw` column
|
**Auth model (calibre 9.x):** users live in a SQLite userdb whose `pw` column
|
||||||
is **plaintext** — the file is mode 600. The single admin account is
|
is **plaintext** — the file is mode 600. The single admin account is
|
||||||
bootstrapped once by the deploy handler
|
bootstrapped once by the deploy handler
|
||||||
(`deploy_calibre_server` in `scripts/deploy-remote.sh`) via
|
(`deploy_calibre_server` in `scripts/deploy-remote.sh`)
|
||||||
`calibre-server --manage-users -- add`, the password piped on stdin with
|
`calibre-server --manage-users -- add`, the password piped on stdin with
|
||||||
`printf` (a trailing newline from `echo` lands in the stored password and
|
`printf` (a trailing newline from `echo` lands in the stored password and
|
||||||
breaks every login). The desktop's basic auth (`CUSTOM_USER`/`CUSTOM_PASSWORD`)
|
breaks every login). The desktop's basic auth (`CUSTOM_USER`/`CUSTOM_PASSWORD`)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ The `iv` edition is no longer available for arm32/armv6. Use `dj` for maximum co
|
|||||||
|
|
||||||
## Volumes
|
## Volumes
|
||||||
|
|
||||||
Live deployment: the whole LUKS array is served at URL `/w` via
|
Live deployment: the whole LUKS array is served at URL `/w`
|
||||||
`Volume=/mnt/entropy:/w:ro` in the quadlet unit ([[Entropy Volume]]) —
|
`Volume=/mnt/entropy:/w:ro` in the quadlet unit ([[Entropy Volume]]) —
|
||||||
read-only **at the mount level**, so no containerized consumer can write into
|
read-only **at the mount level**, so no containerized consumer can write into
|
||||||
the array through this path regardless of account permissions. The ac edition
|
the array through this path regardless of account permissions. The ac edition
|
||||||
@@ -111,7 +111,7 @@ convention; see [[#Command-line interface (ac edition)]] above.
|
|||||||
```
|
```
|
||||||
|
|
||||||
`${COPAY_ACCOUNT}`/`${COPAY_PASSWORD}` are **never** written into the unit:
|
`${COPAY_ACCOUNT}`/`${COPAY_PASSWORD}` are **never** written into the unit:
|
||||||
they live in the host-local `/home/dank/Containers/copyparty/env` (0600),
|
they live in the host-local `/home/dank/Containers/copyparty/env` (0600)
|
||||||
loaded via `[Service] EnvironmentFile=` so systemd expands them into
|
loaded via `[Service] EnvironmentFile=` so systemd expands them into
|
||||||
`Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD}` at start. The password still
|
`Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD}` at start. The password still
|
||||||
lands in the container argv — visible to anyone who can run
|
lands in the container argv — visible to anyone who can run
|
||||||
@@ -178,6 +178,6 @@ Upgrade to v1.20.19+ to cover all known advisories. The dirkey fix (v1.20.17) ma
|
|||||||
## Citations
|
## Citations
|
||||||
|
|
||||||
- GitHub Security Advisories, `9001/copyparty` —
|
- GitHub Security Advisories, `9001/copyparty` —
|
||||||
https://github.com/9001/copyparty/security/advisories (accessed 2026-10-19);
|
https://github.com/9001/copyparty/security/advisories (accessed 2026-10-19)
|
||||||
each row's patched version read from `first_patched_version` in the
|
each row's patched version read from `first_patched_version` in the
|
||||||
`https://api.github.com/advisories/<GHSA-id>` response.
|
`https://api.github.com/advisories/<GHSA-id>` response.
|
||||||
@@ -54,5 +54,5 @@ The following services ran on Dankserver and are now retired from this stack:
|
|||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[index]] — repository index
|
- repository index
|
||||||
- [[Network Architecture]] — routing and proxy
|
- [[Network Architecture]] — routing and proxy
|
||||||
@@ -14,7 +14,7 @@ serving music, movies, and shows over LAN. It replaced the earlier research
|
|||||||
brief; it runs alongside [[Plex]], which remains the primary server for some
|
brief; it runs alongside [[Plex]], which remains the primary server for some
|
||||||
clients.
|
clients.
|
||||||
|
|
||||||
**Host:** Dankserver (192.168.0.235) — **Access:** `http://192.168.0.235:8096`,
|
**Host:** Dankserver (192.168.0.235) — **Access:** `http://192.168.0.235:8096`
|
||||||
public vhost `jelly.franksdank.cloud` behind the [[Anubis]] guard chain →
|
public vhost `jelly.franksdank.cloud` behind the [[Anubis]] guard chain →
|
||||||
[[Authentik]] outpost (post-auth route proxies `localhost:8096`).
|
[[Authentik]] outpost (post-auth route proxies `localhost:8096`).
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ user to verify the password.
|
|||||||
- LDAP provider `Jellyfin LDAP` (Base DN `dc=franksdank,dc=cloud`, bind/search
|
- LDAP provider `Jellyfin LDAP` (Base DN `dc=franksdank,dc=cloud`, bind/search
|
||||||
mode `direct`) + application `Jellyfin`.
|
mode `direct`) + application `Jellyfin`.
|
||||||
- LDAP outpost `Jellyfin LDAP Outpost` → container `authentik-outpost-ldap`
|
- LDAP outpost `Jellyfin LDAP Outpost` → container `authentik-outpost-ldap`
|
||||||
(quadlet `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`),
|
(quadlet `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`)
|
||||||
publishing `389`/`636` on the Dankclaw host for cross-host binds.
|
publishing `389`/`636` on the Dankclaw host for cross-host binds.
|
||||||
- Service account `svc-jellyfin-ldap` (DN
|
- Service account `svc-jellyfin-ldap` (DN
|
||||||
`cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud`).
|
`cn=svc-jellyfin-ldap,ou=users,dc=franksdank,dc=cloud`).
|
||||||
@@ -80,7 +80,7 @@ user to verify the password.
|
|||||||
**Jellyfin side** (plugin config, set via admin → Plugins → LDAP-Auth):
|
**Jellyfin side** (plugin config, set via admin → Plugins → LDAP-Auth):
|
||||||
- Server `192.168.0.143:389` (plain LDAP over the trusted LAN), Base DN
|
- Server `192.168.0.143:389` (plain LDAP over the trusted LAN), Base DN
|
||||||
`dc=franksdank,dc=cloud`, bind user = the service-account DN, search
|
`dc=franksdank,dc=cloud`, bind user = the service-account DN, search
|
||||||
attributes `uid,cn,mail,displayName`, uid attr `uid`, username attr `cn`,
|
attributes `uid,cn,mail,displayName`, uid attr `uid`, username attr `cn`
|
||||||
`CreateUsersFromLdap=true`, all folders enabled.
|
`CreateUsersFromLdap=true`, all folders enabled.
|
||||||
|
|
||||||
**Gotcha — the bind credential is a PASSWORD, not a token.** The LDAP bind is
|
**Gotcha — the bind credential is a PASSWORD, not a token.** The LDAP bind is
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ root runs as the `dank` user, and the deployer adopts the data root with
|
|||||||
## Integration points
|
## Integration points
|
||||||
|
|
||||||
- **Caddy:** Plex is exempt from the Anubis bot challenge
|
- **Caddy:** Plex is exempt from the Anubis bot challenge
|
||||||
(plain-HTTP legacy clients cannot solve JS challenges) — see
|
(plain-HTTP legacy clients cannot solve JS challenges) —
|
||||||
[[Anubis]] "Exempt by construction".
|
[[Anubis]] "Exempt by construction".
|
||||||
- **Tautulli** ([[Tautulli]]) is the companion: it polls PMS on the appnet
|
- **Tautulli** ([[Tautulli]]) is the companion: it polls PMS on the appnet
|
||||||
for statistics and notifications and makes no sense without Plex.
|
for statistics and notifications and makes no sense without Plex.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Anubis → Authentik chain ([[Caddy]] routes it post-auth to
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Current RomM releases require MariaDB plus Redis (or Valkey) for the task
|
Current RomM releases require MariaDB plus Redis (or Valkey) for the task
|
||||||
queue; this deployment runs **redis** (`REDIS_HOST=redis` in the live unit),
|
queue; this deployment runs **redis** (`REDIS_HOST=redis` in the live unit)
|
||||||
not valkey.
|
not valkey.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -114,7 +114,7 @@ scan:
|
|||||||
## Deployment (Quadlet)
|
## Deployment (Quadlet)
|
||||||
|
|
||||||
Since 2026-09-01 the trio runs as repo-tracked quadlet units, deployed with
|
Since 2026-09-01 the trio runs as repo-tracked quadlet units, deployed with
|
||||||
`./scripts/deploy.sh dankserver --service romm` (any of `romm`,
|
`./scripts/deploy.sh dankserver --service romm` (any of `romm`
|
||||||
`romm-mariadb`, `romm-redis` triggers the full ordered cutover):
|
`romm-mariadb`, `romm-redis` triggers the full ordered cutover):
|
||||||
|
|
||||||
- Units: `dankserver/romm/quadlet/{romm,romm-mariadb,romm-redis}.container`
|
- Units: `dankserver/romm/quadlet/{romm,romm-mariadb,romm-redis}.container`
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ Verified: `msg status` returns live JSON state; unknown panel ids return an
|
|||||||
error listing all valid panel ids (clipboard, control-center, launcher,
|
error listing all valid panel ids (clipboard, control-center, launcher,
|
||||||
polkit, session, setup-wizard, test, tray-drawer, wallpaper).
|
polkit, session, setup-wizard, test, tray-drawer, wallpaper).
|
||||||
|
|
||||||
Verified v5 msg verb grammar (keybinds-safe): `panel-toggle <id>`,
|
Verified v5 msg verb grammar (keybinds-safe): `panel-toggle <id>`
|
||||||
`session lock|suspend|lock-and-suspend|logout|reboot|shutdown`,
|
`session lock|suspend|lock-and-suspend|logout|reboot|shutdown`
|
||||||
`volume-up [step]`, `volume-down`, `volume-mute`, `mic-mute`,
|
`volume-up [step]`, `volume-down`, `volume-mute`, `mic-mute`
|
||||||
`media next|previous|playPause` (graceful no-op, rc 0, with no active
|
`media next|previous|playPause` (graceful no-op, rc 0, with no active
|
||||||
player), `brightness-up [target] [step]`, `brightness-down`,
|
player), `brightness-up [target] [step]`, `brightness-down`
|
||||||
`settings-open [context]`, `settings-toggle [context]`, `config-reload`,
|
`settings-open [context]`, `settings-toggle [context]`, `config-reload`
|
||||||
`wallpaper-get [connector]`, `wallpaper-set <path>`, `wallpaper-next`,
|
`wallpaper-get [connector]`, `wallpaper-set <path>`, `wallpaper-next`
|
||||||
`color-scheme-get|set`, `theme-mode-get`, `templates-apply`,
|
`color-scheme-get|set`, `theme-mode-get`, `templates-apply`
|
||||||
`screenshot-fullscreen`.
|
`screenshot-fullscreen`.
|
||||||
|
|
||||||
**Wallpaper directory semantics:** empty/unset `[wallpaper] directory`
|
**Wallpaper directory semantics:** empty/unset `[wallpaper] directory`
|
||||||
@@ -81,9 +81,9 @@ Custom palettes live at `~/.config/noctalia/palettes/<Name>.json`
|
|||||||
synced. Missing/invalid file → silent fallback to the builtin palette.
|
synced. Missing/invalid file → silent fallback to the builtin palette.
|
||||||
|
|
||||||
Palette JSON shape = the community file's shape: `dark`/`light` maps of
|
Palette JSON shape = the community file's shape: `dark`/`light` maps of
|
||||||
`mPrimary`, `mOnPrimary`, `mSecondary`, `mTertiary`, `mError`, `mSurface`,
|
`mPrimary`, `mOnPrimary`, `mSecondary`, `mTertiary`, `mError`, `mSurface`
|
||||||
`mSurfaceVariant`, `mOutline`, `mShadow`, `mHover` plus a `terminal` block
|
`mSurfaceVariant`, `mOutline`, `mShadow`, `mHover` plus a `terminal` block
|
||||||
(`foreground`, `background`, `normal.{black..white}`, `bright.{…}`,
|
(`foreground`, `background`, `normal.{black..white}`, `bright.{…}`
|
||||||
`cursor`, `cursorText`, `selectionFg`, `selectionBg`). NOTE: `noctalia theme
|
`cursor`, `cursorText`, `selectionFg`, `selectionBg`). NOTE: `noctalia theme
|
||||||
-o` emits M3 snake_case tokens (`on_primary_container`, …) — that is the
|
-o` emits M3 snake_case tokens (`on_primary_container`, …) — that is the
|
||||||
*generator* format, not this one. Copy an existing community JSON and mutate
|
*generator* format, not this one. Copy an existing community JSON and mutate
|
||||||
@@ -114,7 +114,7 @@ Procedure:
|
|||||||
`noctalia msg color-scheme-set custom "Gruber Red"` → `ok`; confirm with
|
`noctalia msg color-scheme-set custom "Gruber Red"` → `ok`; confirm with
|
||||||
`color-scheme-get`.
|
`color-scheme-get`.
|
||||||
5. Re-render every configured template: `noctalia msg templates-apply`.
|
5. Re-render every configured template: `noctalia msg templates-apply`.
|
||||||
Regenerates `~/.config/ghostty/themes/noctalia`,
|
Regenerates `~/.config/ghostty/themes/noctalia`
|
||||||
`~/.config/gtk-{3,4}.0/noctalia.css`, kcolorscheme, foot, wezterm,
|
`~/.config/gtk-{3,4}.0/noctalia.css`, kcolorscheme, foot, wezterm,
|
||||||
starship, etc. Symlinked-into-dotfiles paths show up as modified files
|
starship, etc. Symlinked-into-dotfiles paths show up as modified files
|
||||||
worth committing.
|
worth committing.
|
||||||
@@ -216,7 +216,7 @@ Verified on blankpad, niri 26.04 (two PM1601 externals + eDP-1 laptop panel).
|
|||||||
scale, transform, logical pos/size) and `niri --version`. Output **names are
|
scale, transform, logical pos/size) and `niri --version`. Output **names are
|
||||||
connector names** (`eDP-1`, `DP-2`, `HDMI-A-1`), not EDID model strings —
|
connector names** (`eDP-1`, `DP-2`, `HDMI-A-1`), not EDID model strings —
|
||||||
the EDID strings printed before them are decoration. On blankpad the live
|
the EDID strings printed before them are decoration. On blankpad the live
|
||||||
config splits via `include "./cfg/*.kdl"` from `~/.config/niri/config.kdl`,
|
config splits via `include "./cfg/*.kdl"` from `~/.config/niri/config.kdl`
|
||||||
and `cfg/display.kdl` is a **symlink into the dotfiles repo** — edit the
|
and `cfg/display.kdl` is a **symlink into the dotfiles repo** — edit the
|
||||||
repo path (that is what gets committed); check with `ls -ld`.
|
repo path (that is what gets committed); check with `ls -ld`.
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ live call at a time instead of guessing payload shapes:
|
|||||||
- `'Base Url' must not be empty` when keys were camelCase → **PascalCase is
|
- `'Base Url' must not be empty` when keys were camelCase → **PascalCase is
|
||||||
required** (`BaseUrl`, `ApiKey`) — schema keys, not the JSON wire keys.
|
required** (`BaseUrl`, `ApiKey`) — schema keys, not the JSON wire keys.
|
||||||
- `DefaultMetadataProfileId must be > 0` → fetch real ids from
|
- `DefaultMetadataProfileId must be > 0` → fetch real ids from
|
||||||
`GET /api/v1/metadataprofile` (rows like `(1,'Standard')`);
|
`GET /api/v1/metadataprofile` (rows like `(1,'Standard')`)
|
||||||
`GET /api/v1/qualitydefinition` returns the quality CATALOG — ids ≠
|
`GET /api/v1/qualitydefinition` returns the quality CATALOG — ids ≠
|
||||||
profiles.
|
profiles.
|
||||||
- `targetType null` → configContract not registered in that build (this
|
- `targetType null` → configContract not registered in that build (this
|
||||||
@@ -87,7 +87,7 @@ settings values live in `prowlarr.db` → `Applications.Settings` JSON (the
|
|||||||
pipeline hides a miss and you conclude "empty field" when the grep itself
|
pipeline hides a miss and you conclude "empty field" when the grep itself
|
||||||
failed.
|
failed.
|
||||||
4. Decision framing: when posture = gated-with-zero-users, present it as a
|
4. Decision framing: when posture = gated-with-zero-users, present it as a
|
||||||
decision menu (None behind the front-door wall vs External/authentikarr),
|
decision menu (None behind the front-door wall vs External/authentikarr)
|
||||||
not a silent change — first-account-wins means whoever logs in first owns
|
not a silent change — first-account-wins means whoever logs in first owns
|
||||||
the instance.
|
the instance.
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ http://prowlarr:9696/{indexerId}/download?link=...&file=... (torrent handoff)
|
|||||||
The global API key is in `config.xml` (`<ApiKey>`); per-application keys are
|
The global API key is in `config.xml` (`<ApiKey>`); per-application keys are
|
||||||
only used by Prowlarr's own sync calls, NOT by the feeds. The arr download
|
only used by Prowlarr's own sync calls, NOT by the feeds. The arr download
|
||||||
clients authenticate to qBittorrent with the permanent WebUI password stored
|
clients authenticate to qBittorrent with the permanent WebUI password stored
|
||||||
in `/home/dank/.qbpass` (mode 600, owner `dank`) on Dankserver — see
|
in `/home/dank/.qbpass` (mode 600, owner `dank`) on Dankserver —
|
||||||
[[qBittorrent]] for why that password must be pinned in the conf file.
|
[[qBittorrent]] for why that password must be pinned in the conf file.
|
||||||
|
|
||||||
**Readarr caveat:** Readarr is abandoned upstream; its qBittorrent client
|
**Readarr caveat:** Readarr is abandoned upstream; its qBittorrent client
|
||||||
@@ -97,13 +97,13 @@ successor; the indexer row itself is healthy.
|
|||||||
- Categories in the indexer row must be a subset of what the feed's `caps`
|
- Categories in the indexer row must be a subset of what the feed's `caps`
|
||||||
advertises; extras log "does not support the following categories" and
|
advertises; extras log "does not support the following categories" and
|
||||||
some feeds reject them.
|
some feeds reject them.
|
||||||
- **A freshly created indexer row defaults to all-off.** If `enableRss`,
|
- **A freshly created indexer row defaults to all-off.** If `enableRss`
|
||||||
`enableAutomaticSearch`, or `enableInteractiveSearch` aren't set, the
|
`enableAutomaticSearch`, or `enableInteractiveSearch` aren't set, the
|
||||||
app logs `Searching indexers for [X]. 0 active indexers` (Lidarr,
|
app logs `Searching indexers for [X]. 0 active indexers` (Lidarr,
|
||||||
verified). Search proves TV + music chains 2026-08-30: Sonarr returned
|
verified). Search proves TV + music chains 2026-08-30: Sonarr returned
|
||||||
76 live releases for an anime term, Lidarr 18 for YOASOBI – THE BOOK,
|
76 live releases for an anime term, Lidarr 18 for YOASOBI – THE BOOK,
|
||||||
all via the Prowlarr-Nyaa feed.
|
all via the Prowlarr-Nyaa feed.
|
||||||
- The v1 API collection is `/api/v1/applications` (not `/api/v1/app`);
|
- The v1 API collection is `/api/v1/applications` (not `/api/v1/app`)
|
||||||
GET responses carry `fields: []` — real settings values live in
|
GET responses carry `fields: []` — real settings values live in
|
||||||
`prowlarr.db` → `Applications.Settings` JSON (read-only inspection).
|
`prowlarr.db` → `Applications.Settings` JSON (read-only inspection).
|
||||||
- Instance name/`InstanceName=Prowlarr` in `config.xml` does NOT prefix the
|
- Instance name/`InstanceName=Prowlarr` in `config.xml` does NOT prefix the
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ qBittorrent is the torrent download client for the *arr stack. The stack runs on
|
|||||||
not on Dankserver. It receives download requests from Sonarr, Radarr, Lidarr,
|
not on Dankserver. It receives download requests from Sonarr, Radarr, Lidarr,
|
||||||
and Readarr via category tagging.
|
and Readarr via category tagging.
|
||||||
|
|
||||||
**Host:** FrankBook-Pro (podman machine) — **Access:** WebUI on port `8080`,
|
**Host:** FrankBook-Pro (podman machine) — **Access:** WebUI on port `8080`
|
||||||
published by the [[Gluetun]] unit because qBittorrent shares that container's
|
published by the [[Gluetun]] unit because qBittorrent shares that container's
|
||||||
network namespace (a netns-sharer publishes no ports of its own).
|
network namespace (a netns-sharer publishes no ports of its own).
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ Mac shell export `PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"` for
|
|||||||
`podman machine ssh`.
|
`podman machine ssh`.
|
||||||
|
|
||||||
**Access note (2026-09-21):** the Mac's login user is **dank**@192.168.0.112
|
**Access note (2026-09-21):** the Mac's login user is **dank**@192.168.0.112
|
||||||
(NOT core — `core` is the podman-machine VM user, reached only via
|
(NOT core — `core` is the podman-machine VM user, reached only
|
||||||
`podman machine ssh`). blankpad has key auth for `dank@` (bootstrapped
|
`podman machine ssh`). blankpad has key auth for `dank@` (bootstrapped
|
||||||
2026-09-21) and a `frankbook-pro` alias in `~/.ssh/config`. macOS sshd has a
|
2026-09-21) and a `frankbook-pro` alias in `~/.ssh/config`. macOS sshd has a
|
||||||
low MaxAuthTries: when password-bootstrapping use `-o
|
low MaxAuthTries: when password-bootstrapping use `-o
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ and `hermesadmin.` (app-enforced passwords, no public DNS for the first,
|
|||||||
(`dankclaw/Caddyfile:241-251`), and the FrankBook-Pro *arr/qBittorrent vhosts
|
(`dankclaw/Caddyfile:241-251`), and the FrankBook-Pro *arr/qBittorrent vhosts
|
||||||
(`dankclaw/Caddyfile:260-352`). There is also no vhost at all for ComfyUI, so
|
(`dankclaw/Caddyfile:260-352`). There is also no vhost at all for ComfyUI, so
|
||||||
it is exempt by absence — and therefore unauthenticated on the LAN at
|
it is exempt by absence — and therefore unauthenticated on the LAN at
|
||||||
`comfyui.franksdank.cloud:8188`. The guarded set is exactly `www.`, `search.`,
|
`comfyui.franksdank.cloud:8188`. The guarded set is exactly `www.`, `search.`
|
||||||
`grafana.`, `prometheus.`, `loki.`, `seerr.`, `marreta.`, `romm.`, `files.`
|
`grafana.`, `prometheus.`, `loki.`, `seerr.`, `marreta.`, `romm.`, `files.`
|
||||||
(`dankclaw/Caddyfile:283-423`).
|
(`dankclaw/Caddyfile:283-423`).
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ it is exempt by absence — and therefore unauthenticated on the LAN at
|
|||||||
instead of allowed, the browser's fetch throws and it renders its own
|
instead of allowed, the browser's fetch throws and it renders its own
|
||||||
error page (iOS/macOS browsers phrase it as "the request failed and the
|
error page (iOS/macOS browsers phrase it as "the request failed and the
|
||||||
interceptors did not return an alternative response") instead of the
|
interceptors did not return an alternative response") instead of the
|
||||||
login flow. Verify with a JS-capable client end-to-end — mint via
|
login flow. Verify with a JS-capable client end-to-end — mint
|
||||||
`make-challenge`, solve, submit with browser-exact query params — before
|
`make-challenge`, solve, submit with browser-exact query params — before
|
||||||
declaring a challenge chain healthy.
|
declaring a challenge chain healthy.
|
||||||
7. **Single-file bind mount + git pull = stale policy.** The policy is
|
7. **Single-file bind mount + git pull = stale policy.** The policy is
|
||||||
@@ -158,7 +158,7 @@ it is exempt by absence — and therefore unauthenticated on the LAN at
|
|||||||
answers any request lacking `X-Real-Ip` with a 500 whose log line reads
|
answers any request lacking `X-Real-Ip` with a 500 whose log line reads
|
||||||
`[misconfiguration] X-Real-Ip header is not set`. The real chain always
|
`[misconfiguration] X-Real-Ip header is not set`. The real chain always
|
||||||
sets it (every guarded Caddy listener carries `header_up X-Real-Ip
|
sets it (every guarded Caddy listener carries `header_up X-Real-Ip
|
||||||
{remote_host}` — the bats seam enforces the line), so users never see
|
{remote_host}` — the bats seam enforces the line), so users never
|
||||||
this. But any direct probe of `:8923` (scratch container, curl, a new
|
this. But any direct probe of `:8923` (scratch container, curl, a new
|
||||||
reverse-proxy hop) MUST set the header or it looks like a policy failure.
|
reverse-proxy hop) MUST set the header or it looks like a policy failure.
|
||||||
Scratch-verify with `-H "X-Real-Ip: 192.168.0.20"`.
|
Scratch-verify with `-H "X-Real-Ip: 192.168.0.20"`.
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ read-only `SELECT`s, 2026-10-18). Nine proxy providers, all attached to outpost
|
|||||||
`7e39655b-58d3-4fe6-a80a-513f31051217` ("authentik Embedded Outpost", 9
|
`7e39655b-58d3-4fe6-a80a-513f31051217` ("authentik Embedded Outpost", 9
|
||||||
members in `authentik_outposts_outpost_providers`), all with
|
members in `authentik_outposts_outpost_providers`), all with
|
||||||
`internal_host = http://caddy:8017`, all carrying the **same three flow
|
`internal_host = http://caddy:8017`, all carrying the **same three flow
|
||||||
UUIDs** (`default-authentication-flow`,
|
UUIDs** (`default-authentication-flow`
|
||||||
`default-provider-authorization-implicit-consent`,
|
`default-provider-authorization-implicit-consent`
|
||||||
`default-provider-invalidation-flow`), and each backed by an Application
|
`default-provider-invalidation-flow`), and each backed by an Application
|
||||||
(the `slug` column below):
|
(the `slug` column below):
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ Two provisioning rules, each verified live by an outage:
|
|||||||
reload** — including hosts whose providers were fine (verified 2026-09-02
|
reload** — including hosts whose providers were fine (verified 2026-09-02
|
||||||
when three providers provisioned without any flows took down every guarded
|
when three providers provisioned without any flows took down every guarded
|
||||||
vhost after a server restart). Backfill all three flows (copy from pk 1:
|
vhost after a server restart). Backfill all three flows (copy from pk 1:
|
||||||
`default-authentication-flow`,
|
`default-authentication-flow`
|
||||||
`default-provider-authorization-implicit-consent` (the implicit-consent
|
`default-provider-authorization-implicit-consent` (the implicit-consent
|
||||||
authorization flow, `default-invalidation-flow`) and the retry loop
|
authorization flow, `default-invalidation-flow`) and the retry loop
|
||||||
recovers on its next 3-second cycle. The silent killer: while the outpost
|
recovers on its next 3-second cycle. The silent killer: while the outpost
|
||||||
@@ -94,7 +94,7 @@ implementation diverges from upstream in ways that bit us; all verified live:
|
|||||||
contains `"authorization_code"` (enum value with underscore), the token
|
contains `"authorization_code"` (enum value with underscore), the token
|
||||||
endpoint rejects every exchange with `invalid_request`.
|
endpoint rejects every exchange with `invalid_request`.
|
||||||
- **Token endpoint is form-encoded + Basic-auth only.** A JSON body makes
|
- **Token endpoint is form-encoded + Basic-auth only.** A JSON body makes
|
||||||
`parse_token_request` see no `grant_type` → `unsupported_grant_type`;
|
`parse_token_request` see no `grant_type` → `unsupported_grant_type`
|
||||||
POST-body client secrets are not accepted — authenticate the confidential
|
POST-body client secrets are not accepted — authenticate the confidential
|
||||||
client with `Authorization: Basic base64(client_id:client_secret)`.
|
client with `Authorization: Basic base64(client_id:client_secret)`.
|
||||||
- **Provider resolution is by `client_id` query param** on the global
|
- **Provider resolution is by `client_id` query param** on the global
|
||||||
@@ -107,7 +107,7 @@ implementation diverges from upstream in ways that bit us; all verified live:
|
|||||||
becomes the *expression source* instead of the context; every call raises a
|
becomes the *expression source* instead of the context; every call raises a
|
||||||
swallowed `PropertyMappingExpressionException` and the claim is skipped.
|
swallowed `PropertyMappingExpressionException` and the claim is skipped.
|
||||||
Scope-grant filtering has the same class of bug: `ScopeMapping` has no
|
Scope-grant filtering has the same class of bug: `ScopeMapping` has no
|
||||||
provider FK (linkage lives in the `ProviderPropertyMapping` through model),
|
provider FK (linkage lives in the `ProviderPropertyMapping` through model)
|
||||||
so `filter(provider=...)` always yields an empty queryset and tokens asked
|
so `filter(provider=...)` always yields an empty queryset and tokens asked
|
||||||
to carry scopes get zero. Net effect: userinfo/introspection answers with
|
to carry scopes get zero. Net effect: userinfo/introspection answers with
|
||||||
`{sub}` only — email/groups/entitlements claims cannot be delivered by this
|
`{sub}` only — email/groups/entitlements claims cannot be delivered by this
|
||||||
@@ -156,7 +156,7 @@ Hard-won rules (the first is now contested by the live database):
|
|||||||
and retest.
|
and retest.
|
||||||
- `ak shell` import path for the provider model in 2026.8:
|
- `ak shell` import path for the provider model in 2026.8:
|
||||||
`from authentik.providers.ldap.models import LDAPProvider`.
|
`from authentik.providers.ldap.models import LDAPProvider`.
|
||||||
*(unverified 2026-10-18: importing it outside a configured Django shell fails
|
- (unverified 2026-10-18: importing it outside a configured Django shell fails
|
||||||
with `ImproperlyConfigured`, and no `ak shell` was driven against the live
|
with `ImproperlyConfigured`, and no `ak shell` was driven against the live
|
||||||
auth service for this check)*
|
auth service for this check)*
|
||||||
|
|
||||||
@@ -188,13 +188,13 @@ is a `password` field on the `default-user-settings` prompt
|
|||||||
|
|
||||||
Two ORM gotchas: `Flow.stages.add()` fails on this through model
|
Two ORM gotchas: `Flow.stages.add()` fails on this through model
|
||||||
(`Can't bulk create a multi-table inherited model` — `FlowStageBinding` is
|
(`Can't bulk create a multi-table inherited model` — `FlowStageBinding` is
|
||||||
MTI), so create the `FlowStageBinding` row directly with an explicit `order`;
|
MTI), so create the `FlowStageBinding` row directly with an explicit `order`
|
||||||
and a prompt field is a `Prompt` row linked through `PromptStageField`
|
and a prompt field is a `Prompt` row linked through `PromptStageField`
|
||||||
(`prompt_stage` + `prompt`; the order comes from `Prompt.order`).
|
(`prompt_stage` + `prompt`; the order comes from `Prompt.order`).
|
||||||
|
|
||||||
**A blank password must not set an empty password.** `set_password("")` would
|
**A blank password must not set an empty password.** `set_password("")` would
|
||||||
make the account log in with no password, so a validation policy
|
make the account log in with no password, so a validation policy
|
||||||
(`default-user-settings-no-empty-password`, bound to the prompt via
|
(`default-user-settings-no-empty-password`, bound to the prompt
|
||||||
`PromptStageValidationPolicy`) denies the form when `prompt_data["password"]`
|
`PromptStageValidationPolicy`) denies the form when `prompt_data["password"]`
|
||||||
is `""` and allows it when set. Verified with the `PolicyEvaluator`
|
is `""` and allows it when set. Verified with the `PolicyEvaluator`
|
||||||
(empty → deny, set → allow, absent → allow) and end-to-end by driving
|
(empty → deny, set → allow, absent → allow) and end-to-end by driving
|
||||||
@@ -213,7 +213,7 @@ three `*_flow_id` columns; the proxy table is
|
|||||||
`authentik_providers_proxy_proxyprovider` (doubled `proxy`) and its PK column
|
`authentik_providers_proxy_proxyprovider` (doubled `proxy`) and its PK column
|
||||||
really is **`oauth2provider_ptr_id`** (proxy providers inherit the OAuth2
|
really is **`oauth2provider_ptr_id`** (proxy providers inherit the OAuth2
|
||||||
table in 2026.8), the LDAP table is
|
table in 2026.8), the LDAP table is
|
||||||
`authentik_providers_ldap_ldapprovider` with PK `provider_ptr_id`;
|
`authentik_providers_ldap_ldapprovider` with PK `provider_ptr_id`
|
||||||
`authentik_core_application` attaches to a provider through `provider_id`
|
`authentik_core_application` attaches to a provider through `provider_id`
|
||||||
(its PK is `policybindingmodel_ptr_id`, not `id`), and `authentik_outposts_outpost`
|
(its PK is `policybindingmodel_ptr_id`, not `id`), and `authentik_outposts_outpost`
|
||||||
is keyed by `uuid`. So `Provider.objects.filter(type=…)` raising `FieldError`
|
is keyed by `uuid`. So `Provider.objects.filter(type=…)` raising `FieldError`
|
||||||
|
|||||||
@@ -15,20 +15,20 @@ application-level authentication with its proxy outpost. Not wrapped: Gitea,
|
|||||||
OpenWebUI, Jellyfin (LDAP outpost), the *arr stack, Pi-hole (basicauth edge) —
|
OpenWebUI, Jellyfin (LDAP outpost), the *arr stack, Pi-hole (basicauth edge) —
|
||||||
they handle their own session. Grafana is the exception that is both: it is a
|
they handle their own session. Grafana is the exception that is both: it is a
|
||||||
guarded vhost (`grafana-proxy`, pk 8, locked to Authentik SSO) *and* an OAuth2
|
guarded vhost (`grafana-proxy`, pk 8, locked to Authentik SSO) *and* an OAuth2
|
||||||
relying party (`grafana`, pk 9) (`dankclaw/Caddyfile:122-132`,
|
relying party (`grafana`, pk 9) (`dankclaw/Caddyfile:122-132`
|
||||||
`dankclaw/Caddyfile:374-377`; live provider table in [[Authentik Outpost Reference]]).
|
`dankclaw/Caddyfile:374-377`; live provider table in [[Authentik Outpost Reference]]).
|
||||||
|
|
||||||
- **Version:** `2026.8.3` (pinned in `dankclaw/authentik/quadlet/*.container`;
|
- **Version:** `2026.8.3` (pinned in `dankclaw/authentik/quadlet/*.container`
|
||||||
the outpost's boot log reads `"Booting authentik" … "version": "2026.8.3"`,
|
the outpost's boot log reads `"Booting authentik" … "version": "2026.8.3"`
|
||||||
2026-10-18)
|
2026-10-18)
|
||||||
- **Host:** dankclaw (`192.168.0.143`)
|
- **Host:** dankclaw (`192.168.0.143`)
|
||||||
- **Name:** `auth.franksdank.cloud`
|
- **Name:** `auth.franksdank.cloud`
|
||||||
- **Stack: five quadlet units, no compose.** `authentik-postgres`,
|
- **Stack: five quadlet units, no compose.** `authentik-postgres`
|
||||||
`authentik-server`, `authentik-worker` (`deploy_authentik`,
|
`authentik-server`, `authentik-worker` (`deploy_authentik`
|
||||||
`scripts/deploy-remote.sh:980-1004`) plus two outposts — `authentik-outpost`
|
`scripts/deploy-remote.sh:980-1004`) plus two outposts — `authentik-outpost`
|
||||||
(the Rust **proxy** outpost that actually serves `/outpost.goauthentik.io/*`,
|
(the Rust **proxy** outpost that actually serves `/outpost.goauthentik.io/*`
|
||||||
`deploy_authentik_outpost`, `scripts/deploy-remote.sh:1006-1018`) and
|
`deploy_authentik_outpost`, `scripts/deploy-remote.sh:1006-1018`) and
|
||||||
`authentik-outpost-ldap` (`deploy_authentik_outpost_ldap`,
|
`authentik-outpost-ldap` (`deploy_authentik_outpost_ldap`
|
||||||
`scripts/deploy-remote.sh:1020-1032`). All five report `generated` +
|
`scripts/deploy-remote.sh:1020-1032`). All five report `generated` +
|
||||||
`active`, `Restart=always`, `WantedBy=default.target`, with linger on
|
`active`, `Restart=always`, `WantedBy=default.target`, with linger on
|
||||||
(`systemctl --user`, 2026-10-18). The "3 compose services" wording is the
|
(`systemctl --user`, 2026-10-18). The "3 compose services" wording is the
|
||||||
@@ -36,7 +36,7 @@ relying party (`grafana`, pk 9) (`dankclaw/Caddyfile:122-132`,
|
|||||||
- **LDAP outpost:** serves LDAP for Jellyfin — see the section below.
|
- **LDAP outpost:** serves LDAP for Jellyfin — see the section below.
|
||||||
|
|
||||||
**Split off from this page:** [[Authentik Processes]] owns the per-container
|
**Split off from this page:** [[Authentik Processes]] owns the per-container
|
||||||
facts for `authentik-postgres`, `authentik-server` and `authentik-worker`;
|
facts for `authentik-postgres`, `authentik-server` and `authentik-worker`
|
||||||
[[Authentik Outpost Reference]] owns the provider/flow/token lookup material —
|
[[Authentik Outpost Reference]] owns the provider/flow/token lookup material —
|
||||||
the live proxy-provider inventory, the proxy-provider provisioning rules, the
|
the live proxy-provider inventory, the proxy-provider provisioning rules, the
|
||||||
OAuth2 relying-party quirks, the LDAP provider objects, the user-settings flow
|
OAuth2 relying-party quirks, the LDAP provider objects, the user-settings flow
|
||||||
@@ -136,7 +136,7 @@ routing table, and the Authentik proxy-provider list must be the same set of
|
|||||||
hosts. A guarded host with no provider 404s at login time; a provider with no
|
hosts. A guarded host with no provider 404s at login time; a provider with no
|
||||||
DNS record breaks off-LAN clients.
|
DNS record breaks off-LAN clients.
|
||||||
|
|
||||||
The live proxy-provider inventory (the per-vhost `external_host` → app mapping),
|
The live proxy-provider inventory (the per-vhost `external_host` → app mapping)
|
||||||
the two provisioning rules the outpost enforces, and this fork's OAuth2
|
the two provisioning rules the outpost enforces, and this fork's OAuth2
|
||||||
relying-party quirks are reference lookup: [[Authentik Outpost Reference]].
|
relying-party quirks are reference lookup: [[Authentik Outpost Reference]].
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ relying-party quirks are reference lookup: [[Authentik Outpost Reference]].
|
|||||||
|
|
||||||
The second outpost — `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`
|
The second outpost — `dankclaw/authentik/quadlet/authentik-outpost-ldap.container`
|
||||||
→ `authentik-outpost-ldap.service`, one of the five units in this stack —
|
→ `authentik-outpost-ldap.service`, one of the five units in this stack —
|
||||||
serves LDAP for Jellyfin's LDAP-Auth plugin (`enabled`=generated, `active`,
|
serves LDAP for Jellyfin's LDAP-Auth plugin (`enabled`=generated, `active`
|
||||||
`Restart=always`, container up, 2026-10-18). The image is pinned by its **amd64
|
`Restart=always`, container up, 2026-10-18). The image is pinned by its **amd64
|
||||||
single-platform digest**
|
single-platform digest**
|
||||||
`ghcr.io/goauthentik/ldap@sha256:2e7b259eb8c37faac916946ddf48ec681ee9d3a9f724fe8521bf161973f13cf9`
|
`ghcr.io/goauthentik/ldap@sha256:2e7b259eb8c37faac916946ddf48ec681ee9d3a9f724fe8521bf161973f13cf9`
|
||||||
@@ -159,7 +159,7 @@ rule at the current `2026.8.3` pin. Resolve once with `podman manifest inspect
|
|||||||
.digest'` and pin that digest.
|
.digest'` and pin that digest.
|
||||||
|
|
||||||
- Ports **389** (ldap) and **636** (ldaps) publish on the LAN
|
- Ports **389** (ldap) and **636** (ldaps) publish on the LAN
|
||||||
(`PublishPort=389:3389`, `636:6636`,
|
(`PublishPort=389:3389`, `636:6636`
|
||||||
`dankclaw/authentik/quadlet/authentik-outpost-ldap.container:30-31`; `ss -ltnp`
|
`dankclaw/authentik/quadlet/authentik-outpost-ldap.container:30-31`; `ss -ltnp`
|
||||||
shows `0.0.0.0:389` and `0.0.0.0:636`, 2026-10-18) and Dankserver can reach
|
shows `0.0.0.0:389` and `0.0.0.0:636`, 2026-10-18) and Dankserver can reach
|
||||||
them — a bare TCP connect from Dankserver to `192.168.0.143:389` succeeds
|
them — a bare TCP connect from Dankserver to `192.168.0.143:389` succeeds
|
||||||
@@ -196,7 +196,7 @@ every outpost session (see the ORM playbook).
|
|||||||
core quadlets, starts postgres health-gated (`wait_container_healthy
|
core quadlets, starts postgres health-gated (`wait_container_healthy
|
||||||
authentik-postgres 120`), then the server (300 s health wait) and the worker.
|
authentik-postgres 120`), then the server (300 s health wait) and the worker.
|
||||||
The outposts are separate handlers: `--service authentik-outpost` and
|
The outposts are separate handlers: `--service authentik-outpost` and
|
||||||
`--service authentik-outpost-ldap` (`scripts/deploy-remote.sh:1006-1032`,
|
`--service authentik-outpost-ldap` (`scripts/deploy-remote.sh:1006-1032`
|
||||||
dispatch at `:1579-1588`).
|
dispatch at `:1579-1588`).
|
||||||
2. Open `https://auth.franksdank.cloud` — the initial setup wizard runs. It
|
2. Open `https://auth.franksdank.cloud` — the initial setup wizard runs. It
|
||||||
creates the superuser (email + password) and generates the DB schema.
|
creates the superuser (email + password) and generates the DB schema.
|
||||||
@@ -210,7 +210,7 @@ every outpost session (see the ORM playbook).
|
|||||||
4. For apps with native OIDC: add an **OAuth2/OpenID Connect** provider, create
|
4. For apps with native OIDC: add an **OAuth2/OpenID Connect** provider, create
|
||||||
the app with the correct redirect URI, and paste the `client_id` /
|
the app with the correct redirect URI, and paste the `client_id` /
|
||||||
`client_secret` into the app's SSO settings. The relying parties that exist
|
`client_secret` into the app's SSO settings. The relying parties that exist
|
||||||
today are `grafana` (pk 9), `Seerr OIDC` (pk 12) and `Open WebUI` (pk 14);
|
today are `grafana` (pk 9), `Seerr OIDC` (pk 12) and `Open WebUI` (pk 14)
|
||||||
**Gitea has no provider row at all** (live provider table, 2026-10-18) — it
|
**Gitea has no provider row at all** (live provider table, 2026-10-18) — it
|
||||||
authenticates itself and stays off the chain by design
|
authenticates itself and stays off the chain by design
|
||||||
(`dankclaw/Caddyfile:110-120`).
|
(`dankclaw/Caddyfile:110-120`).
|
||||||
@@ -243,7 +243,7 @@ every outpost session (see the ORM playbook).
|
|||||||
The embedded outpost does not re-evaluate group membership per request. Each
|
The embedded outpost does not re-evaluate group membership per request. Each
|
||||||
completed authorize writes one row to `authentik_providers_proxy_proxysession`
|
completed authorize writes one row to `authentik_providers_proxy_proxysession`
|
||||||
whose `session_data` JSON holds the claims (`groups` included) that get served
|
whose `session_data` JSON holds the claims (`groups` included) that get served
|
||||||
for that browser session until the row expires (observed TTLs 1–24 h per
|
for that browser session until the row expires (observed TTLs 1–24 h
|
||||||
row). A session authorized **before** a user was added to a group keeps
|
row). A session authorized **before** a user was added to a group keeps
|
||||||
serving the old group list — Caddy-side RBAC header gates then 403 despite
|
serving the old group list — Caddy-side RBAC header gates then 403 despite
|
||||||
correct membership, and no authentik event is generated (the 403 happens at
|
correct membership, and no authentik event is generated (the 403 happens at
|
||||||
@@ -309,7 +309,7 @@ client → TLS edge → Anubis :8923 → outpost decision → internal
|
|||||||
000 (connection refused) — the post-auth listener lives in the caddy
|
000 (connection refused) — the post-auth listener lives in the caddy
|
||||||
container's netns; probe inside it or grep the config. Non-browser clients
|
container's netns; probe inside it or grep the config. Non-browser clients
|
||||||
cannot solve the JS challenge — before flipping, audit service-to-service
|
cannot solve the JS challenge — before flipping, audit service-to-service
|
||||||
traffic: ingestion/push paths must target internal addresses (`loki:3100`),
|
traffic: ingestion/push paths must target internal addresses (`loki:3100`)
|
||||||
never a public hostname of a guarded service. Exempt-by-construction
|
never a public hostname of a guarded service. Exempt-by-construction
|
||||||
categories stay unguarded: the IdP itself (`auth.`), git smart-HTTP
|
categories stay unguarded: the IdP itself (`auth.`), git smart-HTTP
|
||||||
endpoints, services with their own complete auth — document the exemption
|
endpoints, services with their own complete auth — document the exemption
|
||||||
@@ -336,14 +336,14 @@ EOF
|
|||||||
`sed -n '/objects imported/,$p'` to drop the noise *(unverified 2026-10-18:
|
`sed -n '/objects imported/,$p'` to drop the noise *(unverified 2026-10-18:
|
||||||
the exact banner text and the 229-object count — no `ak shell` was driven
|
the exact banner text and the 229-object count — no `ak shell` was driven
|
||||||
against the live auth service for this audit)*.
|
against the live auth service for this audit)*.
|
||||||
- Import paths (each wrong guess costs a failed exec): `User`, `Group`,
|
- Import paths (each wrong guess costs a failed exec): `User`, `Group`
|
||||||
`Application`, `ApplicationEntitlement`, `PropertyMapping` →
|
`Application`, `ApplicationEntitlement`, `PropertyMapping` →
|
||||||
`authentik.core.models`; `ProxyProvider` →
|
`authentik.core.models`; `ProxyProvider` →
|
||||||
`authentik.providers.proxy.models` (NOT `authentik.outposts.models`);
|
`authentik.providers.proxy.models` (NOT `authentik.outposts.models`)
|
||||||
`LDAPProvider` → `authentik.providers.ldap.models`;
|
`LDAPProvider` → `authentik.providers.ldap.models`
|
||||||
`authentik.core.models.application` does NOT exist as an import path; there
|
`authentik.core.models.application` does NOT exist as an import path; there
|
||||||
is no `authentik.policies.group` module in this build.
|
is no `authentik.policies.group` module in this build.
|
||||||
*(unverified 2026-10-18: these module paths were not re-executed — importing
|
- (unverified 2026-10-18: these module paths were not re-executed — importing
|
||||||
outside a configured Django shell fails with `ImproperlyConfigured`)*
|
outside a configured Django shell fails with `ImproperlyConfigured`)*
|
||||||
- The live database's table and column names (the 2026.x renames, the MTI parent
|
- The live database's table and column names (the 2026.x renames, the MTI parent
|
||||||
keys, the read-only `psql` audit recipe) are reference lookup:
|
keys, the read-only `psql` audit recipe) are reference lookup:
|
||||||
@@ -376,7 +376,7 @@ downstream —
|
|||||||
`category: user` panels from `services.json`.
|
`category: user` panels from `services.json`.
|
||||||
- `openwebui_users` / `openwebui_admins` → ApplicationEntitlements
|
- `openwebui_users` / `openwebui_admins` → ApplicationEntitlements
|
||||||
**"Open WebUI Users"** and **"Open WebUI Admins"**, both rows attached to the
|
**"Open WebUI Users"** and **"Open WebUI Admins"**, both rows attached to the
|
||||||
`Open WebUI` application (`authentik_core_applicationentitlement`,
|
`Open WebUI` application (`authentik_core_applicationentitlement`
|
||||||
2026-10-18), consumed by the "Open WebUI roles" property mapping → `roles`
|
2026-10-18), consumed by the "Open WebUI roles" property mapping → `roles`
|
||||||
claim *(unverified 2026-10-18: the group→entitlement binding and the mapping
|
claim *(unverified 2026-10-18: the group→entitlement binding and the mapping
|
||||||
expression were not read; the mapping itself is not visible under that name
|
expression were not read; the mapping itself is not visible under that name
|
||||||
@@ -392,7 +392,7 @@ downstream —
|
|||||||
artem 16, kirby 17, ry 18, nic 19, debo 20, freya 21, mitch 22, eric 23 —
|
artem 16, kirby 17, ry 18, nic 19, debo 20, freya 21, mitch 22, eric 23 —
|
||||||
and every one carries the `<user>@franksdank.cloud` address the template
|
and every one carries the `<user>@franksdank.cloud` address the template
|
||||||
prescribes. Creation dates (2026-10-07 → 2026-10-15) were not re-checked
|
prescribes. Creation dates (2026-10-07 → 2026-10-15) were not re-checked
|
||||||
*(unverified 2026-10-18)*. Template: `username` +
|
- (unverified 2026-10-18)*. Template: `username` +
|
||||||
`<user>@franksdank.cloud` + display name + `is_active=True` + the password
|
`<user>@franksdank.cloud` + display name + `is_active=True` + the password
|
||||||
set via `set_password`, membership in `Non-Admin Users` + `jellyfin_users` +
|
set via `set_password`, membership in `Non-Admin Users` + `jellyfin_users` +
|
||||||
`openwebui_users`. `alex` was recreated from scratch on 2026-10-08 into this
|
`openwebui_users`. `alex` was recreated from scratch on 2026-10-08 into this
|
||||||
@@ -400,7 +400,7 @@ downstream —
|
|||||||
the row carried no MFA device, token, entitlement, or proxy session, and
|
the row carried no MFA device, token, entitlement, or proxy session, and
|
||||||
events survive a user delete because `Event.user` is a denormalized JSON
|
events survive a user delete because `Event.user` is a denormalized JSON
|
||||||
field, not a FK).
|
field, not a FK).
|
||||||
- EXCLUDE from a user-level account: `homepage-admins`, `jellyfin_admins`,
|
- EXCLUDE from a user-level account: `homepage-admins`, `jellyfin_admins`
|
||||||
`openwebui_admins`, `authentik Admins`, `authentik Read-only`.
|
`openwebui_admins`, `authentik Admins`, `authentik Read-only`.
|
||||||
- Other user-category vhosts (SearXNG, Gitea, Calibre-Web, RomM, Copyparty,
|
- Other user-category vhosts (SearXNG, Gitea, Calibre-Web, RomM, Copyparty,
|
||||||
Plex, Seerr, Marreta) need no per-user group: any authenticated user
|
Plex, Seerr, Marreta) need no per-user group: any authenticated user
|
||||||
@@ -429,8 +429,8 @@ More field gotchas (each cost a failed exec): `ApplicationEntitlement.applicatio
|
|||||||
can be null (guard with `getattr`); `LDAPProvider` has
|
can be null (guard with `getattr`); `LDAPProvider` has
|
||||||
`search_mode`/`bind_mode`/`base_dn` and lacks `search_base`/`of_groups`/
|
`search_mode`/`bind_mode`/`base_dn` and lacks `search_base`/`of_groups`/
|
||||||
`organization` — the backing table
|
`organization` — the backing table
|
||||||
(`authentik_providers_ldap_ldapprovider`) has exactly `provider_ptr_id`,
|
(`authentik_providers_ldap_ldapprovider`) has exactly `provider_ptr_id`
|
||||||
`base_dn`, `certificate_id`, `tls_server_name`, `gid_start_number`,
|
`base_dn`, `certificate_id`, `tls_server_name`, `gid_start_number`
|
||||||
`uid_start_number`, `search_mode`, `bind_mode`, `mfa_support`
|
`uid_start_number`, `search_mode`, `bind_mode`, `mfa_support`
|
||||||
(`information_schema.columns`, 2026-10-18); `OAuth2Provider` has no `.scopes`
|
(`information_schema.columns`, 2026-10-18); `OAuth2Provider` has no `.scopes`
|
||||||
(scopes are linked through `ProviderPropertyMapping`, which is why
|
(scopes are linked through `ProviderPropertyMapping`, which is why
|
||||||
@@ -484,5 +484,5 @@ podman run --rm --network container:<container> --entrypoint /bin/busybox \
|
|||||||
- [Jellyfin](../Dankserver/Jellyfin.md) — consumes the LDAP outpost via its LDAP-Auth plugin
|
- [Jellyfin](../Dankserver/Jellyfin.md) — consumes the LDAP outpost via its LDAP-Auth plugin
|
||||||
- [Network Architecture](../Architecture/Network%20Architecture.md) — the flat naming scheme
|
- [Network Architecture](../Architecture/Network%20Architecture.md) — the flat naming scheme
|
||||||
- Research: `.docaudit/research/auth.md` — the full provider comparison
|
- Research: `.docaudit/research/auth.md` — the full provider comparison
|
||||||
*(the file is gone from the repo and from the host checkout as of
|
- (the file is gone from the repo and from the host checkout as of
|
||||||
2026-10-18)*
|
2026-10-18)*
|
||||||
@@ -47,7 +47,7 @@ torrent.
|
|||||||
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=25s
|
WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=25s
|
||||||
|
|
||||||
The private key is hashed into the VM env file and never enters the repo.
|
The private key is hashed into the VM env file and never enters the repo.
|
||||||
NAT-PMP inbound forwarding is available on Proton (gluetun supports it via
|
NAT-PMP inbound forwarding is available on Proton (gluetun supports it
|
||||||
`FIREWALL_INPUT_PORTS` + `NATPMP=true`) — deliberately deferred: the e2e
|
`FIREWALL_INPUT_PORTS` + `NATPMP=true`) — deliberately deferred: the e2e
|
||||||
gate passes outbound-only, and swarm peers needing an inbound port is a
|
gate passes outbound-only, and swarm peers needing an inbound port is a
|
||||||
seeding-era concern, not a grab-only one.
|
seeding-era concern, not a grab-only one.
|
||||||
@@ -86,7 +86,7 @@ are unaffected.
|
|||||||
got `Unauthenticated`); the live conf was patched and
|
got `Unauthenticated`); the live conf was patched and
|
||||||
`scripts/deploy-remote.sh` fixed to write the `_PBKDF2` key. Gotcha
|
`scripts/deploy-remote.sh` fixed to write the `_PBKDF2` key. Gotcha
|
||||||
(live, 2026-09-01): after qB 5.x ran, its conf rewrite left a hash line
|
(live, 2026-09-01): after qB 5.x ran, its conf rewrite left a hash line
|
||||||
4.6.7 rejects (`invalid credentials` for every login, including the arrs);
|
4.6.7 rejects (`invalid credentials` for every login, including the arrs)
|
||||||
rewriting the same `@ByteArray` line fresh (same password, 100k
|
rewriting the same `@ByteArray` line fresh (same password, 100k
|
||||||
iterations) restored login. Any future version flip: regenerate the line.
|
iterations) restored login. Any future version flip: regenerate the line.
|
||||||
- `HostHeaderValidation=false` + `ServerDomains=*`: the socat forwarder
|
- `HostHeaderValidation=false` + `ServerDomains=*`: the socat forwarder
|
||||||
@@ -98,7 +98,7 @@ are unaffected.
|
|||||||
path (no auto-TMM), so the default itself points at `Finished Torrents`:
|
path (no auto-TMM), so the default itself points at `Finished Torrents`:
|
||||||
every grab lands there regardless of client behavior; human review moves
|
every grab lands there regardless of client behavior; human review moves
|
||||||
files onward; nothing writes to the entropy volume.
|
files onward; nothing writes to the entropy volume.
|
||||||
- Categories seeded: `Finished Torrents`, `lidarr`, `radarr`, `readarr`,
|
- Categories seeded: `Finished Torrents`, `lidarr`, `radarr`, `readarr`
|
||||||
`tv-sonarr` (all → `Finished Torrents`).
|
`tv-sonarr` (all → `Finished Torrents`).
|
||||||
- **Download clients** (gateway `10.88.0.1:8080`, user `admin`): all five test
|
- **Download clients** (gateway `10.88.0.1:8080`, user `admin`): all five test
|
||||||
green against live qB — sonarr (`tv-sonarr`), radarr, lidarr, readarr
|
green against live qB — sonarr (`tv-sonarr`), radarr, lidarr, readarr
|
||||||
@@ -109,12 +109,12 @@ are unaffected.
|
|||||||
as "Authentication failure" — that kills runtime auth too, not just API
|
as "Authentication failure" — that kills runtime auth too, not just API
|
||||||
creates; the fix was pinning qB itself (below), after which readarr tests
|
creates; the fix was pinning qB itself (below), after which readarr tests
|
||||||
200 like the rest.
|
200 like the rest.
|
||||||
- **qB pinned to 4.6.7** (`Image=lscr.io/linuxserver/qbittorrent:4.6.7`,
|
- **qB pinned to 4.6.7** (`Image=lscr.io/linuxserver/qbittorrent:4.6.7`
|
||||||
not `latest`): `latest` resolved to 5.x, whose `204` login breaks
|
not `latest`): `latest` resolved to 5.x, whose `204` login breaks
|
||||||
Readarr's frozen client; 4.6.7 answers the legacy `200` + `"Ok."` that
|
Readarr's frozen client; 4.6.7 answers the legacy `200` + `"Ok."` that
|
||||||
every client accepts. Pinned in
|
every client accepts. Pinned in
|
||||||
`frankbook-pro/qbittorrent/quadlet/qbittorrent.container`.
|
`frankbook-pro/qbittorrent/quadlet/qbittorrent.container`.
|
||||||
- **Prowlarr Applications** push to sonarr/radarr/lidarr (`fullSync`,
|
- **Prowlarr Applications** push to sonarr/radarr/lidarr (`fullSync`
|
||||||
gateway ports). Readarr deliberately omitted: prowlarr's indexer-push
|
gateway ports). Readarr deliberately omitted: prowlarr's indexer-push
|
||||||
test to readarr's `/api/v1/indexer/test` 500s (EOL schema drift).
|
test to readarr's `/api/v1/indexer/test` 500s (EOL schema drift).
|
||||||
Legacy `Prowlarr-*` indexer rows carried in the migrated dankserver DBs
|
Legacy `Prowlarr-*` indexer rows carried in the migrated dankserver DBs
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ The Prometheus datasource is declared in git, not clicked into `grafana.db`:
|
|||||||
address Caddy proxies to, never the loopback-published port — and carries
|
address Caddy proxies to, never the loopback-published port — and carries
|
||||||
`timeInterval: 15s` so Grafana's rate windows cannot be shorter than the
|
`timeInterval: 15s` so Grafana's rate windows cannot be shorter than the
|
||||||
scrape interval (`prometheus.yml:5`). The `llm-fleet` dashboard is provisioned
|
scrape interval (`prometheus.yml:5`). The `llm-fleet` dashboard is provisioned
|
||||||
alongside it by `dashboards/provider.yml` (provider `fleet`,
|
alongside it by `dashboards/provider.yml` (provider `fleet`
|
||||||
`updateIntervalSeconds: 30`, `allowUiUpdates: true`).
|
`updateIntervalSeconds: 30`, `allowUiUpdates: true`).
|
||||||
|
|
||||||
A Loki datasource is still **not** provisioned — the tree contains exactly one
|
A Loki datasource is still **not** provisioned — the tree contains exactly one
|
||||||
@@ -70,7 +70,7 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`; measured 2026-
|
|||||||
Basic client auth, form-encoded token body, and the fork's userinfo-claims
|
Basic client auth, form-encoded token body, and the fork's userinfo-claims
|
||||||
bug that leaves identity at the `sub` hash only) is in
|
bug that leaves identity at the `sub` hash only) is in
|
||||||
[[Authentik Outpost Reference]] → "OAuth2 relying-party quirks". Consequences for Grafana:
|
[[Authentik Outpost Reference]] → "OAuth2 relying-party quirks". Consequences for Grafana:
|
||||||
The unit sets `ALLOW_SIGN_UP=false`, `AUTO_UPSERT=true`, `ALLOW_ASSIGN_ORG=true`,
|
The unit sets `ALLOW_SIGN_UP=false`, `AUTO_UPSERT=true`, `ALLOW_ASSIGN_ORG=true`
|
||||||
`ORG_ID=1` and **no** role-attribute mapping (`grafana.container:52-55`), so
|
`ORG_ID=1` and **no** role-attribute mapping (`grafana.container:52-55`), so
|
||||||
users land as Viewer with `login = <sub hex>`; no email/groups/role claims
|
users land as Viewer with `login = <sub hex>`; no email/groups/role claims
|
||||||
reach Grafana until the evaluator bug is fixed — do not write role mappings
|
reach Grafana until the evaluator bug is fixed — do not write role mappings
|
||||||
@@ -79,7 +79,7 @@ Grafana responds on `/api/health` with 200 and `"database":"ok"`; measured 2026-
|
|||||||
the host without the database or an admin password. *(unverified 2026-10-18)*)
|
the host without the database or an admin password. *(unverified 2026-10-18)*)
|
||||||
- **Never point this unit's `EnvironmentFile` at the repo-root `.env`.** That
|
- **Never point this unit's `EnvironmentFile` at the repo-root `.env`.** That
|
||||||
file is a gluetun env file and it carries `HOME=/root`; systemd imports it
|
file is a gluetun env file and it carries `HOME=/root`; systemd imports it
|
||||||
for the service, podman then looks for its configuration under `/root`,
|
for the service, podman then looks for its configuration under `/root`
|
||||||
cannot read it, and the unit dies at start with
|
cannot read it, and the unit dies at start with
|
||||||
`stat /root/.config: permission denied`. The unit reads
|
`stat /root/.config: permission denied`. The unit reads
|
||||||
`/home/dank/Containers/grafana/env`, which the deployer seeds with exactly
|
`/home/dank/Containers/grafana/env`, which the deployer seeds with exactly
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ See [[Container Host Reachability]] for the netns rules.
|
|||||||
published nothing → structurally unprobeable from the host).
|
published nothing → structurally unprobeable from the host).
|
||||||
3. Probe from the **correct vantage**: from a bridge-net container the host
|
3. Probe from the **correct vantage**: from a bridge-net container the host
|
||||||
is its gateway IP (`/proc/net/route`, e.g. `10.89.1.1` on
|
is its gateway IP (`/proc/net/route`, e.g. `10.89.1.1` on
|
||||||
`dankclaw_default`) — *not* `host.containers.internal` (`169.254.1.2`),
|
`dankclaw_default`) — *not* `host.containers.internal` (`169.254.1.2`)
|
||||||
which belongs to the default `podman` net (`10.88.0.0/16`). Probe with
|
which belongs to the default `podman` net (`10.88.0.0/16`). Probe with
|
||||||
bash `/dev/tcp` (no `python3`/`ip` in these images); escape loop vars so
|
bash `/dev/tcp` (no `python3`/`ip` in these images); escape loop vars so
|
||||||
they expand **inside** `podman exec bash -c`.
|
they expand **inside** `podman exec bash -c`.
|
||||||
@@ -83,7 +83,7 @@ python3 -m http.server 8899 --bind 127.0.0.1
|
|||||||
```
|
```
|
||||||
|
|
||||||
Then in the browser: load `http://127.0.0.1:8899/index.html`, **wait ~1.5 s**
|
Then in the browser: load `http://127.0.0.1:8899/index.html`, **wait ~1.5 s**
|
||||||
(panels render after the async fetch; checking immediately returns 0 panels),
|
(panels render after the async fetch; checking immediately returns 0 panels)
|
||||||
extract via `tab.evaluate` — LLM cards render `.desc-role`/`.desc-spec`/
|
extract via `tab.evaluate` — LLM cards render `.desc-role`/`.desc-spec`/
|
||||||
`.desc-inputs`/`.desc-use` from the card's `details` object, other cards a
|
`.desc-inputs`/`.desc-use` from the card's `details` object, other cards a
|
||||||
plain `.desc`; `getComputedStyle(el).color` confirms tones. Screenshot for
|
plain `.desc`; `getComputedStyle(el).color` confirms tones. Screenshot for
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ The homepage is live on Dankclaw. `./scripts/deploy.sh dankclaw` copies the stat
|
|||||||
**Docs page shipped (2026-09-17):** `/docs` serves a static user documentation
|
**Docs page shipped (2026-09-17):** `/docs` serves a static user documentation
|
||||||
page (`shared/homepage/docs.html`) — access walkthrough, the running-model
|
page (`shared/homepage/docs.html`) — access walkthrough, the running-model
|
||||||
guide, media how-tos, and troubleshooting. Per-service `/docs/<svc>/` pages
|
guide, media how-tos, and troubleshooting. Per-service `/docs/<svc>/` pages
|
||||||
remain a separate thread. The three LLM cards (`frank-dragoon`, `frank-medium`,
|
remain a separate thread. The three LLM cards (`frank-dragoon`, `frank-medium`
|
||||||
`frank-zealot`) moved from admin-only to the **user** page with live model
|
`frank-zealot`) moved from admin-only to the **user** page with live model
|
||||||
descriptions and a `docs` link to `/docs#models`.
|
descriptions and a `docs` link to `/docs#models`.
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ without which the picker showed "No results found" for every family account
|
|||||||
"equivalent to Claude Sonnet 5 / OpenAI-Luna (Summer 2026 frontier
|
"equivalent to Claude Sonnet 5 / OpenAI-Luna (Summer 2026 frontier
|
||||||
intelligence)", frank-archon "equivalent to Claude Opus 4.8 / OpenAI-Terra
|
intelligence)", frank-archon "equivalent to Claude Opus 4.8 / OpenAI-Terra
|
||||||
(same era)", frank-zealot "equivalent to last year's frontier level
|
(same era)", frank-zealot "equivalent to last year's frontier level
|
||||||
(2025)". Rendered by `descHTML()` as `.desc-equiv` (head color, italic);
|
(2025)". Rendered by `descHTML()` as `.desc-equiv` (head color, italic)
|
||||||
pinned in `homepage_config.bats`.
|
pinned in `homepage_config.bats`.
|
||||||
|
|
||||||
**Protoss rename + Model Information component (2026-10-08):** the model
|
**Protoss rename + Model Information component (2026-10-08):** the model
|
||||||
@@ -91,7 +91,7 @@ same renames, api-id mapping, supervision and compaction notes.
|
|||||||
app card now carries hex-clipped art beside its name (`services.json` `art`
|
app card now carries hex-clipped art beside its name (`services.json` `art`
|
||||||
field, files under `img/apps/`): 19 brand marks tinted amber from the
|
field, files under `img/apps/`): 19 brand marks tinted amber from the
|
||||||
Simple Icons CDN plus 19 hand-improvised monogram SVGs (amber hex + VT323
|
Simple Icons CDN plus 19 hand-improvised monogram SVGs (amber hex + VT323
|
||||||
initials) for apps with no mark — Copyparty, Marreta, Hermes (+dashboard),
|
initials) for apps with no mark — Copyparty, Marreta, Hermes (+dashboard)
|
||||||
node-exporter, Gluetun, OpenWebUI, Calibre-Web, RomM, Seerr, Loki, Alloy,
|
node-exporter, Gluetun, OpenWebUI, Calibre-Web, RomM, Seerr, Loki, Alloy,
|
||||||
Alertmanager, Valkey, Prowlarr, Readarr, Lidarr, Tautulli, ComfyUI. Model
|
Alertmanager, Valkey, Prowlarr, Readarr, Lidarr, Tautulli, ComfyUI. Model
|
||||||
cards render in the order zealot → dragoon → archon (registry order is the
|
cards render in the order zealot → dragoon → archon (registry order is the
|
||||||
@@ -143,8 +143,8 @@ per-app colors: Copyparty gold, Marreta brick, Grafana orange, Loki
|
|||||||
indigo, Neo4j network, Alloy violet, Promtail pink, Alertmanager red,
|
indigo, Neo4j network, Alloy violet, Promtail pink, Alertmanager red,
|
||||||
Prometheus orange, Node Exporter teal, cAdvisor sky, Guake yellow,
|
Prometheus orange, Node Exporter teal, cAdvisor sky, Guake yellow,
|
||||||
Authentik blue, Seer green radar, RomM a purple GameBoy. New per-service
|
Authentik blue, Seer green radar, RomM a purple GameBoy. New per-service
|
||||||
fields: `art_bg` (art backdrop — OpenWebUI's dark logo needs `#f5f5f5`),
|
fields: `art_bg` (art backdrop — OpenWebUI's dark logo needs `#f5f5f5`)
|
||||||
`warn` on OpenWebUI (searches ride the Seerr→SearXNG path),
|
`warn` on OpenWebUI (searches ride the Seerr→SearXNG path)
|
||||||
`model.compaction` (the amber "set this as compaction model" callout
|
`model.compaction` (the amber "set this as compaction model" callout
|
||||||
frank-zealot carries). frank-dragoon's card portrait is the SC2 unit
|
frank-zealot carries). frank-dragoon's card portrait is the SC2 unit
|
||||||
portrait (fandom wiki `Dragoon_portrait.jpg`, webp→jpeg). The whole
|
portrait (fandom wiki `Dragoon_portrait.jpg`, webp→jpeg). The whole
|
||||||
@@ -175,7 +175,7 @@ titles are chamfered plates with the diamond glyph inside the plate.
|
|||||||
Fonts: body is **Share Tech Mono** (legible retro terminal), headings
|
Fonts: body is **Share Tech Mono** (legible retro terminal), headings
|
||||||
are **Rajdhani 600** (the StarCraft UI face) — VT323 is retired; all
|
are **Rajdhani 600** (the StarCraft UI face) — VT323 is retired; all
|
||||||
three pages load the pair from Google Fonts. Four tiles that had
|
three pages load the pair from Google Fonts. Four tiles that had
|
||||||
reverted to initials got pictograms: Calibre-Web an open book (orange),
|
reverted to initials got pictograms: Calibre-Web an open book (orange)
|
||||||
RomM a purple GameBoy, Copyparty a blue folder with upload arrow,
|
RomM a purple GameBoy, Copyparty a blue folder with upload arrow,
|
||||||
Marreta a crumbling brick wall with a red crack (`img/apps/app-*.svg`).
|
Marreta a crumbling brick wall with a red crack (`img/apps/app-*.svg`).
|
||||||
frank-dragoon's card accent is phase-crystal teal (`#5fe0d0`), matching
|
frank-dragoon's card accent is phase-crystal teal (`#5fe0d0`), matching
|
||||||
@@ -197,7 +197,7 @@ state — the only animation left is the speaking-voice pulse. The result
|
|||||||
is flat navy panels on crisp 1px borders: same Protoss direction, half
|
is flat navy panels on crisp 1px borders: same Protoss direction, half
|
||||||
the pixels.
|
the pixels.
|
||||||
|
|
||||||
**Model ids official (2026-10-08):** the card ids `frank-zealot`,
|
**Model ids official (2026-10-08):** the card ids `frank-zealot`
|
||||||
`frank-dragoon`, `frank-archon` are now the harness's official model ids
|
`frank-dragoon`, `frank-archon` are now the harness's official model ids
|
||||||
in `models.yml`/`config.yml` (see [[OMP Harness]]); the
|
in `models.yml`/`config.yml` (see [[OMP Harness]]); the
|
||||||
systemd services keep the `frank-archon`/`frank-dragoon`/
|
systemd services keep the `frank-archon`/`frank-dragoon`/
|
||||||
@@ -205,7 +205,7 @@ systemd services keep the `frank-archon`/`frank-dragoon`/
|
|||||||
one name per model.
|
one name per model.
|
||||||
|
|
||||||
**Api ids renamed on the wire (2026-10-09):** the promise in
|
**Api ids renamed on the wire (2026-10-09):** the promise in
|
||||||
`docs.html` is kept — the served model ids are now `frank-zealot`,
|
`docs.html` is kept — the served model ids are now `frank-zealot`
|
||||||
`frank-dragoon`, `frank-archon`, so the docs page no longer carries
|
`frank-dragoon`, `frank-archon`, so the docs page no longer carries
|
||||||
`api id:` annotations, and the curl/opencode/Pi examples send
|
`api id:` annotations, and the curl/opencode/Pi examples send
|
||||||
`frank-zealot`. The systemd services keep the old names. `docs.html`
|
`frank-zealot`. The systemd services keep the old names. `docs.html`
|
||||||
@@ -216,7 +216,7 @@ ids, which did not change.
|
|||||||
clip-paths replaced with `border-radius` (the clipped corners read as
|
clip-paths replaced with `border-radius` (the clipped corners read as
|
||||||
gaps in the 1px border); section-title diamonds moved from absolute
|
gaps in the 1px border); section-title diamonds moved from absolute
|
||||||
positioning to a fixed-width inline glyph (aligned, wrap-safe); closed
|
positioning to a fixed-width inline glyph (aligned, wrap-safe); closed
|
||||||
WIP items grey out (dim type, desaturated art, muted border via
|
WIP items grey out (dim type, desaturated art, muted border
|
||||||
`:has(.badge.wip-closed)`); the Dragoon tagline is one spliced track
|
`:has(.badge.wip-closed)`); the Dragoon tagline is one spliced track
|
||||||
(`sfx/dragoon-tagline.ogg` — loudnorm-matched, silence-trimmed, 350 ms
|
(`sfx/dragoon-tagline.ogg` — loudnorm-matched, silence-trimmed, 350 ms
|
||||||
beat; the two SC2 source oggs are trashed); privacy notes became
|
beat; the two SC2 source oggs are trashed); privacy notes became
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ sudo systemctl restart systemd-resolved && resolvectl flush-caches
|
|||||||
```
|
```
|
||||||
|
|
||||||
The `~` (route-only) prefix is load-bearing: it routes only those domains to
|
The `~` (route-only) prefix is load-bearing: it routes only those domains to
|
||||||
Pi-hole. Verify: `getent hosts www.franksdank.cloud` → `192.168.0.143`;
|
Pi-hole. Verify: `getent hosts www.franksdank.cloud` → `192.168.0.143`
|
||||||
`resolvectl status` shows `DNS Domain: ~franksdank.cloud ~franksdank.casa`.
|
`resolvectl status` shows `DNS Domain: ~franksdank.cloud ~franksdank.casa`.
|
||||||
|
|
||||||
**Layer 2 — browser DoH (the bypass: "curl works, browser times out").** If
|
**Layer 2 — browser DoH (the bypass: "curl works, browser times out").** If
|
||||||
@@ -236,7 +236,7 @@ Endpoints (all POST, JSON body):
|
|||||||
`.content.records[]` as some docs claim — parse with
|
`.content.records[]` as some docs claim — parse with
|
||||||
`jq -r '.records[] | [.id,.name,.type,.content,(.ttl|tostring)] | @tsv'`.
|
`jq -r '.records[] | [.id,.name,.type,.content,(.ttl|tostring)] | @tsv'`.
|
||||||
- Delete: `/dns/delete/{domain}/{id}` (body carries apikey+secretapikey).
|
- Delete: `/dns/delete/{domain}/{id}` (body carries apikey+secretapikey).
|
||||||
- Edit: `/dns/edit/{domain}/{id}` — pass the full record (`id`, `name`,
|
- Edit: `/dns/edit/{domain}/{id}` — pass the full record (`id`, `name`
|
||||||
`type`, `content`, `ttl`, `prio`).
|
`type`, `content`, `ttl`, `prio`).
|
||||||
|
|
||||||
Quirks that bite (verified live 2026-08-31):
|
Quirks that bite (verified live 2026-08-31):
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ The [[Alloy]] agent on Dankclaw is wired to ship container and journal logs to `
|
|||||||
The service answers and the API works; there is simply no data in it. Measured on 2026-10-18:
|
The service answers and the API works; there is simply no data in it. Measured on 2026-10-18:
|
||||||
|
|
||||||
- `curl -s http://127.0.0.1:3100/loki/api/v1/labels` → `{"status":"success"}` with no `data` key (no label names exist), and a 7-day `query_range` on `{job="systemd-journal"}` returns `"result":[]`.
|
- `curl -s http://127.0.0.1:3100/loki/api/v1/labels` → `{"status":"success"}` with no `data` key (no label names exist), and a 7-day `query_range` on `{job="systemd-journal"}` returns `"result":[]`.
|
||||||
- `du -sh /opt/loki/data` → a few hundred KB of directory scaffolding (`wal`,
|
- `du -sh /opt/loki/data` → a few hundred KB of directory scaffolding (`wal`
|
||||||
`chunks`, `compactor`, `tsdb-shipper-active`, `tsdb-shipper-cache`) —
|
`chunks`, `compactor`, `tsdb-shipper-active`, `tsdb-shipper-cache`) —
|
||||||
`find /opt/loki/data -type f` counted **8 files**, all WAL segments,
|
`find /opt/loki/data -type f` counted **8 files**, all WAL segments,
|
||||||
cluster-seed and delete-request bookkeeping, **no chunk objects**
|
cluster-seed and delete-request bookkeeping, **no chunk objects**
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ committed.
|
|||||||
| `shared/prometheus/rules/llm-fleet.yml` | alert rules + recording rules |
|
| `shared/prometheus/rules/llm-fleet.yml` | alert rules + recording rules |
|
||||||
| `shared/grafana/provisioning/` | datasources + dashboards (provisioned tree) |
|
| `shared/grafana/provisioning/` | datasources + dashboards (provisioned tree) |
|
||||||
|
|
||||||
Targets and datasources reference **container names** (`grafana:3000`,
|
Targets and datasources reference **container names** (`grafana:3000`
|
||||||
`loki:3100` — `shared/prometheus/prometheus.yml:52-61`), never
|
`loki:3100` — `shared/prometheus/prometheus.yml:52-61`), never
|
||||||
loopback-published ports; `alloy` is **not scraped at all** (no job in the
|
loopback-published ports; `alloy` is **not scraped at all** (no job in the
|
||||||
config — its `127.0.0.1:12345` UI is only probed by the homepage timer,
|
config — its `127.0.0.1:12345` UI is only probed by the homepage timer,
|
||||||
@@ -47,7 +47,7 @@ from `deploy_observability`:
|
|||||||
- `copy_grafana_provisioning` → `/opt/grafana/provisioning/` (`:286-300`)
|
- `copy_grafana_provisioning` → `/opt/grafana/provisioning/` (`:286-300`)
|
||||||
|
|
||||||
`scripts/deploy-plan.py` maps `shared/prometheus/**` to `prometheus` (plus
|
`scripts/deploy-plan.py` maps `shared/prometheus/**` to `prometheus` (plus
|
||||||
`alertmanager` when the changed file is `alertmanager.yml`, `:251-256`),
|
`alertmanager` when the changed file is `alertmanager.yml`, `:251-256`)
|
||||||
`shared/loki/**` to `loki` and `shared/alloy/**` to `alloy` (`:257-260`), and
|
`shared/loki/**` to `loki` and `shared/alloy/**` to `alloy` (`:257-260`), and
|
||||||
`shared/grafana/**` to `grafana` (`:261-264`); changes under
|
`shared/grafana/**` to `grafana` (`:261-264`); changes under
|
||||||
`dankclaw/observability/` select the touched unit and its dependency consumer
|
`dankclaw/observability/` select the touched unit and its dependency consumer
|
||||||
@@ -71,9 +71,9 @@ HTTP probe, not a metrics endpoint.
|
|||||||
|
|
||||||
**Add an alert/recording rule** — edit `shared/prometheus/rules/llm-fleet.yml`.
|
**Add an alert/recording rule** — edit `shared/prometheus/rules/llm-fleet.yml`.
|
||||||
Current delivered set: **4 alert rules + 2 recording rules** —
|
Current delivered set: **4 alert rules + 2 recording rules** —
|
||||||
`LlmRequestsQueueing`, `LlmPrefixCacheCollapse`, `LlmSpecDecodeOff`,
|
`LlmRequestsQueueing`, `LlmPrefixCacheCollapse`, `LlmSpecDecodeOff`
|
||||||
`LlmEndpointDown`, plus `job:llm_prefix_cache_reuse:rate5m` and
|
`LlmEndpointDown`, plus `job:llm_prefix_cache_reuse:rate5m` and
|
||||||
`job:llm_spec_accept_length:rate5m` (`shared/prometheus/rules/llm-fleet.yml:32-110`),
|
`job:llm_spec_accept_length:rate5m` (`shared/prometheus/rules/llm-fleet.yml:32-110`)
|
||||||
confirmed live: `curl -s http://127.0.0.1:9090/api/v1/rules` (2026-10-18)
|
confirmed live: `curl -s http://127.0.0.1:9090/api/v1/rules` (2026-10-18)
|
||||||
reports 4 alerting + 2 recording. Rules reload with the prometheus service.
|
reports 4 alerting + 2 recording. Rules reload with the prometheus service.
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ curl -s -u admin:<pw> http://127.0.0.1:3002/api/datasources | python3 -m json.to
|
|||||||
|
|
||||||
The two Prometheus queries were run on 2026-10-18 and are what every number on
|
The two Prometheus queries were run on 2026-10-18 and are what every number on
|
||||||
this page comes from: 4 alerting + 2 recording rules, and `up` for
|
this page comes from: 4 alerting + 2 recording rules, and `up` for
|
||||||
`prometheus`, `node-dankclaw`, `node-dankserver`, `grafana`, `loki`,
|
`prometheus`, `node-dankclaw`, `node-dankserver`, `grafana`, `loki`
|
||||||
`frank-zealot`, `frank-dragoon`, `frank-archon` with the four structural downs
|
`frank-zealot`, `frank-dragoon`, `frank-archon` with the four structural downs
|
||||||
listed in [[Prometheus]]. The Grafana query needs the local admin password and
|
listed in [[Prometheus]]. The Grafana query needs the local admin password and
|
||||||
was not run here; the filesystem side (`ls /opt/grafana/provisioning/*`) is
|
was not run here; the filesystem side (`ls /opt/grafana/provisioning/*`) is
|
||||||
@@ -114,12 +114,12 @@ table (refused = outage, 404/HTML = config defect).
|
|||||||
|
|
||||||
## Current delivered state (delivery chain live 2026-09-17, re-verified 2026-10-18)
|
## Current delivered state (delivery chain live 2026-09-17, re-verified 2026-10-18)
|
||||||
|
|
||||||
Prometheus → Alertmanager (`dankclaw/observability/quadlet/alertmanager.container`,
|
Prometheus → Alertmanager (`dankclaw/observability/quadlet/alertmanager.container`
|
||||||
loopback `:9093`) → host-side Signal relay (`scripts/signal-relay.py`, user
|
loopback `:9093`) → host-side Signal relay (`scripts/signal-relay.py`, user
|
||||||
unit, `192.168.0.143:18093` — the LAN IP, the one bind a container can reach
|
unit, `192.168.0.143:18093` — the LAN IP, the one bind a container can reach
|
||||||
via `host.containers.internal`) → signal-cli, addressed by number
|
via `host.containers.internal`) → signal-cli, addressed by number
|
||||||
`+12062476336` (`scripts/signal-relay.py:36-43`). Every hop re-checked
|
`+12062476336` (`scripts/signal-relay.py:36-43`). Every hop re-checked
|
||||||
2026-10-18: `ss -ltnp` shows `python3` on `192.168.0.143:18093`,
|
2026-10-18: `ss -ltnp` shows `python3` on `192.168.0.143:18093`
|
||||||
`curl -s http://192.168.0.143:18093/health` → `ok`, and
|
`curl -s http://192.168.0.143:18093/health` → `ok`, and
|
||||||
`journalctl --user -u signal-relay` ends with successful `sent to +12062476336:`
|
`journalctl --user -u signal-relay` ends with successful `sent to +12062476336:`
|
||||||
lines for `LlmEndpointDown`, `LlmRequestsQueueing`, `LlmSpecDecodeOff` and
|
lines for `LlmEndpointDown`, `LlmRequestsQueueing`, `LlmSpecDecodeOff` and
|
||||||
@@ -148,7 +148,7 @@ state lives in Prometheus' process, not on disk — a restart re-stamps
|
|||||||
`activeAt` on a long-standing alert, so a "fresh" alert after a reboot is not
|
`activeAt` on a long-standing alert, so a "fresh" alert after a reboot is not
|
||||||
evidence of a fresh fault. (Alertmanager by contrast **does** persist:
|
evidence of a fresh fault. (Alertmanager by contrast **does** persist:
|
||||||
`--storage.path=/alertmanager` on a host volume keeps silences and the
|
`--storage.path=/alertmanager` on a host volume keeps silences and the
|
||||||
notification log — `dankclaw/observability/quadlet/alertmanager.container:39-42`,
|
notification log — `dankclaw/observability/quadlet/alertmanager.container:39-42`
|
||||||
`/opt/alertmanager/data`.)
|
`/opt/alertmanager/data`.)
|
||||||
|
|
||||||
Remaining known downs are the structural ones above (HTML-scraped probes, Gitea
|
Remaining known downs are the structural ones above (HTML-scraped probes, Gitea
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ Dankserver runs one member of this family, node-exporter, as its own unit
|
|||||||
## Exposure
|
## Exposure
|
||||||
|
|
||||||
The five HTTP surfaces publish on `127.0.0.1` only — `ss -ltnp` (2026-10-18)
|
The five HTTP surfaces publish on `127.0.0.1` only — `ss -ltnp` (2026-10-18)
|
||||||
shows `127.0.0.1:9090`, `127.0.0.1:9093`, `127.0.0.1:3002`, `127.0.0.1:3100`,
|
shows `127.0.0.1:9090`, `127.0.0.1:9093`, `127.0.0.1:3002`, `127.0.0.1:3100`
|
||||||
`127.0.0.1:12345` and no wildcard — because each of them is unauthenticated
|
`127.0.0.1:12345` and no wildcard — because each of them is unauthenticated
|
||||||
inside, and several carry lifecycle or query APIs. Browser access goes through
|
inside, and several carry lifecycle or query APIs. Browser access goes through
|
||||||
Caddy: each vhost (`dankclaw/Caddyfile:128-145`) proxies to the guard
|
Caddy: each vhost (`dankclaw/Caddyfile:128-145`) proxies to the guard
|
||||||
`anubis:8923`, and the post-auth handlers route by container name on
|
`anubis:8923`, and the post-auth handlers route by container name on
|
||||||
`dankclaw_default` — `loki.franksdank.cloud` → `loki:3100` (`:364-366`),
|
`dankclaw_default` — `loki.franksdank.cloud` → `loki:3100` (`:364-366`)
|
||||||
`grafana.franksdank.cloud` → `grafana:3000` (`:374-376`),
|
`grafana.franksdank.cloud` → `grafana:3000` (`:374-376`)
|
||||||
`prometheus.franksdank.cloud` → `prometheus:9090` (`:379-381`).
|
`prometheus.franksdank.cloud` → `prometheus:9090` (`:379-381`).
|
||||||
Alertmanager has no vhost at all. node-exporter is the exception that must live
|
Alertmanager has no vhost at all. node-exporter is the exception that must live
|
||||||
in the host network namespace, and it binds exactly two addresses rather than
|
in the host network namespace, and it binds exactly two addresses rather than
|
||||||
@@ -70,13 +70,13 @@ and the podman bridge subnet `10.89.0.0/16` before the DROP
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
`./scripts/deploy.sh dankclaw --service <name>` runs `deploy_observability`,
|
`./scripts/deploy.sh dankclaw --service <name>` runs `deploy_observability`
|
||||||
which installs the unit, copies that service's config
|
which installs the unit, copies that service's config
|
||||||
(`copy_prometheus_config`, `copy_alertmanager_config`, `copy_loki_config`,
|
(`copy_prometheus_config`, `copy_alertmanager_config`, `copy_loki_config`
|
||||||
`copy_alloy_config`, `copy_grafana_provisioning`, plus
|
`copy_alloy_config`, `copy_grafana_provisioning`, plus
|
||||||
`install_signal_relay` with the alertmanager deploy), then waits on the loopback
|
`install_signal_relay` with the alertmanager deploy), then waits on the loopback
|
||||||
health endpoint (`/prometheus`: `/-/healthy`, `/alertmanager`: `/-/healthy`,
|
health endpoint (`/prometheus`: `/-/healthy`, `/alertmanager`: `/-/healthy`
|
||||||
`/grafana`: `/api/health`, `/loki`: `/ready`, `/alloy`: `/-/healthy`,
|
`/grafana`: `/api/health`, `/loki`: `/ready`, `/alloy`: `/-/healthy`
|
||||||
`/node-exporter`: `/metrics`). Startup order follows the data dependencies —
|
`/node-exporter`: `/metrics`). Startup order follows the data dependencies —
|
||||||
`grafana` after `prometheus`, `alloy` after `loki`
|
`grafana` after `prometheus`, `alloy` after `loki`
|
||||||
(`grafana.container:32-33`, `alloy.container:38-39`). See
|
(`grafana.container:32-33`, `alloy.container:38-39`). See
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ The DFlash drafter proposes, then ngram-mod runs as an additional implementation
|
|||||||
`--spec-type` takes a comma list, and the gate is `ctx_dft` in
|
`--spec-type` takes a comma list, and the gate is `ctx_dft` in
|
||||||
`common/speculative.cpp` (`common_speculative_init_result`): **draftless**
|
`common/speculative.cpp` (`common_speculative_init_result`): **draftless**
|
||||||
types (the ngram family — simple, map-k, map-k4v, mod, cache) are added
|
types (the ngram family — simple, map-k, map-k4v, mod, cache) are added
|
||||||
unconditionally, while **context-backed** types (`draft-mtp`, `draft-dflash`,
|
unconditionally, while **context-backed** types (`draft-mtp`, `draft-dflash`
|
||||||
`draft-dspark`, `draft-eagle3`) all require a draft CONTEXT. Consequences:
|
`draft-dspark`, `draft-eagle3`) all require a draft CONTEXT. Consequences:
|
||||||
|
|
||||||
- A chain may hold ONE context-backed drafter plus any number of ngram types.
|
- A chain may hold ONE context-backed drafter plus any number of ngram types.
|
||||||
@@ -201,7 +201,7 @@ Speculative decoding is lossless under rejection sampling: the target verifies e
|
|||||||
On the mid-August 2026 Dankclaw shape `draft-dflash` was the benchmarked winner
|
On the mid-August 2026 Dankclaw shape `draft-dflash` was the benchmarked winner
|
||||||
at 4k — 20.2 t/s, against 16.9 with ngram layered on and 8.7 ngram-only; those
|
at 4k — 20.2 t/s, against 16.9 with ngram layered on and 8.7 ngram-only; those
|
||||||
three figures are that retired llama.cpp era's property ([[frank-dragoon]] owns
|
three figures are that retired llama.cpp era's property ([[frank-dragoon]] owns
|
||||||
the era and its shape). MTP is a viable alternative to test, not a bug fix: the parallel-stream releases investigated on 2026-08-13 were client-side aborts and a Dankserver crash, not speculative decoding behavior (see [[deployment]]).
|
the era and its shape). MTP is a viable alternative to test, not a bug fix: the parallel-stream releases investigated on 2026-08-13 were client-side aborts and a Dankserver crash, not speculative decoding behavior (see ).
|
||||||
|
|
||||||
## Draft Models
|
## Draft Models
|
||||||
|
|
||||||
@@ -257,7 +257,7 @@ DSpark took `ROCm error: an illegal memory access` inside the shared draft
|
|||||||
context with two concurrent 16k prompts; DFlash corrupted multi-slot into
|
context with two concurrent 16k prompts; DFlash corrupted multi-slot into
|
||||||
premature EOS and was retired first. The timeline, the crash class, the
|
premature EOS and was retired first. The timeline, the crash class, the
|
||||||
"concurrency is the trigger, not size" finding and the `PARALLEL=1` workaround
|
"concurrency is the trigger, not size" finding and the `PARALLEL=1` workaround
|
||||||
are on [[qwen3.8-spec-matrix]]; the shape they were measured on is
|
are on ; the shape they were measured on is
|
||||||
[[frank-dragoon]]'s mid-August 2026 era.
|
[[frank-dragoon]]'s mid-August 2026 era.
|
||||||
|
|
||||||
## A/B protocol on a live llama.cpp endpoint
|
## A/B protocol on a live llama.cpp endpoint
|
||||||
@@ -303,7 +303,7 @@ Protocol:
|
|||||||
|
|
||||||
Read the right counters — per request `timings.draft_n` / `draft_n_accepted` /
|
Read the right counters — per request `timings.draft_n` / `draft_n_accepted` /
|
||||||
`predicted_per_second`; cumulative `/metrics`
|
`predicted_per_second`; cumulative `/metrics`
|
||||||
`llamacpp:spec_decode_num_drafts_total` / `..._accepted_tokens_total`;
|
`llamacpp:spec_decode_num_drafts_total` / `..._accepted_tokens_total`
|
||||||
per-task `draft acceptance = ... (N accepted / M generated)`. `draft_n == 0`
|
per-task `draft acceptance = ... (N accepted / M generated)`. `draft_n == 0`
|
||||||
while tokens flow = the proposer is NOT engaged, whatever the flags say. A
|
while tokens flow = the proposer is NOT engaged, whatever the flags say. A
|
||||||
zero-draft config is a useful negative control: it proves the other config's
|
zero-draft config is a useful negative control: it proves the other config's
|
||||||
@@ -367,7 +367,7 @@ states, so a dense-27B DFlash2 is useless for a 512-expert MoE trunk.
|
|||||||
|
|
||||||
**Step 4 — acceptance reality checks.** Treat 40–60 % as the honest band for a
|
**Step 4 — acceptance reality checks.** Treat 40–60 % as the honest band for a
|
||||||
matched pair; the arm that anchors the bottom of it on this fleet is the
|
matched pair; the arm that anchors the bottom of it on this fleet is the
|
||||||
depth-7 `draft-dflash` row of [[frank-medium-llamacpp-dflash2]]'s spec table
|
depth-7 `draft-dflash` row of 's spec table
|
||||||
(accepted share, rate and VRAM for every depth are in that table). Cheapest
|
(accepted share, rate and VRAM for every depth are in that table). Cheapest
|
||||||
proxy for `p` at temp 0 when both models are greedy and share a tokenizer:
|
proxy for `p` at temp 0 when both models are greedy and share a tokenizer:
|
||||||
greedy continuation agreement on identical prompts, two API calls, no weights
|
greedy continuation agreement on identical prompts, two API calls, no weights
|
||||||
@@ -381,10 +381,10 @@ drafting.
|
|||||||
**Step 5 — check the attachment surface BEFORE the arithmetic.** Closed
|
**Step 5 — check the attachment surface BEFORE the arithmetic.** Closed
|
||||||
engines refuse external drafters by design: halogen-flash-server's
|
engines refuse external drafters by design: halogen-flash-server's
|
||||||
`DRAFTER_CAP` gates `dflash2` on the loaded checkpoint and an unavailable
|
`DRAFTER_CAP` gates `dflash2` on the loaded checkpoint and an unavailable
|
||||||
name is a deliberate 400 (a silent downgrade would make comparisons lie);
|
name is a deliberate 400 (a silent downgrade would make comparisons lie)
|
||||||
there is no device routing in its loopback token protocol, so a drafter can
|
there is no device routing in its loopback token protocol, so a drafter can
|
||||||
never sit on another card. Pure llama.cpp DOES route the draft:
|
never sit on another card. Pure llama.cpp DOES route the draft:
|
||||||
`--spec-draft-device/-devd` (default follows `--device`), `-ngld`,
|
`--spec-draft-device/-devd` (default follows `--device`), `-ngld`
|
||||||
`-otd`, `-ctkd/-ctvd`, `--spec-draft-n-max` (default 3), `--spec-draft-p-min`.
|
`-otd`, `-ctkd/-ctvd`, `--spec-draft-n-max` (default 3), `--spec-draft-p-min`.
|
||||||
One process sees only the devices its backends were built with — CUDA+Vulkan
|
One process sees only the devices its backends were built with — CUDA+Vulkan
|
||||||
is the combination that shows a CUDA card AND an AMD iGPU (HIP+CUDA cannot
|
is the combination that shows a CUDA card AND an AMD iGPU (HIP+CUDA cannot
|
||||||
@@ -411,20 +411,16 @@ measurement that would settle it.
|
|||||||
|
|
||||||
- Speculative decoding helps low-QPS, memory-bound, latency-focused workloads. It helps less at high QPS.
|
- Speculative decoding helps low-QPS, memory-bound, latency-focused workloads. It helps less at high QPS.
|
||||||
- DFlash needs a non-causal attention backend; the ROCm flash_attn backend must be validated before production use.
|
- DFlash needs a non-causal attention backend; the ROCm flash_attn backend must be validated before production use.
|
||||||
- The old `--swap-space` flag is retired; the live CPU-KV-offload knob pair is owned by [[RAM CPU Offloading in VLLM]].
|
-The old `--swap-space` flag is retired; the live CPU-KV-offload knob pair is owned by .
|
||||||
- `--speculative-config` is the preferred CLI. Legacy `--spec-method`, `--spec-model`, and `--spec-tokens` flags still exist.
|
- `--speculative-config` is the preferred CLI. Legacy `--spec-method`, `--spec-model`, and `--spec-tokens` flags still exist.
|
||||||
- Use `rejection_sample_method` default `strict` for lossless output.
|
- Use `rejection_sample_method` default `strict` for lossless output.
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[Quantization by Hardware Architecture]]
|
- [[Quantization by Hardware Architecture]]
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]]
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]]
|
|
||||||
- [[Dankclaw System Specs]]
|
- [[Dankclaw System Specs]]
|
||||||
- [[Dankserver System Specs]]
|
- [[Dankserver System Specs]]
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[deployment]]
|
|
||||||
- [[GGUF Compatibility]] — header parsing and custom tensor-type lock-in
|
- [[GGUF Compatibility]] — header parsing and custom tensor-type lock-in
|
||||||
- [[Llama Cpp Serving Sizing]] — slot-pool and drafter KV sizing arithmetic
|
- [[Llama Cpp Serving Sizing]] — slot-pool and drafter KV sizing arithmetic
|
||||||
- [[Llama Cpp Multi-Device Splits]] — where the drafter's KV actually lands
|
- where the drafter's KV actually lands
|
||||||
- [[frank-dragoon]] and [[frank-zealot]] — the single-GPU env drop-in A/B vehicle
|
- [[frank-dragoon]] and [[frank-zealot]] — the single-GPU env drop-in A/B vehicle
|
||||||
@@ -30,8 +30,8 @@ taken in.
|
|||||||
|
|
||||||
- **Backend**: that era's production was **Vulkan0**, not ROCm. On this box ROCm
|
- **Backend**: that era's production was **Vulkan0**, not ROCm. On this box ROCm
|
||||||
wins prefill and Vulkan wins decode, and the decode gap widens with context:
|
wins prefill and Vulkan wins decode, and the decode gap widens with context:
|
||||||
the 64k A/B is [[qwen3.8-strix-optimizations]] §4, the 262k A/B is
|
the 64k A/B is §4, the 262k A/B is
|
||||||
[[qwen3.8-spec-matrix]] §High-Context Backend Comparison. The "HIP
|
§High-Context Backend Comparison. The "HIP
|
||||||
out-performs Vulkan for decode" claim did not hold on the 8060S.
|
out-performs Vulkan for decode" claim did not hold on the 8060S.
|
||||||
- **Power profile**: the `/sys/class/drm/card0/device/power_profile` file
|
- **Power profile**: the `/sys/class/drm/card0/device/power_profile` file
|
||||||
does not exist on this machine (checked 2026-08-15). GPU DPM is already
|
does not exist on this machine (checked 2026-08-15). GPU DPM is already
|
||||||
@@ -39,7 +39,7 @@ taken in.
|
|||||||
- **KV cache**: production ran `q8_0` K **and** `q8_0` V (both quantized). What
|
- **KV cache**: production ran `q8_0` K **and** `q8_0` V (both quantized). What
|
||||||
`frank-zealot` runs now is its own Current shape — this row is not it.
|
`frank-zealot` runs now is its own Current shape — this row is not it.
|
||||||
- **Spec**: DFlash/DSpark are retired. That era's production was `--spec-default`
|
- **Spec**: DFlash/DSpark are retired. That era's production was `--spec-default`
|
||||||
(ngram-mod) with draft-mtp first — timeline on [[qwen3.8-spec-matrix]]; the
|
(ngram-mod) with draft-mtp first — timeline on ; the
|
||||||
spec setting each endpoint runs today is on its own page.
|
spec setting each endpoint runs today is on its own page.
|
||||||
- **TTM**: the `pages_limit` raise that removed the ~15 GiB ceiling is recorded
|
- **TTM**: the `pages_limit` raise that removed the ~15 GiB ceiling is recorded
|
||||||
once on [[Dankclaw System Specs]] (Memory).
|
once on [[Dankclaw System Specs]] (Memory).
|
||||||
@@ -54,8 +54,8 @@ taken in.
|
|||||||
|
|
||||||
That 2026-08 reading is the firmware-VGM era: the BIOS carved ~96 GiB as
|
That 2026-08 reading is the firmware-VGM era: the BIOS carved ~96 GiB as
|
||||||
"dedicated" iGPU VRAM, so the kernel saw only ~30 GiB (the same split is
|
"dedicated" iGPU VRAM, so the kernel saw only ~30 GiB (the same split is
|
||||||
recorded in [[ling-3.0-flash-trial]] §Why Vulkan + GTT; the 76.2 GiB row is an
|
recorded in §Why Vulkan + GTT; the 76.2 GiB row is an
|
||||||
era [[frank-dragoon]] load reading). Every live number on this box — `MemTotal`,
|
era [[frank-dragoon]] load reading). Every live number on this box — `MemTotal`
|
||||||
the ~114 GiB GTT aperture that is the real GPU budget, the 2.0 GiB dedicated
|
the ~114 GiB GTT aperture that is the real GPU budget, the 2.0 GiB dedicated
|
||||||
carve and the `ttm` page cap — belongs to [[Dankclaw System Specs]], re-read
|
carve and the `ttm` page cap — belongs to [[Dankclaw System Specs]], re-read
|
||||||
2026-10-18.
|
2026-10-18.
|
||||||
@@ -125,9 +125,9 @@ All require root and a reboot for the modprobe change.
|
|||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
- [[frank-dragoon]] — measured backend/spec numbers
|
- [[frank-dragoon]] — measured backend/spec numbers
|
||||||
- [[deployment]] — deploy and update process
|
- deploy and update process
|
||||||
- AMD ROCm RDNA3.5 system optimization guide (gfx1151 memory/BIOS)
|
- AMD ROCm RDNA3.5 system optimization guide (gfx1151 memory/BIOS)
|
||||||
- llama.cpp source: `ggml/CMakeLists.txt`, `ggml/src/ggml-hip/CMakeLists.txt`,
|
- llama.cpp source: `ggml/CMakeLists.txt`, `ggml/src/ggml-hip/CMakeLists.txt`
|
||||||
`common/arg.cpp`, `common/common.h`
|
`common/arg.cpp`, `common/common.h`
|
||||||
- NotebookCheck Ryzen AI MAX+ 395 analysis
|
- NotebookCheck Ryzen AI MAX+ 395 analysis
|
||||||
|
|
||||||
@@ -135,5 +135,3 @@ All require root and a reboot for the modprobe change.
|
|||||||
|
|
||||||
- [[frank-dragoon]]
|
- [[frank-dragoon]]
|
||||||
- [[DFlash DSpark Speculative Decoding]]
|
- [[DFlash DSpark Speculative Decoding]]
|
||||||
- [[deployment]]
|
|
||||||
- [[index]]
|
|
||||||
@@ -32,7 +32,7 @@ Dankclaw is an AMD APU + RTX 3090 inference host. It ran CIRU ROCmFPX llama.cpp
|
|||||||
| Device ID | 0x1586 |
|
| Device ID | 0x1586 |
|
||||||
| Architecture | gfx1151 (Strix Halo) |
|
| Architecture | gfx1151 (Strix Halo) |
|
||||||
| GPU-Visible Memory | ~114 GiB GTT aperture (the budget; `mem_info_gtt_total` = 122,402,963,456 B read 2026-10-18; rocm-smi's smaller SVM carve-out is not the budget) |
|
| GPU-Visible Memory | ~114 GiB GTT aperture (the budget; `mem_info_gtt_total` = 122,402,963,456 B read 2026-10-18; rocm-smi's smaller SVM carve-out is not the budget) |
|
||||||
| Physical RAM | 121 GiB visible to the OS (`free -g` total 121, read 2026-10-18) on a 128 GB part — 8 × 16 GB LPDDR5 ([[ling-3.0-flash-trial]] §hardware). The 64 GiB this page recorded as "corrected 2026-08-17" (from 32 GB) is stale |
|
| Physical RAM | 121 GiB visible to the OS (`free -g` total 121, read 2026-10-18) on a 128 GB part — 8 × 16 GB LPDDR5 ( §hardware). The 64 GiB this page recorded as "corrected 2026-08-17" (from 32 GB) is stale |
|
||||||
|
|
||||||
### NPU
|
### NPU
|
||||||
|
|
||||||
@@ -44,9 +44,9 @@ Dankclaw is an AMD APU + RTX 3090 inference host. It ran CIRU ROCmFPX llama.cpp
|
|||||||
| Driver | amdxdna 0.7.0, in-tree — bound since 2026-10-18 (`lspci -k -s c6:00.1` → `Kernel driver in use: amdxdna`; journal 2026-10-18 01:25:14 `Initialized amdxdna_accel_driver 0.7.0 for 0000:c6:00.1 on minor 0`) |
|
| Driver | amdxdna 0.7.0, in-tree — bound since 2026-10-18 (`lspci -k -s c6:00.1` → `Kernel driver in use: amdxdna`; journal 2026-10-18 01:25:14 `Initialized amdxdna_accel_driver 0.7.0 for 0000:c6:00.1 on minor 0`) |
|
||||||
| Firmware | 1.1.2.65 (`/usr/lib/firmware/amdnpu/17f0_11/npu_7.sbin` → `npu.sbin.1.1.2.65.zst`, loaded at boot 2026-10-18 01:25:13) |
|
| Firmware | 1.1.2.65 (`/usr/lib/firmware/amdnpu/17f0_11/npu_7.sbin` → `npu.sbin.1.1.2.65.zst`, loaded at boot 2026-10-18 01:25:13) |
|
||||||
| Runtime | None installed. FastFlowLM 0.9.45 + XRT (installed 2026-08-16) was **retired 2026-09-11** with the `amd_iommu=off` decision — see [[dankclaw-iommu-translation-off]]; the package remains at `/home/dank/GitPackages/fastflowlm_0.9.45_ubuntu26.04_amd64.deb` |
|
| Runtime | None installed. FastFlowLM 0.9.45 + XRT (installed 2026-08-16) was **retired 2026-09-11** with the `amd_iommu=off` decision — see [[dankclaw-iommu-translation-off]]; the package remains at `/home/dank/GitPackages/fastflowlm_0.9.45_ubuntu26.04_amd64.deb` |
|
||||||
| Status | Not in service — no NPU endpoint is deployed. The device itself is usable since the 2026-10-18 `iommu=pt` boot (`/dev/accel/accel0` present, `crw-rw---- root:render 261,0`); Gitea issue #3 tracks enablement. See [[FastFlowLM NPU Inference]] |
|
| Status | Not in service — no NPU endpoint is deployed. The device itself is usable since the 2026-10-18 `iommu=pt` boot (`/dev/accel/accel0` present, `crw-rw---- root:render 261,0`); Gitea issue #3 tracks enablement. See |
|
||||||
|
|
||||||
The NPU endpoint is retired (2026-09-11; [[frank-npu]]). The `frank-zealot` name now serves **Ternary-Bonsai-2-27B-PQ2_0** on the RTX 3090 via the llama.cpp PrismML fork (see [[frank-zealot]]); the Gemma4-12B QAT rung it once pointed at is retired history ([[frank-zealot]], 2026-09-17 → 10-01).
|
The NPU endpoint is retired (2026-09-11; ). The `frank-zealot` name now serves **Ternary-Bonsai-2-27B-PQ2_0** on the RTX 3090 via the llama.cpp PrismML fork (see ); the Gemma4-12B QAT rung it once pointed at is retired history (, 2026-09-17 → 10-01).
|
||||||
|
|
||||||
### ROCm
|
### ROCm
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ The NPU endpoint is retired (2026-09-11; [[frank-npu]]). The `frank-zealot` name
|
|||||||
| Interface | Type | Speed | IP | Notes |
|
| Interface | Type | Speed | IP | Notes |
|
||||||
|-----------|------|-------|----|-------|
|
|-----------|------|-------|----|-------|
|
||||||
| eno1 | onboard | 2500 Mb/s (2.5 GbE) | 192.168.0.143 (DHCP) | Carried the RPC path to Dankserver during the 2026-08-17 pipeline-parallelism trial; no RPC listener runs today (`ss -ltn` shows no :50052, 2026-10-18). Default route via 192.168.0.1. |
|
| eno1 | onboard | 2500 Mb/s (2.5 GbE) | 192.168.0.143 (DHCP) | Carried the RPC path to Dankserver during the 2026-08-17 pipeline-parallelism trial; no RPC listener runs today (`ss -ltn` shows no :50052, 2026-10-18). Default route via 192.168.0.1. |
|
||||||
| enx00e04cc2e1e5 | USB-C to Ethernet adapter | 1000 Mb/s | 192.168.0.116 | UNPLUGGED 2026-08-17. While present, the route to 192.168.0.235 went through this adapter (lower route metric), so all RPC traffic was capped at 1 GbE. See [[Pipeline Parallelism/Ethernet Bandwidth Math]] section 0. |
|
| enx00e04cc2e1e5 | USB-C to Ethernet adapter | 1000 Mb/s | 192.168.0.116 | UNPLUGGED 2026-08-17. While present, the route to 192.168.0.235 went through this adapter (lower route metric), so all RPC traffic was capped at 1 GbE. See section 0. |
|
||||||
| docker0 | bridge | - | 172.17.0.1 | Usually down. |
|
| docker0 | bridge | - | 172.17.0.1 | Usually down. |
|
||||||
|
|
||||||
The L2 gateway/switch at 192.168.0.1 (MAC f0:4b:8a:65:c5:40) is at
|
The L2 gateway/switch at 192.168.0.1 (MAC f0:4b:8a:65:c5:40) is at
|
||||||
@@ -112,5 +112,3 @@ cable between claw and server is required for 10G.
|
|||||||
|
|
||||||
- [[frank-dragoon]]
|
- [[frank-dragoon]]
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
- [[Shared Memory Architecture Dankclaw]]
|
||||||
- [[frank-redo-2026-08-17]]
|
|
||||||
- [[FastFlowLM NPU Inference]]
|
|
||||||
@@ -30,7 +30,7 @@ read 2026-10-18).
|
|||||||
| Property | Value |
|
| Property | Value |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| Board | **ASUS ProArt B850-Creator WiFi NEO** (Rev 1.xx) — swapped in 2026-09-20 |
|
| Board | **ASUS ProArt B850-Creator WiFi NEO** (Rev 1.xx) — swapped in 2026-09-20 |
|
||||||
| Previous | ASUS TUF GAMING B650-E WIFI (card #2 was behind the chipset there; see [[Dual-R9700-Cooperation]]) |
|
| Previous | ASUS TUF GAMING B650-E WIFI (card #2 was behind the chipset there; see ) |
|
||||||
| GPU lanes | CPU GPP ports bifurcated **x8/x8** at Gen5: `PCIEX16(G5)_1` → `00:01.1`, `PCIEX16(G5)_2` → `00:01.3` (both bridges: `current_link_width=8`, `current_link_speed=32.0 GT/s`, read 2026-10-18); ~31.5 GB/s per card. Each card sits behind an on-board PCIe switch (`01:00.0`/`02:00.0`, `05:00.0`/`06:00.0` — "Navi 10 XL Upstream/Downstream Port of PCI Express Switch"), so the GPU endpoint itself trains **x16** Gen5 while the CPU-facing leg stays x8 |
|
| GPU lanes | CPU GPP ports bifurcated **x8/x8** at Gen5: `PCIEX16(G5)_1` → `00:01.1`, `PCIEX16(G5)_2` → `00:01.3` (both bridges: `current_link_width=8`, `current_link_speed=32.0 GT/s`, read 2026-10-18); ~31.5 GB/s per card. Each card sits behind an on-board PCIe switch (`01:00.0`/`02:00.0`, `05:00.0`/`06:00.0` — "Navi 10 XL Upstream/Downstream Port of PCI Express Switch"), so the GPU endpoint itself trains **x16** Gen5 while the CPU-facing leg stays x8 |
|
||||||
| Atomic routing | `AtomicOpsCap: Routing+` on every bridge in both GPU chains, `AtomicOpsCtl: ReqEn+` on both endpoints — the condition that ruled out TP=2 for the previous board (2026-09-20 census; not re-readable today — `lspci -vv` needs root) *(unverified 2026-10-18)* |
|
| Atomic routing | `AtomicOpsCap: Routing+` on every bridge in both GPU chains, `AtomicOpsCtl: ReqEn+` on both endpoints — the condition that ruled out TP=2 for the previous board (2026-09-20 census; not re-readable today — `lspci -vv` needs root) *(unverified 2026-10-18)* |
|
||||||
| Free | `PCIEX16(G4)` (chipset, x4), M.2_2, M.2_3 |
|
| Free | `PCIEX16(G4)` (chipset, x4), M.2_2, M.2_3 |
|
||||||
@@ -55,7 +55,7 @@ read 2026-10-18).
|
|||||||
| Architecture | gfx1201 (RDNA4) |
|
| Architecture | gfx1201 (RDNA4) |
|
||||||
| VRAM | 32,624 MiB each (`rocm-smi` reports 34,208,743,424 B per card) |
|
| VRAM | 32,624 MiB each (`rocm-smi` reports 34,208,743,424 B per card) |
|
||||||
| Link | PCIe 5.0, endpoint-trained **x16** each behind the on-board switch; the CPU-facing leg is x8 (`00:01.1` / `00:01.3`, both `current_link_width=8` at 32.0 GT/s, read 2026-10-18). No XGMI. Separate IOMMU groups — `03:00.0` → group **15**, `07:00.0` → group **20** (`/sys/bus/pci/devices/.../iommu_group`, read 2026-10-18) |
|
| Link | PCIe 5.0, endpoint-trained **x16** each behind the on-board switch; the CPU-facing leg is x8 (`00:01.1` / `00:01.3`, both `current_link_width=8` at 32.0 GT/s, read 2026-10-18). No XGMI. Separate IOMMU groups — `03:00.0` → group **15**, `07:00.0` → group **20** (`/sys/bus/pci/devices/.../iommu_group`, read 2026-10-18) |
|
||||||
| P2P | **Live and in use.** The running engine prints "I device: the 2 cards reach each other's memory directly (peer to peer)" (`podman logs frank-archon`, 2026-10-15 boot). The 2026-09-20 census had predicted the opposite — ACS `ReqRedir+` on both root ports, so traffic hairpinning at the root complex — and that prediction is superseded; see [[Dual-R9700-Cooperation]] |
|
| P2P | **Live and in use.** The running engine prints "I device: the 2 cards reach each other's memory directly (peer to peer)" (`podman logs frank-archon`, 2026-10-15 boot). The 2026-09-20 census had predicted the opposite — ACS `ReqRedir+` on both root ports, so traffic hairpinning at the root complex — and that prediction is superseded; see |
|
||||||
| PCI | 03:00.0 and **07:00.0** (were `03:00.0` / `0b:00.0` on the B650-E) |
|
| PCI | 03:00.0 and **07:00.0** (were `03:00.0` / `0b:00.0` on the B650-E) |
|
||||||
| iGPU | Raphael (gfx1036) at **11:00.0** — enumerates as ROCm device 2 (rocm-smi GPU[2], 512 MiB). The retired llama.cpp trios hid it with `HIP_VISIBLE_DEVICES=0,1`; the live radiance engine instead enumerates all three AQL devices and ignores it ("3 devices [0:gfx1201 1:gfx1201 2:gfx1036], using 2 of 3, ignoring gfx1036") |
|
| iGPU | Raphael (gfx1036) at **11:00.0** — enumerates as ROCm device 2 (rocm-smi GPU[2], 512 MiB). The retired llama.cpp trios hid it with `HIP_VISIBLE_DEVICES=0,1`; the live radiance engine instead enumerates all three AQL devices and ignores it ("3 devices [0:gfx1201 1:gfx1201 2:gfx1036], using 2 of 3, ignoring gfx1036") |
|
||||||
|
|
||||||
@@ -104,8 +104,7 @@ RCCL-backed rebuild needs no new host packages.
|
|||||||
| Serving stack | radiance 1.0.13 container (`stilldeadcode/radiance@sha256:4c3f484564…`), TP=2 across both R9700s — [[frank-archon]]. The llama.cpp `fb27a525` build at `~/GitPackages/llama.cpp-dflash2/build-rdna4-rocm714` is the retired frank-medium artifact |
|
| Serving stack | radiance 1.0.13 container (`stilldeadcode/radiance@sha256:4c3f484564…`), TP=2 across both R9700s — [[frank-archon]]. The llama.cpp `fb27a525` build at `~/GitPackages/llama.cpp-dflash2/build-rdna4-rocm714` is the retired frank-medium artifact |
|
||||||
|
|
||||||
The stale AMD-repo ROCm 6.3.3 packages (167, `~24.04`-suffixed) that used to
|
The stale AMD-repo ROCm 6.3.3 packages (167, `~24.04`-suffixed) that used to
|
||||||
hijack `libhsa-runtime64` were purged on 2026-09-15 — see
|
hijack `libhsa-runtime64` were purged on 2026-09-15 —
|
||||||
[[frank-medium-rocmfpx]].
|
|
||||||
|
|
||||||
### Active Services
|
### Active Services
|
||||||
|
|
||||||
@@ -117,12 +116,12 @@ host-network podman container answering `curl 127.0.0.1:7770/health` →
|
|||||||
|
|
||||||
The services this page once called active are gone: `frank-medium.service`
|
The services this page once called active are gone: `frank-medium.service`
|
||||||
(llama-server, 32:33 layer split + DFlash2 drafter) is `not-found` — that rung
|
(llama-server, 32:33 layer split + DFlash2 drafter) is `not-found` — that rung
|
||||||
ran 2026-09-16 → 09-20 and is documented in [[frank-medium-llamacpp-dflash2]];
|
ran 2026-09-16 → 09-20 and is documented in
|
||||||
`frank-zealot.service` (FreeToken/CUDA) was retired with the 2026-09-15 GPU
|
`frank-zealot.service` (FreeToken/CUDA) was retired with the 2026-09-15 GPU
|
||||||
swap. Its archived unit was moved to `~/.AGENTTRASH/20260915-frank-zealot-retire/host-units/`
|
swap. Its archived unit was moved to `~/.AGENTTRASH/20260915-frank-zealot-retire/host-units/`
|
||||||
and the vllm-radiance replica trio to `~/.AGENTTRASH/20260916-vllm-radiance-retire/`
|
and the vllm-radiance replica trio to `~/.AGENTTRASH/20260916-vllm-radiance-retire/`
|
||||||
— neither path exists today (checked 2026-10-18), so the archive location is
|
— neither path exists today (checked 2026-10-18), so the archive location is
|
||||||
unverified. See [[deployment]].
|
unverified. See .
|
||||||
|
|
||||||
## Hardware Relevance
|
## Hardware Relevance
|
||||||
|
|
||||||
@@ -134,7 +133,7 @@ unverified. See [[deployment]].
|
|||||||
tokens at 32:33, ~6.3/~5.2 GiB headroom) is history. Either way a batch or
|
tokens at 32:33, ~6.3/~5.2 GiB headroom) is history. Either way a batch or
|
||||||
context change needs a re-fit and not a guess.
|
context change needs a re-fit and not a guess.
|
||||||
- 246 GiB system RAM is **not** headroom: the live shape pins a 43.88 GiB host
|
- 246 GiB system RAM is **not** headroom: the live shape pins a 43.88 GiB host
|
||||||
pool per rank (`FRANK_ARCHON_HOST_POOL_MIB=44928`,
|
pool per rank (`FRANK_ARCHON_HOST_POOL_MIB=44928`
|
||||||
`scripts/frank-archon.env:184`) plus the 47.68 GiB n-gram/PLE table in RAM,
|
`scripts/frank-archon.env:184`) plus the 47.68 GiB n-gram/PLE table in RAM,
|
||||||
and `free -g` showed **90 GiB available** with the engine loaded (read
|
and `free -g` showed **90 GiB available** with the engine loaded (read
|
||||||
2026-10-18). The frank-medium claim that "nothing in the serving path is
|
2026-10-18). The frank-medium claim that "nothing in the serving path is
|
||||||
@@ -147,6 +146,6 @@ unverified. See [[deployment]].
|
|||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[frank-archon]] — the active serving runbook
|
- [[frank-archon]] — the active serving runbook
|
||||||
- [[Dual-R9700-Cooperation]] — why the PCIe path matters, and what the swap changed
|
- why the PCIe path matters, and what the swap changed
|
||||||
- [[deployment]] — the deploy flow
|
- the deploy flow
|
||||||
- [[Shared Memory Architecture Dankclaw]] — for contrast, Dankclaw's unified memory
|
- [[Shared Memory Architecture Dankclaw]] — for contrast, Dankclaw's unified memory
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Dual R9700 Cooperation
|
|
||||||
description: "Why two R9700s could not run tensor-parallel on the old board, and what the board swap unblocked."
|
|
||||||
tags:
|
|
||||||
- frank-medium
|
|
||||||
- rocm
|
|
||||||
- rccl
|
|
||||||
- pcie-atomics
|
|
||||||
- tensor-parallelism
|
|
||||||
- dankserver
|
|
||||||
timestamp: 2026-09-16
|
|
||||||
---
|
|
||||||
|
|
||||||
# Dual R9700 Cooperation
|
|
||||||
|
|
||||||
Dankserver's two R9700s run **one tensor-parallel engine across both cards** —
|
|
||||||
today that is `frank-archon` under radiance 1.0.13, TP=2
|
|
||||||
([[frank-archon]]). When this page was written the endpoint
|
|
||||||
was `frank-medium`: first llama.cpp's layer split
|
|
||||||
([[frank-medium-llamacpp-dflash2]], 2026-09-16 → 09-20), then from 2026-09-20
|
|
||||||
one vLLM-radiance process at `tensor_parallel_size 2`
|
|
||||||
([[frank-medium-radiance-mxfp4]], 2026-09-20 → 09-26). This page records why
|
|
||||||
true tensor parallelism was impossible on the previous board (TUF GAMING B650-E
|
|
||||||
WIFI), and what the 2026-09-20 board swap changed.
|
|
||||||
|
|
||||||
## The short answer
|
|
||||||
|
|
||||||
**The wall is gone.** On the B650-E, card #2 sat behind the chipset, whose
|
|
||||||
bridges could not route PCIe atomic operations. RCCL's kernels carry a hidden
|
|
||||||
hostcall-buffer parameter and ROCr refuses to supply it to a device without
|
|
||||||
atomics, so *no* RCCL collective or P2P operation could be dispatched on that
|
|
||||||
card — see the [measured matrix](#measured-matrix) and the
|
|
||||||
[2026-09-16 table](#the-same-gate-as-this-board-reports-it-2026-09-16) below.
|
|
||||||
|
|
||||||
On 2026-09-20 the board became an **ASUS ProArt B850-Creator WiFi NEO**, which
|
|
||||||
bifurcates the CPU's GPP ports **x8/x8**: the board-swap path this page
|
|
||||||
recommended, at B850 rather than X870E money. Both cards now hang off CPU root
|
|
||||||
ports (`00:01.1`, `00:01.3`), every bridge on both chains reads
|
|
||||||
`AtomicOpsCap: Routing+`, and **both endpoints read `AtomicOpsCtl: ReqEn+`** —
|
|
||||||
the exact precondition this page's own test plan named, and it is now
|
|
||||||
[measured](#measured-verdict-2026-09-20) rather than assumed.
|
|
||||||
|
|
||||||
What that bought:
|
|
||||||
|
|
||||||
- **Both cooperation modes now work, and they were both run.** vLLM TP=2 booted
|
|
||||||
and served (its R4D allreduce needs no RCCL collective at all), and llama.cpp
|
|
||||||
`--split-mode tensor` booted and served against an RCCL-backed build — so a
|
|
||||||
collective HAS been dispatched on this host since the swap.
|
|
||||||
- **TP=2 is the production shape.** It wins on every axis measured, by 2.1x on
|
|
||||||
decode and 3.6x at 4-way concurrency, and it replaced the layer split on the day
|
|
||||||
of measurement. The payload has moved since: the MXFP4 `frank-medium` shape is
|
|
||||||
retired 2026-09-26, and what runs today is the iq4r publisher under radiance
|
|
||||||
1.0.13 ([[frank-archon]]).
|
|
||||||
- **The drafter, not the collective, is what llama.cpp lost.** Its tensor mode
|
|
||||||
could not carry the DFlash2 drafter, which is the honest caveat on the
|
|
||||||
comparison below.
|
|
||||||
- **The bandwidth story moved too.** Both cards ride Gen5 x8 (~31.5 GB/s each):
|
|
||||||
card 0 effectively unchanged (was Gen4 x16), card 1 up ~16x (was Gen4 x1,
|
|
||||||
~1.97 GB/s). The M.2→PCIe riser plan is dead — `PCIEX16_2` holds card #2 now
|
|
||||||
and M.2_2 is free.
|
|
||||||
|
|
||||||
## The gate, in source
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
void Device::checkAtomicSupport() { // rocclr/device/rocm/rocdevice.cpp:181
|
|
||||||
std::vector<amd::Device::LinkAttrType> link_attrs;
|
|
||||||
link_attrs.push_back(std::make_pair(LinkAttribute::kLinkAtomicSupport, 0));
|
|
||||||
if (findLinkInfo(cpu_agent_info_->fine_grain_pool, &link_attrs)) {
|
|
||||||
if (link_attrs[0].second == 1) {
|
|
||||||
info_.pcie_atomics_ = true; // no else branch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The flag is a **queried hardware property**, consumed at
|
|
||||||
`rocvirtual.cpp:3954` (the hostcall/AQL dispatch path), in `rocblit.cpp` (SDMA
|
|
||||||
copy paths) and exported as `hipDeviceProp.hostNativeAtomicSupported`. There is
|
|
||||||
no environment variable, module parameter, or sysfs toggle around it — verified
|
|
||||||
by grepping ROCclr for `getenv`/`HSA_*`/`ROCR_*`/`AMD_*` in the gate's
|
|
||||||
neighborhood: none. Writing the endpoint's Device Control 2 bit 6 by hand
|
|
||||||
(`setpci -s 0b:00.0 CAP_EXP+28.w=0440`) does nothing because the value is
|
|
||||||
consumed at driver probe time.
|
|
||||||
|
|
||||||
What has to be true upstream of it: every PCIe bridge on the GPU's path to the
|
|
||||||
CPU must advertise `AtomicOpsCap: Routing+`. On this board card #2's path
|
|
||||||
includes `05:00.0` and `06:09.0`, both `Routing-` — a **read-only capability
|
|
||||||
bit**, so no software can enable it.
|
|
||||||
|
|
||||||
### The same gate, as this board reports it (2026-09-16)
|
|
||||||
|
|
||||||
`lspci -vv` on dankserver. Card 0's chain is root port `00:01.1` → `01:00.0` →
|
|
||||||
`02:00.0` → `03:00.0`; card 1's is `00:02.1` → `05:00.0` → `06:00.0` →
|
|
||||||
`06:09.0` → `09:00.0` → `0a:00.0` → `0b:00.0`:
|
|
||||||
|
|
||||||
| Device | Role | `AtomicOpsCap` | `AtomicOpsCtl` | Link |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `00:01.1` | CPU root port → PCIEX16_1 | Routing+ | — | x16 |
|
|
||||||
| `03:00.0` | **card 0** | 32bit+ 64bit+ | **ReqEn+** | x16 |
|
|
||||||
| `00:02.1` | CPU root port → chipset | **Routing+** | — | x4 |
|
|
||||||
| `05:00.0`, `06:00.0`, `06:09.0` | B650 bridges | **Routing−** | — | x1 at `06:09.0` |
|
|
||||||
| `09:00.0`, `0a:00.0` | PCIe switch ports | Routing+ | — | x1 (downgraded) |
|
|
||||||
| `0b:00.0` | **card 1** | 32bit+ 64bit+ | **ReqEn−** | x16 |
|
|
||||||
|
|
||||||
Three things fall out of that table, none of them in the source alone:
|
|
||||||
|
|
||||||
- **The CPU side is fine** — `00:02.1` is `Routing+`. The wall is inside the
|
|
||||||
chipset, not the platform, which is why a slot on CPU lanes is the fix.
|
|
||||||
- **The endpoint's own bit differs between the cards, and that is the kernel's
|
|
||||||
doing.** `pci_enable_atomic_ops_to_root()` walks the bridge chain and refuses
|
|
||||||
to set Requester Enable when any bridge cannot route atomics. ROCclr's
|
|
||||||
`pcie_atomics_` and the endpoint's `ReqEn` are two faces of one cause, and it
|
|
||||||
makes `lspci -vv -s <bdf> | grep AtomicOpsCtl` a one-command diagnosis: on
|
|
||||||
this host card 0 reads `ReqEn+` and card 1 `ReqEn−`.
|
|
||||||
- **Card 1's effective link is x1**, not the x16 its own capability block
|
|
||||||
advertises: `06:09.0` trained at x1, so ~2 GB/s is the real ceiling. That is a
|
|
||||||
bandwidth fact rather than an atomics one, and it only matters for
|
|
||||||
collectives — a 27B TP=2 allreduce is ≈5.6 ms/token at x1 against ≈1.4 ms at
|
|
||||||
x4. The layer split's boundary copy does not care.
|
|
||||||
|
|
||||||
## Re-measured 2026-09-20 (ProArt B850-Creator WiFi NEO)
|
|
||||||
|
|
||||||
`lspci -vv` after the board swap. Card 0's chain is `00:01.1` → `01:00.0` →
|
|
||||||
`02:00.0` → `03:00.0`; card 1's is `00:01.3` → `05:00.0` → `06:00.0` →
|
|
||||||
`07:00.0`. **No chipset bridge is in either path** — `00:02.1` now carries only
|
|
||||||
LAN/SATA/WiFi:
|
|
||||||
|
|
||||||
| Device | Role | `AtomicOpsCap` | `AtomicOpsCtl` | Link |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `00:01.1` | CPU GPP → PCIEX16_1 | Routing+ | — | Gen5 x8 |
|
|
||||||
| `01:00.0` | card 0 switch upstream | Routing+ | — | Gen5 x8 |
|
|
||||||
| `02:00.0` | card 0 switch downstream | Routing+ | — | Gen5 x16 |
|
|
||||||
| `03:00.0` | **card 0** | 32bit+ 64bit+ | **ReqEn+** | Gen5 x16 (on-card) |
|
|
||||||
| `00:01.3` | CPU GPP → PCIEX16_2 | Routing+ | — | Gen5 x8 |
|
|
||||||
| `05:00.0` | card 1 switch upstream | Routing+ | — | Gen5 x8 |
|
|
||||||
| `06:00.0` | card 1 switch downstream | Routing+ | — | Gen5 x16 |
|
|
||||||
| `07:00.0` | **card 1** | 32bit+ 64bit+ | **ReqEn+** | Gen5 x16 (on-card) |
|
|
||||||
|
|
||||||
- Both endpoints read `CAP_EXP+28.w = 0x1440` — bit 6 set, which is the
|
|
||||||
kernel's `pci_enable_atomic_ops_to_root()` having walked a chain with no
|
|
||||||
`Routing-` bridge. Same one-command diagnosis, opposite answer.
|
|
||||||
- `dmesg` agrees on bandwidth: `252.056 Gb/s available PCIe bandwidth, limited
|
|
||||||
by 32.0 GT/s PCIe x8 link at 0000:00:01.1` (and `.13`) — ~31.5 GB/s per
|
|
||||||
card. DMI confirms the bifurcation: `PCIEX16(G5)_2`, "8x or x8".
|
|
||||||
- `rocm-smi --showtopo` reported `PCIE` between the cards (Navi 48 has no XGMI),
|
|
||||||
they were in separate IOMMU groups (15 and 20 — re-read 2026-10-18: still 15
|
|
||||||
and 20), and both root ports had ACS `ReqRedir+`, so this page inferred P2P
|
|
||||||
would hairpin at the root complex. **That inference is superseded:** the live
|
|
||||||
engine reports the two cards reaching each other's memory directly, which
|
|
||||||
[[frank-archon]] and [[Dankserver System Specs]] record. Plain P2P traffic
|
|
||||||
measured 45.27 GB/s bidirectional on the *old* board — DMA was never the gate,
|
|
||||||
atomics were.
|
|
||||||
- Kernel 7.0.0-31-generic with `iommu=pt` then; this host runs 7.0.0-38-generic
|
|
||||||
with `iommu=pt` today (`/proc/cmdline`, read 2026-10-18). Both were ruled out
|
|
||||||
as causes ([above](#ruled-out-empirically-do-not-re-derive)) and this host's
|
|
||||||
IOMMU posture is unchanged.
|
|
||||||
|
|
||||||
**Still unproven (as of 2026-09-20).** The gate that matters is ROCclr's
|
|
||||||
`checkAtomicSupport()` reading the `kLinkAtomicSupport` link attribute; the ReqEn
|
|
||||||
bit is the same cause seen from the kernel side, but no RCCL kernel had been
|
|
||||||
launched since the swap. Until the 2-rank probe runs — or a `--split-mode row`
|
|
||||||
boot that does *not* log `internal AllReduce init failed ... falling back to
|
|
||||||
meta-backend butterfly` — "collectives work here" was a strong expectation, not a
|
|
||||||
measurement. **RCCL never got that measurement**; what settled the operational
|
|
||||||
question instead is production traffic — radiance's TP=2 wire carries cross-rank
|
|
||||||
all-reduces over the direct P2P path and prints its own precision, and the
|
|
||||||
measured `wht6` banner with its exact-step bound is on [[frank-archon]].
|
|
||||||
|
|
||||||
## Measured matrix
|
|
||||||
|
|
||||||
Bare 2-rank `torch.distributed` (`backend=nccl`) on the two R9700s, no vLLM, no
|
|
||||||
model. Rank 0 = card #1 (CPU lanes), rank 1 = card #2 (chipset):
|
|
||||||
|
|
||||||
| Operation | rank 0 (card #1) | rank 1 (card #2) |
|
|
||||||
|---|---|---|
|
|
||||||
| `broadcast` | OK | **FAIL** `DistBackendError` |
|
|
||||||
| `all_reduce` | OK | **FAIL** |
|
|
||||||
| `barrier` | OK | **FAIL** |
|
|
||||||
| `send` / `recv` (P2P) | blocked, then timeout | **FAIL** |
|
|
||||||
| `all_gather` (after failure) | group unusable | — |
|
|
||||||
|
|
||||||
The `AMD_LOG_LEVEL=3` trace names the cause: `Pcie atomics not enabled, hostcall
|
|
||||||
not supported` → `AQL dispatch failed!` → `hipLaunchKernel` returns
|
|
||||||
`hipErrorIllegalState` for `ncclDevKernel_Generic_4(...)`. Nothing in vLLM or
|
|
||||||
RCCL mentions atomics; the runtime log is the only place it appears.
|
|
||||||
|
|
||||||
**`send`/`recv` failing is the decisive result**: it kills pipeline parallelism
|
|
||||||
as an option, because vLLM's PP stage handoff is exactly that primitive.
|
|
||||||
|
|
||||||
## Software escapes: closed, and the last one is now moot
|
|
||||||
|
|
||||||
Every row below was written under the old board. The last row — the only door
|
|
||||||
that was ever open — stopped mattering on 2026-09-20, because the hardware gate
|
|
||||||
it existed to route around no longer applies to this host.
|
|
||||||
|
|
||||||
| Idea | Finding |
|
|
||||||
|---|---|
|
|
||||||
| vLLM's IPC custom allreduce | **Gated off by a table keyed to CUDA sm versions** — `{9.0, 10.0, 10.3, 10.7}`; ROCm reports capability `12.0` for gfx1201, so the gate is `False`. `custom_ar` itself is compiled into the image. |
|
|
||||||
| ...and if that table were patched | Still insufficient: `broadcast`, `barrier`, and the TP logits `all_gather` have no non-RCCL path. |
|
|
||||||
| The radiance fork's own allreduce | Already present and *on by default* (`RADIANCE_USE_R4D_AR`, size gate `RADIANCE_AR_MAX_KB`): a one-shot P2P push allreduce wrapping `CudaCommunicator.all_reduce`, exchanging IPC handles over the **gloo** group. With the gate raised it covers prefill and decode, and `ncclDevKernel` disappears from the profile. But it does not intercept `PyNcclCommunicator.__init__`'s **warmup allreduce**, nor `pynccl_comm.all_gather` — both direct RCCL calls. |
|
|
||||||
| NCCL symmetric memory | Unavailable: the shipped RCCL advertises NCCL 2.30.4 but exposes no `ncclDevComm*` API, so torch's symmetric-memory path cannot run. |
|
|
||||||
| Prefill/decode disaggregation | Needs a KV-transfer connector plus coordination collectives; both out. |
|
|
||||||
| gloo for GPU work | CPU/TCP collectives — orders of magnitude too slow for per-layer TP. |
|
|
||||||
| **Rebuild RCCL so its kernels do not declare a hostcall buffer** | **OPEN — unverified.** The claim is that the hostcall requirement is triggered by device-side `printf`/`assert`, and that `-mprintf-kind=buffered` avoids it. Counter-evidence: RCCL's device source at `src/device/` contains **no live** `printf`, `assert`, or `__assertfail` (checked), so what makes its kernels declare `HiddenHostcallBuffer` is unexplained. The claim traces to search-result summaries, not a primary source. Cheap to settle once a host is available: recompile librccl with `-mprintf-kind=buffered`, dump a kernel's declared arguments (`roc-obj-extract` + `llvm-readelf --notes`) and look for `hidden_hostcall_buffer`; if it is gone, a 2-rank probe should dispatch. **Stop and mask the service first** — see the note below. |
|
|
||||||
|
|
||||||
## Bandwidth: why x4 is enough
|
|
||||||
|
|
||||||
A 27B TP=2 allreduce at batch 4 moves roughly
|
|
||||||
$2 \times \text{batch} \times \text{hidden} \times 2\ \text{bytes} \approx 160\ \text{KB}$
|
|
||||||
per layer per forward pass; across ~62 layers that is **≈10 MB per token-step**,
|
|
||||||
or **~1.4 ms at PCIe 4.0 x4** (~7.9 GB/s), and under 0.4 ms at the Gen5 x8
|
|
||||||
~31.5 GB/s each card now has. TP is compute-bound here, not interconnect-bound.
|
|
||||||
That is why the cheap option (the M.2 riser) and the expensive one (the board
|
|
||||||
swap) were both considered adequate on bandwidth alone — and why the board that
|
|
||||||
landed, at x8/x8, clears the arithmetic with room to spare.
|
|
||||||
|
|
||||||
## Ruled out empirically (do not re-derive)
|
|
||||||
|
|
||||||
- Kernel versions 7.0.0-28 / -30 / -31 — byte-identical failure.
|
|
||||||
- `iommu=pt`, and `amd_iommu=off` is unrelated (that was a Dankclaw prefill knob).
|
|
||||||
- P2P hardware: 45.274 GB/s bidirectional between the cards works fine — plain
|
|
||||||
memory traffic is not the problem; *atomic* routing is.
|
|
||||||
- A from-source librccl rebuild: identical failure at the identical call path.
|
|
||||||
- Every NCCL transport/algorithm knob.
|
|
||||||
- A one-card test proves nothing: NCCL short-circuits at `world_size=1`, which
|
|
||||||
is exactly why the two replicas work at all.
|
|
||||||
- Independent corroboration: StillDeadcode/vllm-radiance Codeberg issue #22 —
|
|
||||||
different host, same image, kernel 7.0.0-30, same `enqueue.cc` failure.
|
|
||||||
|
|
||||||
## Measured verdict (2026-09-20)
|
|
||||||
|
|
||||||
Both cooperation modes were run on this host on 2026-09-20 — the same salted
|
|
||||||
10-prompt battery (`scripts/bench-conversation.py`), the service stopped and
|
|
||||||
masked between arms. The arm table is [[frank-medium-radiance-mxfp4]]'s bench of
|
|
||||||
record (arms R = radiance TP=2 + DFlash2 as deployed, I = the llama.cpp layer
|
|
||||||
split that preceded it, L = llama.cpp `--split-mode tensor` against RCCL); this
|
|
||||||
page keeps only the verdict. **TP=2 won on every axis — roughly 2.1× decode and
|
|
||||||
3.6× at 4-way concurrency over the layer split — and replaced the layer split the
|
|
||||||
day of measurement.**
|
|
||||||
|
|
||||||
Arm L carries a caveat no flag can remove: llama.cpp's tensor mode could not load
|
|
||||||
the DFlash2 drafter (it aborts at `ggml-backend.cpp:941` — a draft weight
|
|
||||||
pre-allocated on the non-primary device has no backend once `n_copies == 1`), so
|
|
||||||
it decoded **without** speculation while the other arms decoded with it; and part
|
|
||||||
of R's margin over I is the 4-bit MXFP4 checkpoint, not the parallelism.
|
|
||||||
|
|
||||||
Two numbers elsewhere on this page are therefore not interchangeable:
|
|
||||||
|
|
||||||
- The 272 t/s recorded 2026-09-16 is **prefill**, on a **butterfly** allreduce,
|
|
||||||
with RCCL off, on the old board. It was never a TP decode number, and its
|
|
||||||
paired decode figure is in "The third path" below.
|
|
||||||
- The butterfly tensor arm and the RCCL tensor arm a day later are the same shape
|
|
||||||
family on different collective paths; the measured pair lives with
|
|
||||||
[[frank-medium-radiance-mxfp4]]'s arms.
|
|
||||||
|
|
||||||
RCCL needs PCIe atomics on *every* participating GPU — the gate the old board
|
|
||||||
failed. vLLM's R4D one-shot allreduce dispatches no collective at all, which is
|
|
||||||
why TP=2 runs today rather than merely "being unblocked": the image ships TheRock
|
|
||||||
RCCL, and TheRock carries no gfx1201 device code (`invalid device function`), so
|
|
||||||
radiance could not have used RCCL on this host even with atomics routed. Ubuntu's
|
|
||||||
RCCL 2.27 does carry gfx1201 kernels, and that is what arm L linked against.
|
|
||||||
|
|
||||||
**Rule for any future multi-GPU collective experiment: stop and mask the
|
|
||||||
production service first**, per the benchmarking policy, or run it on a host that
|
|
||||||
is not serving users. The production service is `frank-archon`, a **root** unit,
|
|
||||||
so on Dankserver that is `sudo systemctl stop frank-archon && sudo systemctl
|
|
||||||
mask frank-archon` (`scripts/frank-archon.service`; never `disable` — that
|
|
||||||
deletes the unit file, and `mask` is what the bench policy prescribes). The
|
|
||||||
2026-09-20 wording below — `systemctl --user stop frank-medium && systemctl
|
|
||||||
--user mask frank-medium`, frank-medium being a rootless quadlet — is retired
|
|
||||||
with that service, which today reports `not-found` (read 2026-10-18).
|
|
||||||
|
|
||||||
This is not theoretical: on 2026-09-16 a bare RCCL P2P probe was run against the live
|
|
||||||
service; the host went hard down ~20 minutes later (no ARP, no ICMP, required a
|
|
||||||
power cycle). The mechanism is not proven, but a hung AQL dispatch wedging the
|
|
||||||
GPU driver is a plausible cause and this host has precedent for going
|
|
||||||
unreachable until a power cycle.
|
|
||||||
|
|
||||||
|
|
||||||
## The third path (production in the layer-split era): llama.cpp's layer split
|
|
||||||
|
|
||||||
Everything above is about *collectives*, and vLLM cannot proceed without them
|
|
||||||
in any of its parallelisms — TP needs a per-layer allreduce, and PP's stage
|
|
||||||
handoff is a p2p `send`/`recv` of the hidden state, the primitive this host
|
|
||||||
also fails. **llama.cpp's `--split-mode layer` needs no collective at all**:
|
|
||||||
layers are distributed across the cards and the boundary activation is a
|
|
||||||
plain per-backend copy, once per forward pass. No RCCL kernel is ever launched
|
|
||||||
on the second card, so the atomics gate never comes up.
|
|
||||||
|
|
||||||
That is what `frank-medium` ran from 2026-09-16 to 09-20
|
|
||||||
([[frank-medium-llamacpp-dflash2]]): one process, one model, both cards,
|
|
||||||
262144 context. Its measured layer-split rates are that page's; this host's
|
|
||||||
tensor arm under the butterfly fallback measured 272 t/s prefill / 26.3 tok/s
|
|
||||||
decode. **That tensor number is not a prediction for an RCCL-backed build** — it
|
|
||||||
is what tensor split costs when the allreduce degrades to a meta-backend
|
|
||||||
butterfly, and it is the number the board-swap path exists to retire.
|
|
||||||
|
|
||||||
The ranking therefore changed twice. On 2026-09-16: software already gets both
|
|
||||||
cards onto one model, so the hardware fix stopped being a capability question
|
|
||||||
and became a performance one — *tensor* parallelism, ~40 % decode, plus one
|
|
||||||
pooled KV instead of two independent 122k-token pools. On 2026-09-20 the
|
|
||||||
hardware half of that question was answered by the board swap; the llama.cpp
|
|
||||||
measurement half never ran, and `GGML_HIP_RCCL` still defaults `OFF` in
|
|
||||||
`scripts/build-llama-rdna4.sh:41` (re-read 2026-10-18) for exactly the reason
|
|
||||||
this page documents — moot while no llama.cpp shape is deployed on this host.
|
|
||||||
|
|
||||||
## What would change the answer
|
|
||||||
|
|
||||||
- ~~An RCCL build whose kernels stop declaring a hostcall buffer~~ — **moot
|
|
||||||
since 2026-09-20.** That door only mattered while the hardware gate could not
|
|
||||||
be passed; the board swap passes it. The `-mprintf-kind=buffered` hypothesis
|
|
||||||
and its counter-evidence stay in the escape table for the record.
|
|
||||||
- The symmetric-kernel path inside RCCL, if a release ever ships the device API
|
|
||||||
(`ncclDevComm*`) that torch's symmetric-memory path needs. The shipped TheRock
|
|
||||||
nightly (`~/.local/share/rocm-7.14.0a20260624/lib/librccl.so.1`) still does
|
|
||||||
not.
|
|
||||||
- ~~Firmware or an AGESA update that makes the chipset bridges advertise atomic
|
|
||||||
routing~~ — arrived as new silicon instead.
|
|
||||||
- A GPU-side workaround: none found in ROCclr, and the gate is per-device,
|
|
||||||
queried once at probe.
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[Pipeline Parallelism/index|Pipeline Parallelism]] — two-machine research (the PP variant of this problem)
|
|
||||||
- [[Dankserver System Specs]] — hardware inventory
|
|
||||||
- [[Observability]] — the scrape/dashboard/alert stack around these endpoints
|
|
||||||
@@ -1,448 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: FastFlowLM NPU Inference
|
|
||||||
description: "The AMD NPU-first runtime: fit analysis for the XDNA2, published benchmarks, install state, and test runbook."
|
|
||||||
tags:
|
|
||||||
- fastflowlm
|
|
||||||
- npu
|
|
||||||
- xdna2
|
|
||||||
- dankclaw
|
|
||||||
- research
|
|
||||||
timestamp: 2026-09-03
|
|
||||||
---
|
|
||||||
|
|
||||||
> **SUPERSEDED 2026-08-17.** The Frank redo retired this topology.
|
|
||||||
> See [[frank-redo-2026-08-17]] and [[deployment]] for the active configuration.
|
|
||||||
> The `frank-zealot` name and port 52625 then belonged to the vLLM
|
|
||||||
> service on Dankclaw (nameistoken Quark W8A8); this page is archive.
|
|
||||||
>
|
|
||||||
> **The NPU endpoint is retired — 2026-09-11.** On Dankclaw today
|
|
||||||
> `frank-npu.service` is `masked`/`inactive` and nothing listens on 52625
|
|
||||||
> (`ss -ltn`, read 2026-10-18). The runtime is still installed
|
|
||||||
> (`fastflowlm 1.0.1`, `libxrt2`/`libxrt-npu2`, `/opt/fastflowlm`,
|
|
||||||
> `~/.config/flm`, `/usr/bin/flm` — all read 2026-10-18) and the device is
|
|
||||||
> usable again since the 2026-10-18 `iommu=pt` boot (`amdxdna` bound,
|
|
||||||
> `/dev/accel/accel0` present — [[dankclaw-iommu-translation-off]]), but no
|
|
||||||
> service runs on it. `frank-zealot` today is the llama.cpp PrismML endpoint on
|
|
||||||
> the RTX 3090 ([[frank-zealot]]). See [[frank-npu]]
|
|
||||||
> for the retirement record. Everything below is the 2026-08 research record.
|
|
||||||
|
|
||||||
# FastFlowLM NPU Inference
|
|
||||||
|
|
||||||
FastFlowLM (FLM) runs LLMs entirely on the AMD Ryzen AI NPU. It targets XDNA2 chips: Strix, Strix Halo, Kraken, and Gorgon Point. The project describes itself as "Ollama, but deeply optimized for NPUs." As of 2026-08-16, FLM was installed on Dankclaw, the NPU validated, and `frank-zealot` (port 52625) served in production — that arrangement ended with the 2026-09-11 retirement (see the banner above). See Promotion.
|
|
||||||
|
|
||||||
## Verdict (2026-08-16)
|
|
||||||
|
|
||||||
- The NPU is a complement, not a replacement. It fits dense models up to about 9B.
|
|
||||||
- Qwen3.8-27B (dense) cannot run on the NPU. No FLM build exists and the memory budget is too small.
|
|
||||||
- Qwen3.6-35B-A3B has an official FLM build (Q4_KS). AMD benchmarks it on a 96 GB machine. It probably does not fit the 30 GiB budget of Dankclaw.
|
|
||||||
- Realistic first candidates: Qwen3.5-9B, Qwen3-8B, gpt-oss-20b (at the budget edge).
|
|
||||||
- **Landed 2026-08-16:** gpt-oss-20b serves on the NPU at 19.3 t/s decode, concurrently with the PP head. See Measured Results.
|
|
||||||
- The memlock limit was the setup blocker. Fixed 2026-08-16 via `/etc/security/limits.d/99-flm-memlock.conf`.
|
|
||||||
|
|
||||||
## What FastFlowLM Is
|
|
||||||
|
|
||||||
FLM is an NPU-first inference runtime. It started as a 2025 university project (University of Rhode Island, Ken Qing Yang). The team joined AMD on 2026-07-17. The repository moved to the ROCm organization on 2026-08-11 as v1.0.0.
|
|
||||||
|
|
||||||
Key properties, per the project README and docs:
|
|
||||||
|
|
||||||
- Runs fully on the NPU. No GPU or CPU load during inference.
|
|
||||||
- CLI mode (`flm run <tag>`) and server mode (`flm serve <tag>`).
|
|
||||||
- Server mode exposes a REST API and an OpenAI-compatible API. Default port is 52625.
|
|
||||||
- Models are not GGUF. FLM downloads pre-compiled kernel packs (built with AMD IRON + AIE-MLIR) from Hugging Face.
|
|
||||||
- Context lengths up to 256k tokens on listed models.
|
|
||||||
- Vision, audio, embedding, and MoE model support.
|
|
||||||
- Runtime is about 17 MB. Orchestration code is MIT-licensed. Kernels are free for commercial use.
|
|
||||||
- Models store in `~/.config/flm/` (Linux). Override with `FLM_MODEL_PATH`.
|
|
||||||
|
|
||||||
## NPU Hardware on Dankclaw
|
|
||||||
|
|
||||||
Dankclaw runs an AMD Ryzen AI MAX+ 395 (Strix Halo). The chip integrates an XDNA2 NPU
|
|
||||||
with 50 TOPS of compute and, crucially, **no dedicated VRAM** - it shares system memory
|
|
||||||
via the IOMMU SVA mapping. The budget this page reasons about - 29 GiB usable at
|
|
||||||
4800 MT/s LPDDR5X - is the 2026-08 host state; the live figure is [[Dankclaw System Specs]].
|
|
||||||
|
|
||||||
Live verification on Dankclaw, 2026-08-16:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ flm validate
|
|
||||||
[FLM] Fetching models from: /opt/fastflowlm/share/flm/model_list.json
|
|
||||||
[Linux] Kernel: 7.0.0-29-generic
|
|
||||||
[Linux] NPU: /dev/accel/accel0 with 8 columns
|
|
||||||
[Linux] NPU FW Version: 1.1.2.65
|
|
||||||
[Linux] amdxdna version: 0.7
|
|
||||||
[ERROR] Memlock limit is too low (8MB). Please raise the limit or set to infinity.
|
|
||||||
```
|
|
||||||
|
|
||||||
- Kernel 7.0.0-29-generic shipped the `amdxdna` driver in-tree, loaded. The host now runs 7.0.0-38-generic and the driver is still bound (`lspci -k -s c6:00.1` → `Kernel driver in use: amdxdna`, read 2026-10-18).
|
|
||||||
- Firmware 1.1.2.65 meets the 1.1.0.0 minimum.
|
|
||||||
- The XRT runtime packages (`libxrt2`, `libxrt-npu2`) are installed — still installed on 2026-10-18 (`dpkg -l` → `libxrt2 1:2.21.75+dfsg-4`, `libxrt-npu2 1:2.21.75+dfsg-4`).
|
|
||||||
- The only failure is the memlock ulimit (8 MB). FLM needs `unlimited`.
|
|
||||||
- Ubuntu 26.04 is not on the FLM supported-distro list (24.04, 25.10, Arch, generic). Validation passes anyway. Treat 26.04 as "generic Linux, working".
|
|
||||||
|
|
||||||
## Memory Model
|
|
||||||
|
|
||||||
The NPU allocates from system RAM. AMD/Microsoft enforce an internal cap of about half of total system RAM for NPU use. The gpt-oss model card states a cap of about 15.6 GB on a 32 GB machine, which is half.
|
|
||||||
|
|
||||||
Budget math for this fleet:
|
|
||||||
|
|
||||||
| Machine | RAM | NPU budget (approx. half) |
|
|
||||||
|---------|-----|---------------------------|
|
|
||||||
| Dankclaw (Strix Halo) | 30 GiB | ~15 GiB |
|
|
||||||
| Dankserver (Ryzen 7 7700X) | 62 GB | no NPU |
|
|
||||||
| FLM bench box (Kraken Point) | 32 or 96 GB | ~16 or ~48 GB |
|
|
||||||
|
|
||||||
The half-of-RAM gate scales with host RAM - live figures in [[Dankclaw System Specs]]
|
|
||||||
and [[Dankserver System Specs]]. The live gate on Dankclaw is still `flm validate`
|
|
||||||
(it owns the current number).
|
|
||||||
|
|
||||||
Weight sizes (approximate, Q4-class):
|
|
||||||
|
|
||||||
| Model | Quant | Weights | Fits ~15 GiB budget? |
|
|
||||||
|-------|-------|---------|----------------------|
|
|
||||||
| Qwen3.5-9B | Q4_1 | ~5.5 GB | yes |
|
|
||||||
| Qwen3-8B | Q4_1 | ~5 GB | yes |
|
|
||||||
| gpt-oss-20b (MoE, 3.6B active) | Q4_1 | ~15.1 GB | at the edge |
|
|
||||||
| Qwen3.6-35B-A3B (MoE, 3B active) | Q4_KS | ~20 GB | no, unproven at 30 GiB |
|
|
||||||
| Qwen3.8-27B (dense) | any Q4 | ~16 GB+ | no build exists |
|
|
||||||
|
|
||||||
Do not load a model larger than the budget first. An oversized load can starve the unified pool and crash the box while production services still run.
|
|
||||||
|
|
||||||
## Supported Models (Qwen family, plus gpt-oss)
|
|
||||||
|
|
||||||
All Qwen cards use Q4_1 unless noted. Context lengths are the FLM-published maxima.
|
|
||||||
|
|
||||||
| Tag | Base model | Context max | Notes |
|
|
||||||
|-----|-----------|-------------|-------|
|
|
||||||
| `qwen3:0.6b` | Qwen3-0.6B | 32k | thinking toggle |
|
|
||||||
| `qwen3:1.7b` | Qwen3-1.7B | 32k | thinking toggle |
|
|
||||||
| `qwen3:4b` | Qwen3-4B | 32k | tool calling |
|
|
||||||
| `qwen3:8b` | Qwen3-8B | 32k | tool calling |
|
|
||||||
| `qwen3-tk:4b` | Qwen3-4B-Thinking-2507 | 256k | thinking |
|
|
||||||
| `qwen3-it:4b` | Qwen3-4B-Instruct-2507 | 256k | tool calling |
|
|
||||||
| `qwen3vl-it:4b` | Qwen3-VL-4B-Instruct | 256k | vision |
|
|
||||||
| `qwen2.5-it:3b` | Qwen2.5-3B-Instruct | 32k | |
|
|
||||||
| `qwen2.5vl-it:3b` | Qwen2.5-VL-3B-Instruct | 256k | vision |
|
|
||||||
| `qwen3.5:0.8b` | Qwen3.5-0.8B | 256k | vision, thinking toggle |
|
|
||||||
| `qwen3.5:2b` | Qwen3.5-2B | 256k | vision, tool calling |
|
|
||||||
| `qwen3.5:4b` | Qwen3.5-4B | 256k | vision, tool calling |
|
|
||||||
| `qwen3.5:9b` | Qwen3.5-9B | 256k | vision, tool calling |
|
|
||||||
| `qwen3.6-moe:35b-a3b` | Qwen3.6-35B-A3B | 256k | **Q4_KS**, MoE, vision, thinking, tool calling |
|
|
||||||
| `gpt-oss:20b` | gpt-oss-20b | 128k | Q4_1, ~15.1 GB in FLM |
|
|
||||||
|
|
||||||
Other families: LLaMA, DeepSeek, Gemma, MedGemma, TranslateGemma, LiquidAI/LFM, Phi, Nanbeige, Whisper, EmbeddingGemma, SmolVLA.
|
|
||||||
|
|
||||||
## Published Benchmarks (FLM, vendor numbers)
|
|
||||||
|
|
||||||
All vendor results come from AMD Ryzen AI 7 350 (Kraken Point, 40 TOPS NPU) boxes under the default NPU power mode (Performance). Our NPU is a 50 TOPS Strix Halo XDNA2. Treat these numbers as directional, not as our expected speed.
|
|
||||||
|
|
||||||
Qwen3.5, FLM v0.9.38, Kraken Point 32 GB DRAM. Decode t/s by starting context length:
|
|
||||||
|
|
||||||
| Model | 1k | 2k | 4k | 8k | 16k | 32k |
|
|
||||||
|-------|-----:|-----:|-----:|-----:|-----:|-----:|
|
|
||||||
| Qwen3.5-0.8B | 39.2 | 38.0 | 36.3 | 33.1 | 28.1 | 21.6 |
|
|
||||||
| Qwen3.5-2B | 26.8 | 26.2 | 25.4 | 23.7 | 21.3 | 17.0 |
|
|
||||||
| Qwen3.5-4B | 15.0 | 14.6 | 14.2 | 13.3 | 11.8 | 9.6 |
|
|
||||||
| Qwen3.5-9B | 9.3 | 9.2 | 9.0 | 8.5 | 7.8 | 6.9 |
|
|
||||||
|
|
||||||
Prefill t/s for the same run:
|
|
||||||
|
|
||||||
| Model | 1k | 2k | 4k | 8k | 16k | 32k |
|
|
||||||
|-------|-----:|-----:|-----:|-----:|-----:|-----:|
|
|
||||||
| Qwen3.5-0.8B | 983 | 1257 | 1471 | 1584 | 1579 | 1447 |
|
|
||||||
| Qwen3.5-2B | 803 | 1004 | 1142 | 1223 | 1225 | 1151 |
|
|
||||||
| Qwen3.5-4B | 378 | 440 | 479 | 493 | 487 | 450 |
|
|
||||||
| Qwen3.5-9B | 284 | 333 | 362 | 379 | 378 | 357 |
|
|
||||||
|
|
||||||
Qwen3.6-35B-A3B (Q4_KS), FLM v0.9.45, Kraken Point **96 GB DRAM**:
|
|
||||||
|
|
||||||
| Metric | 1k | 2k | 4k | 8k | 16k | 32k |
|
|
||||||
|--------|-----:|-----:|-----:|-----:|-----:|-----:|
|
|
||||||
| Decode t/s | 13.65 | 13.41 | 13.09 | 12.51 | 11.24 | 9.51 |
|
|
||||||
| Prefill t/s | 78.98 | 118.04 | 156.43 | 197.93 | 218.84 | 221.96 |
|
|
||||||
|
|
||||||
- PP and NPU together cost 2.2 GiB + NPU model + ~2 GiB of headroom. The then-current
|
|
||||||
PP split (head + 3090 worker, 2026-08 era - see [[frank-redo-2026-08-17]]) decoded at
|
|
||||||
14-18 t/s for the 27B dense model, so an NPU offload would free the head or upgrade
|
|
||||||
the worker's model.
|
|
||||||
|
|
||||||
The vendor also claims "over 10x more power-efficient" versus GPU. That is a marketing claim. We have not measured it.
|
|
||||||
|
|
||||||
## Fit to the Frank Program
|
|
||||||
|
|
||||||
| Our model | NPU verdict |
|
|
||||||
|-----------|-------------|
|
|
||||||
| Qwen3.8-27B UD-Q8_K_XL (Dankclaw, dense) | No. No FLM build. ~16 GB+ even at Q4. Exceeds the ~15 GiB budget. |
|
|
||||||
| Qwen3.6-35B-A3B (Dankserver, MoE) | Unproven. Official build exists. Vendor bench ran on 96 GB RAM. A 30 GiB box has ~15 GiB for the NPU. Test last, with the service stopped. |
|
|
||||||
| Qwen3.5-9B / Qwen3-8B | Yes, the realistic tier. ~5-6 GB. Leaves room for KV and the OS. |
|
|
||||||
| gpt-oss-20b (MoE) | **Landed.** Weights are 13.8 GB on disk (q4nx). The model holds ~15 GiB resident on the NPU. Decode is 19.3 t/s. See Measured Results. |
|
|
||||||
|
|
||||||
Proposed role: a small, always-on, low-power endpoint for triage, summarization, embeddings, or agent side-tasks on the Dankclaw NPU. It frees the iGPU and the 3090 for the 27B/35B workloads. It does not replace either.
|
|
||||||
|
|
||||||
## Install State on Dankclaw (verified 2026-08-16)
|
|
||||||
|
|
||||||
- `fastflowlm 1.0.1` installed (deb, upgraded from 0.9.45-12-ga6bd056 on 2026-08-17). `flm` at `/usr/bin/flm`. The 0.9.45 runtime refused the qwythos pack, which declares `flm_version` 1.0.0.
|
|
||||||
- `libxrt2` / `libxrt-npu2` (Xilinx Runtime) installed.
|
|
||||||
- `/dev/accel/accel0` present. `flm validate` passed fully. (Re-read 2026-10-18: `/dev/accel/accel0` is still present and `fastflowlm 1.0.1` is still installed, but the endpoint is retired — `frank-npu.service` masked, no listener on 52625.)
|
|
||||||
- Memlock fixed: `/etc/security/limits.d/99-flm-memlock.conf` sets soft and hard memlock to unlimited. A fresh sshd session picks it up via pam_limits. No reboot needed.
|
|
||||||
- `gpt-oss:20b` pulled (4/4 files verified, 13.8 GB `model.q4nx` + tokenizer) into `~/.config/flm/models/GPT-OSS-20B-NPU2/`.
|
|
||||||
- `qwythos:9b` (Huihui-Qwythos-9B-Claude-Mythos-5-1M-abliterated-NPU2) installed 2026-08-17 via `flm-add.py` (downloaded from the HF repo; stdlib-only, no pip needed). Tag registered in the user registry `~/.config/flm/model_list.json` (a copy of the v1.0.1 system registry + the custom entry); kernels symlinked `~/.config/flm/xclbins/Huihui-...-NPU2 -> /opt/fastflowlm/share/flm/xclbins/Qwen3.5-9B-NPU2`. The service env exports `FLM_CONFIG_PATH` and `FLM_XCLBIN_PATH` so the tag resolves (without them flm hits the unknown-tag fallback, see the incident below).
|
|
||||||
- `flm serve gpt-oss:20b --host 0.0.0.0 -p 52625 --pmode performance` runs as a nohup process. It is not a systemd unit. It does not survive a reboot.
|
|
||||||
- The `llama-server` (frank-dragoon, the PP head) holds an fd on `/dev/accel/accel0` from its own startup (device enumeration, not compute). FLM opened the same device concurrently without conflict. Verified live 2026-08-16.
|
|
||||||
|
|
||||||
## Measured Results (2026-08-16, gpt-oss-20b on the Dankclaw NPU)
|
|
||||||
|
|
||||||
Conditions: `--pmode performance`, PP head (27B, port 7770) serving concurrently, 5.5 GiB system RAM headroom steady-state, NPU resident footprint ~15 GiB.
|
|
||||||
|
|
||||||
| Run | Prompt tok | Compl tok | Wall s | Prefill t/s | Decode t/s | KV occ % |
|
|
||||||
|-----|-----------:|----------:|-------:|------------:|-----------:|---------:|
|
|
||||||
| 1 (smoke) | 74 | 63 | 7.2 | 19.5 | 19.3 | 1.7 |
|
|
||||||
| 2 (160-tok prompt) | 161 | 256 | 17.4 | 40.8 | 19.3 | 5.1 |
|
|
||||||
| 3 (95-tok prompt, 512 out) | 95 | 512 | 30.8 | 24.3 | 19.3 | n/a |
|
|
||||||
|
|
||||||
Decode is flat at 19.3 t/s across all three runs. Output includes `reasoning_content` (gpt-oss is a reasoning model). At `max_tokens 256` one run spent its whole budget thinking and returned empty content. Give it 500+ tokens or strip reasoning in the client.
|
|
||||||
|
|
||||||
Long-context prefill is not yet measured (runs 1-3 used under 200 prompt tokens). The 35B-A3B step remains untested.
|
|
||||||
|
|
||||||
## Model Bakeoff (measured 2026-08-16, 4 models)
|
|
||||||
|
|
||||||
All runs on the Dankclaw NPU (50 TOPS Strix Halo), single stream, `--pmode performance`, PP head (frank-dragoon) serving concurrently on 7770. Decode t/s is the FLM-reported rate. Vision accuracy = ground-truth elements out of 4 (text SPECTRUM, red circle left, blue square right, three green triangles bottom).
|
|
||||||
|
|
||||||
| Model | Tag | Weights | Decode t/s (short) | 8k prefill t/s | 8k decode t/s | Vision decode t/s | Vision accuracy | Max ctx (registry) |
|
|
||||||
|-------|-----|---------|-------------------:|---------------:|--------------:|------------------:|----------------|--------------------|
|
|
||||||
| Gemma4-E4B | `gemma4-it:e4b` | 6.8 GB + 912 MB audio | 12.2 | 582 | 9.9 | 11.7 | 4/4 | 32768 |
|
|
||||||
| Qwen3.5-9B | `qwen3.5:9b` | 7.3 GB | 7.2 | 343 | 6.6 | 7.3 | 4/4 | 32768 |
|
|
||||||
| GPT-OSS-20B | `gpt-oss:20b` | 13.8 GB | 19.3 | 378 | 16.9 | n/a (no vision) | 0/4 | **8192** |
|
|
||||||
| Qwen3.6-35B-A3B | `qwen3.6-moe:35b-a3b` | 22.2 GB | 12.7 | 207 | 11.2 | 12.7 | 4/4 | 32768 |
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- gpt-oss:20b is the fastest decoder (MoE, 3.6B active) but its KV cache fills at ~8k tokens (98.85% occupancy at 7,926; requests past ~8k return 400). It has no vision encoder.
|
|
||||||
- 35B-A3B loads 28-29 GiB resident. It works but leaves ~1.5 GiB headroom with the PP head running. Do not run it alongside the head at full load.
|
|
||||||
- Vision prefill for the two vision models with ~720 token image input: gemma 141 t/s, qwen3.5 176 t/s, 35B-A3B 68 t/s.
|
|
||||||
- All four comply with the same image at 4/4 elements except gpt-oss, which cannot see it.
|
|
||||||
|
|
||||||
## Promotion (2026-08-16)
|
|
||||||
|
|
||||||
`frank-zealot` then ran the NPU endpoint on Dankclaw (2026-08; the name was reused
|
|
||||||
twice after this window - see the banner - and the NPU unit today is `frank-npu`,
|
|
||||||
masked). Unit: `scripts/frank-zealot.service` (symlinked into `/etc/systemd/system`),
|
|
||||||
wrapper `scripts/serve-flm-small.sh`, tunables `scripts/frank-zealot.env`.
|
|
||||||
`LimitMEMLOCK=infinity` is set in the unit. Default model: `gemma4-it:e4b`. Swap
|
|
||||||
`FLM_MODEL` in the env to switch.
|
|
||||||
|
|
||||||
### Boot policy (2026-08-17)
|
|
||||||
|
|
||||||
The `frank-zealot` unit is disabled for boot. Only the PP pair (`frank-dragoon` + `frank-pp-worker`) starts at boot. The unit file was removed from `/etc/systemd/system`; the running FLM service keeps serving until stopped or rebooted. To restore the endpoint, run these commands on Dankclaw:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo ln -sf /home/dank/vllmScripts/scripts/frank-zealot.service /etc/systemd/system/frank-zealot.service
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable frank-zealot
|
|
||||||
```
|
|
||||||
|
|
||||||
Live config (2026-08-16): `FLM_PMODE=turbo`, `FLM_ASR=1` (loads Whisper-V3-Turbo-NPU2; exposes `/v1/audio/transcriptions`), `FLM_CTX_LEN=131072` (verified max — Gemma4 natively supports 128K and the NPU budget fits it; KV occupancy measured at 0.11% for a 123-token exchange, decode 12.0 t/s, 13 GiB headroom).
|
|
||||||
|
|
||||||
The PP split was renamed `frank-pp-head` → `frank-dragoon` (2026-08-16, commit `48142f5`).
|
|
||||||
|
|
||||||
Model swap (2026-08-16, commit `50c55ee`): `FLM_MODEL=qwen3.6-moe:35b-a3b`, `FLM_CTX_LEN=262144` (native max), `FLM_PMODE=turbo`, `FLM_ASR=1` kept. Decode measured 12.5-13.1 t/s. The Gemma4 live config above (131072 ctx) applied to `gemma4-it:e4b`. Swap `FLM_MODEL` and `FLM_CTX_LEN` back to restore it.
|
|
||||||
|
|
||||||
Model swap (2026-08-17, commit `c85ff6c`): `FLM_MODEL=qwythos:9b` (Huihui-Qwythos-9B-Claude-Mythos-5-1M-abliterated-NPU2, Q4NX 7.76 GB). The pack installs through `flm-add` (user registry `~/.config/flm/model_list.json` + user xclbins `~/.config/flm/xclbins`, kernels symlinked from official `Qwen3.5-9B-NPU2`). Runtime upgraded to FLM 1.0.1 first (see Install State). `FLM_CTX_LEN=262144` native max (Qwen3.5-9B hybrid: 8 of 32 layers full attention, so KV stays small; occupancy 0.008% at 21 tokens). `FLM_PMODE=turbo`, `FLM_ASR=1` kept. The env file now exports `FLM_CONFIG_PATH` / `FLM_XCLBIN_PATH` — required, or flm treats the tag as unknown and silently serves llama3.2:1b. `qwen3.6-moe:35b-a3b` files stay installed for rollback. pi client entry (`~/.pi/agent/models.json` on dankware): id `qwythos:9b`, ctx 262144, input `[text]` (pack has no vision weights), compat `supportsDeveloperRole: false` kept.
|
|
||||||
|
|
||||||
## Parallel Streams (measured 2026-08-16)
|
|
||||||
|
|
||||||
The NPU serializes generations. It runs exactly one stream at a time. Extra streams queue in FIFO order. FLM server flags `--socket` (default 10) and `--q-len` (default 10) cap queue depth only. They do not add concurrency.
|
|
||||||
|
|
||||||
Concurrent-request test, gpt-oss-20b, 128-token cap per stream:
|
|
||||||
|
|
||||||
| Streams | Wall s | Aggregate t/s | Per-stream wall s |
|
|
||||||
|--------:|-------:|--------------:|-------------------|
|
|
||||||
| 1 | 10.6 | 12.1 | 10.6 |
|
|
||||||
| 2 | 21.1 | 12.0 | 10.8 / 21.1 |
|
|
||||||
| 4 | 41.1 | 11.6 | 10.9 / 21.6 / 32.4 / 41.1 |
|
|
||||||
|
|
||||||
The 4-stream wall times form a near-perfect 10.7 s ladder. Each stream self-reports 19.3 t/s decode, counting only its own active slices. Trust per-stream wall time, not the self-reported speed, under concurrency.
|
|
||||||
|
|
||||||
Consequence: the NPU endpoint is a single lane. It suits low-frequency background work (triage, summarization, agent side-tasks). It does not suit interactive chat for several simultaneous users. The 27B llama.cpp service, by contrast, runs PARALLEL=2 with true concurrent slots.
|
|
||||||
|
|
||||||
## Test Runbook
|
|
||||||
This runbook targets a retired endpoint; keep it as the procedure if the NPU is
|
|
||||||
ever revived.
|
|
||||||
|
|
||||||
**Host RAM availability.** The gate is half of system RAM by default - a relic of the
|
|
||||||
install: the endpoint ran on a machine that presented 30 GiB of system RAM; on today's
|
|
||||||
box that ceiling is far larger (live figure: [[Dankclaw System Specs]]). Check free RAM
|
|
||||||
first, or stop
|
|
||||||
the service the way the bench policy requires (`sudo systemctl stop
|
|
||||||
frank-dragoon && sudo systemctl mask frank-dragoon` — self-healing would
|
|
||||||
otherwise restart it). The NPU shares the unified pool with everything else on
|
|
||||||
the box.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[1. Raise memlock to unlimited] --> B[2. flm validate + xrt-smi examine]
|
|
||||||
B --> C{NPU visible to XRT?}
|
|
||||||
C -- no --> D[Fix driver or firmware path]
|
|
||||||
C -- yes --> E[3. flm run qwen3.5:4b smoke test, /verbose]
|
|
||||||
E --> F{Generates tokens?}
|
|
||||||
F -- no --> D
|
|
||||||
F -- yes --> G[4. flm serve on a scratch port, never 7770]
|
|
||||||
G --> H[5. Bench decode + prefill t/s]
|
|
||||||
H --> I{Speed acceptable?}
|
|
||||||
I -- no --> J[Record results, stop]
|
|
||||||
I -- yes --> K[6. Attempt gpt-oss:20b]
|
|
||||||
K --> L{Loads in ~15 GiB?}
|
|
||||||
L -- no --> J
|
|
||||||
L -- yes --> M[Bench, record]
|
|
||||||
M --> N[7. Last: qwen3.6-moe:35b-a3b, service stopped]
|
|
||||||
N --> O[Record, decision]
|
|
||||||
```
|
|
||||||
|
|
||||||
1. Raise memlock. Add `* soft memlock unlimited` and `* hard memlock unlimited` to `/etc/security/limits.conf`. Then reboot. For a systemd unit, set `LimitMEMLOCK=infinity` instead.
|
|
||||||
2. Verify the NPU. Run `flm validate`. Run `xrt-smi examine`. Both must show the NPU. `flm validate` uses the kernel device. `flm run` uses XRT. A passing validate does not guarantee XRT sees the device.
|
|
||||||
3. Smoke test. Run `flm run qwen3.5:4b`. Toggle `/verbose` for performance reporting. Type `/bye` to exit.
|
|
||||||
4. Server mode. Run `flm serve qwen3.5:4b`. The default port is 52625. Use a scratch port if 52625 is taken. Never 7770.
|
|
||||||
5. Bench. Measure decode and prefill t/s at 1k/4k/16k starting context. Record to this page.
|
|
||||||
6. Step up in size: 9B, then gpt-oss-20b.
|
|
||||||
7. Attempt `qwen3.6-moe:35b-a3b` only after the smaller runs succeed, and only with the production service stopped. Watch `free -h` during the model download and load.
|
|
||||||
|
|
||||||
## Registering packs absent from the catalog (NPU2)
|
|
||||||
|
|
||||||
Use when adding a non-catalog NPU2 pack (an extraction or fine-tuned model)
|
|
||||||
to the FLM registry. Registry facts: user registry
|
|
||||||
`/home/dank/.config/flm/model_list.json`, shape
|
|
||||||
`{"model_path": "models", "models": {"<tag>": {"<size>": {name, url,
|
|
||||||
file_url, size, flm_min_version, files[], vlm, default_context_length,
|
|
||||||
max_prefill_len, details{format:"NPU2", family, …}, label[]}}}}`; serve
|
|
||||||
resolves tags through `FLM_CONFIG_PATH` at startup, and `flm list` with the
|
|
||||||
env var set lists custom tags (`⏬` = not downloaded).
|
|
||||||
|
|
||||||
1. Back up the registry JSON first (`cp -a`).
|
|
||||||
2. Insert the entry via a python json edit + atomic `os.replace`. Set
|
|
||||||
`files` to the FULL HF file list — **including `vision_weight.q4nx` even
|
|
||||||
for text-only packs**: the qwen3.5-family loader opens it unconditionally
|
|
||||||
and a missing file makes the first request HANG silently (no error log).
|
|
||||||
3. **Do not use `flm pull <tag>` for custom entries** — it throws
|
|
||||||
`json.exception out_of_range key '<tag>:<size>' not found` while building
|
|
||||||
the download list. Place files manually into
|
|
||||||
`/home/dank/.config/flm/models/<PackName>/` with
|
|
||||||
`curl -sfL "https://huggingface.co/<repo>/resolve/main/<file>?download=true"`
|
|
||||||
— plain `raw/main/<file>` returns 135-byte LFS pointer stubs, not
|
|
||||||
weights; verify byte counts against the HF tree API before trusting a
|
|
||||||
download.
|
|
||||||
4. Kernel packs: copy from the same-shape family pack (shape-match
|
|
||||||
`config.json` on hidden_size/num_hidden_layers/head_dim first) into
|
|
||||||
`/opt/fastflowlm/share/flm/xclbins/<PackName>/` via `sudo cp -a`
|
|
||||||
(root-owned dir).
|
|
||||||
5. Deploy/restart the serve unit (the deploy path, not ad-hoc restarts) so
|
|
||||||
the new process loads with all files present.
|
|
||||||
6. Verify with a REAL completion request (`model: "<tag>:<size>"`) —
|
|
||||||
`/v1/models` alone proves registration, not loadability.
|
|
||||||
|
|
||||||
Hard constraints: only ONE `flm serve` process can hold the NPU hardware
|
|
||||||
context — a second concurrent serve process fails every load with
|
|
||||||
`DRM_IOCTL_AMDXDNA_CREATE_HWCTX err=-22` (aie2 mgmt msg opcode 0x2 status
|
|
||||||
0x2000003); never run scratch/verify instances while production holds the
|
|
||||||
NPU. Tag switching happens INSIDE one serve process; the first request
|
|
||||||
after a model switch pays ~6–11 s cold-load cost (visible in
|
|
||||||
`usage.load_duration`). Over ssh, never `pkill -f "<pattern>"` with a
|
|
||||||
pattern present in your own command line — use bracket-guarded patterns
|
|
||||||
(`[q]wen3.5:4b`) or exact PIDs.
|
|
||||||
|
|
||||||
## Tag load-failure diagnosis (200 with an error body)
|
|
||||||
|
|
||||||
`POST /v1/chat/completions` answers **HTTP 200** with a flat
|
|
||||||
`{"error":"Failed to load <tag> model!"}` and no `choices`; `/v1/completions`
|
|
||||||
returns a nested OpenAI-shaped `server_error` (code 500) also under HTTP
|
|
||||||
200. A status-code-only client reads this as a successful empty completion —
|
|
||||||
**always check for the `error` key**. Two distinct causes, distinguished via
|
|
||||||
`journalctl -u <flm-service> --full`:
|
|
||||||
|
|
||||||
1. **Unknown tag** (not in the registry): immediate error, nothing loaded.
|
|
||||||
Correct behavior, not a bug.
|
|
||||||
2. **Registered tag whose kernel pack is missing**: FLM resolves the tag,
|
|
||||||
downloads missing weight files on the fly (first request hangs ~90–110 s),
|
|
||||||
then fails `Failed to load model: No such file
|
|
||||||
'/home/dank/.config/flm/xclbins/<PackName>/<kernel>.xclbin'`; subsequent
|
|
||||||
attempts fail fast (~2 s). Root cause: the serve script exports
|
|
||||||
`FLM_XCLBIN_PATH=/home/dank/.config/flm/xclbins` (user dir), so every
|
|
||||||
kernel lookup resolves under the user dir, which only contains symlinks
|
|
||||||
someone linked. **A tag listed in `/v1/models` is NOT proof it is
|
|
||||||
loadable on this host.** Fix: one symlink per missing pack
|
|
||||||
(`ln -s /opt/fastflowlm/share/flm/xclbins/<PackName>
|
|
||||||
/home/dank/.config/flm/xclbins/<PackName>`) — verified live: after the
|
|
||||||
symlink the first request loaded and answered in 7.4 s wall.
|
|
||||||
|
|
||||||
Switch-cost behavior: the `model` field routes per-request (no restart
|
|
||||||
needed); a request whose model differs from the resident one triggers a full
|
|
||||||
reload (7–11 s wall for an 8-token completion on ~4B-class packs);
|
|
||||||
alternating tags pays the swap each time. Single-lane NPU: concurrent
|
|
||||||
requests queue FIFO behind the current lock holder (`NPU Locked!` →
|
|
||||||
`NPU Lock Released!`). Probe recipe: `/v1/models` for registered tags; grep
|
|
||||||
the journal for `Failed to load model` (the quoted xclbin path names the
|
|
||||||
missing pack dir exactly); diff `ls ~/.config/flm/xclbins/` (symlinks =
|
|
||||||
installed) against `/opt/fastflowlm/share/flm/xclbins/` (available).
|
|
||||||
|
|
||||||
## Incident: qwen3.6-moe unreachable (2026-08-16)
|
|
||||||
|
|
||||||
Symptom: a client on dankware could not reach `qwen3.6-moe:35b-a3b` on port 52625. The journal shows three separate causes in the 14:19-15:03 window.
|
|
||||||
|
|
||||||
1. **EXEC 203 crash loop (14:19-14:20).** The exec bit was lost on `serve-flm-small.sh` on the target. The unit failed `status=203/EXEC: Permission denied` four times, then started after the bit was restored. The git mode is `100755` (commit `3f42210`), and a `git pull` restores the bit. Verify with `git ls-files -s scripts/serve-flm-small.sh`.
|
|
||||||
2. **Wrong model ID (14:29-14:52).** The client config still used the model ID `frank-zealot` (a service alias). FLM rejects that tag with `[ERROR] Model tag 'frank-zealot' is not supported.` Each such request also triggered the unknown-tag fallback below.
|
|
||||||
3. **Swap churn (14:48-15:03).** The swap `gemma4-it:e4b` → `qwen3.6-moe:35b-a3b` restarted the service several times. Each load of the 23 GB model takes about 1.5-2.5 minutes at 262144 ctx. The endpoint does not serve during a load.
|
|
||||||
|
|
||||||
Fix: the client model ID must be the FLM registry tag `qwen3.6-moe:35b-a3b`. The working pi agent entry in `~/.pi/agent/models.json` is `baseUrl http://192.168.0.143:52625/v1`, `id qwen3.6-moe:35b-a3b`, `contextWindow 262144`, `reasoning true`, `input [text, image]`. Verified end-to-end from dankware (192.168.0.226) at 15:32 on 2026-08-16.
|
|
||||||
|
|
||||||
### Unknown-tag fallback (FLM behavior)
|
|
||||||
|
|
||||||
When a request names a tag that is not in the FLM registry, FLM does not return a clean 404. It logs the ERROR, then falls back to the default model `llama3.2:1b`:
|
|
||||||
|
|
||||||
- If the `llama3.2:1b` files are missing, FLM downloads them (1.2 GB on 2026-08-16) while the request hangs.
|
|
||||||
- FLM then loads the 1B model in place of the requested model.
|
|
||||||
- The files now exist in `~/.config/flm/models/Llama-3.2-1B-NPU2` (1.3 GB) on dankclaw. A future unknown-tag request loads the 1B model silently, with no download and no error.
|
|
||||||
|
|
||||||
**Correction (live-probed 2026-08-30):** the fallback no longer fires on the
|
|
||||||
current serve path — an unknown tag now answers HTTP 200 with a flat
|
|
||||||
`{"error":"Failed to load <tag> model!"}` body and nothing is loaded or served;
|
|
||||||
`/v1/completions` answers a nested `server_error` (code 500) under HTTP 200.
|
|
||||||
|
|
||||||
Keep client model IDs as FLM registry tags. Never send a service alias. Optional cleanup: move `~/.config/flm/models/Llama-3.2-1B-NPU2` (an incident artifact) to `.AGENTTRASH` so a stray tag re-downloads loudly instead of serving the wrong model.
|
|
||||||
|
|
||||||
### Request compatibility: the `developer` role (found 2026-08-16)
|
|
||||||
|
|
||||||
pi sessions failed against this endpoint with `Stream ended without finish_reason` while curl worked. Root cause, verified by field bisection:
|
|
||||||
|
|
||||||
- The FLM Qwen chat template accepts only the roles `system`, `user`, and `assistant`. Any other role raises `Unexpected message role.` in the template.
|
|
||||||
- FLM reports the error differently per mode. Non-streaming: HTTP 500 with the template traceback. Streaming: HTTP 200, and the error JSON is the first body. A streaming client sees a stream with no chunks and no `finish_reason`.
|
|
||||||
- pi sends `role: "developer"` when `model.reasoning && compat.supportsDeveloperRole` (pi source, `openai-completions.js`). The default `supportsDeveloperRole` is true for standard OpenAI-compatible providers, so a reasoning model without a `compat` block always triggers the bug.
|
|
||||||
- Fix: `"compat": { "supportsDeveloperRole": false }` on the model entry in `~/.pi/agent/models.json`. pi then sends `role: "system"`. Verified with a headless pi session: full round trip, `finish_reason: stop`.
|
|
||||||
- FLM ignores unknown body fields. `reasoning_effort`, `store`, `max_completion_tokens`, and `tools: []` all arrive in pi requests and pass through without error.
|
|
||||||
|
|
||||||
## Risks and Open Questions
|
|
||||||
|
|
||||||
- **Ubuntu 26.04 is unofficial.** The distro list stops at 25.10. Validation passes, but breakage would fall on the "generic Linux" path.
|
|
||||||
- **The 30 GiB budget is untested at the very top.** gpt-oss-20b (15 GiB resident) works. 35B-A3B (~20 GB weights) is still an unknown. Vendor data covers 32 GB and 96 GB boxes only.
|
|
||||||
- **The FLM serve process was not under systemd** (2026-08-era risk). A reboot
|
|
||||||
of Dankclaw killed it. The fleet law today is the opposite: every deployed
|
|
||||||
container/service ships a boot-persistent, self-healing unit, and the retired
|
|
||||||
NPU unit is deliberately `masked` (`frank-npu.service`, read 2026-10-18) so it
|
|
||||||
cannot come back at boot.
|
|
||||||
- **gpt-oss reasoning budget.** The model can spend its full token budget on `reasoning_content` and return empty content. Clients must handle or cap this.
|
|
||||||
- **No llama.cpp GGUF path.** FLM uses its own pre-compiled kernels. Our UD-Q8_K_XL and HauhauCS builds are not importable. NPU work means FLM-specific model tags.
|
|
||||||
- **NPU production role (2026-08-16, retired 2026-09-11).** `frank-zealot` then
|
|
||||||
served `qwen3.6-moe:35b-a3b` on port 52625; the endpoint was retired with the
|
|
||||||
`amd_iommu=off` decision that day and the unit is masked today. The remaining
|
|
||||||
NPU content (benchmarks, runbook steps) is research on a device that is again
|
|
||||||
usable since 2026-10-18 but serves nothing.
|
|
||||||
|
|
||||||
## Citations
|
|
||||||
|
|
||||||
1. ROCm/FastFlowLM README (chip support, Linux support date, ROCm move, quick start, model storage paths): https://github.com/ROCm/FastFlowLM , accessed 2026-08-16.
|
|
||||||
2. FastFlowLM docs overview: https://fastflowlm.com/docs/ , accessed 2026-08-16.
|
|
||||||
3. FastFlowLM Qwen model cards (tags, quantization, context lengths): https://fastflowlm.com/docs/models/qwen/ , accessed 2026-08-16.
|
|
||||||
4. FastFlowLM gpt-oss model card (15.1 GB weight size, ~15.6 GB NPU cap, 32 GB inconsistency note): https://fastflowlm.com/docs/models/gpt-oss/ , accessed 2026-08-16.
|
|
||||||
5. FastFlowLM Qwen3.5 benchmarks (v0.9.38, Kraken Point 32 GB): https://fastflowlm.com/docs/benchmarks/qwen3.5_results/ , accessed 2026-08-16.
|
|
||||||
6. FastFlowLM Qwen3.6 benchmarks (v0.9.45, Kraken Point 96 GB): https://fastflowlm.com/docs/benchmarks/qwen3.6_results/ , accessed 2026-08-16.
|
|
||||||
7. AMD blog, "FastFlowLM Joins AMD to Advance AI Inference" (2026-07-17): https://www.amd.com/en/blogs/2026/fastflowlm-joins-amd-to-advance-ai-inference.html
|
|
||||||
8. Wikipedia, "Zen 5" article, Strix Halo section (Ryzen AI MAX+ 395: XDNA2 NPU, 50 TOPS, 256-bit LPDDR5X) via local ZIM `wikipedia_en_all_maxi_2026-02`.
|
|
||||||
9. Live system checks on Dankclaw, 2026-08-16: `flm validate` output, `dpkg -l` package list, kernel 7.0.0-29-generic, `/proc/3052096/fd` device fd inspection.
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: FreeToken Builds
|
|
||||||
description: "Building the FreeToken runtime here: the CUDA and ROCm builds and the dependency trap each one hit."
|
|
||||||
tags: [freetoken, build, dankclaw, vllm, mtp]
|
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
|
||||||
---
|
|
||||||
|
|
||||||
# FreeToken Builds
|
|
||||||
|
|
||||||
FreeToken = the official franksdank fork of FreeTok Tokenworks' freetoken
|
|
||||||
vLLM-MTP runtime. Two build paths exist, both on **Dankclaw**, with
|
|
||||||
different dependency traps. Both were verified end-to-end 2026-09-11/12.
|
|
||||||
|
|
||||||
## Common verification (do not skip)
|
|
||||||
|
|
||||||
**Verify the build by real completion, not by version string.** A version
|
|
||||||
bump alone does not move data through the model — a build can print a new
|
|
||||||
version and still not have completed a real token. Before declaring a
|
|
||||||
FreeToken build deployable, run an actual completion (any tiny prompt) and
|
|
||||||
read the returned content:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import json, urllib.request
|
|
||||||
req = urllib.request.Request("http://<host>:<port>/v1/chat/completions",
|
|
||||||
json.dumps({"model": "frank-zealot", "messages": [{"role": "user",
|
|
||||||
"content": "Hi"}]}).encode(), {"Content-Type": "application/json"})
|
|
||||||
print(json.load(urllib.request.urlopen(req, timeout=600)))
|
|
||||||
```
|
|
||||||
|
|
||||||
A 200 with a real `choices[0].message.content` is the only build green.
|
|
||||||
|
|
||||||
## CUDA build (the torch/vLLM dependency trap)
|
|
||||||
|
|
||||||
Both paths build on **Dankclaw** with the same preinstall state: Python
|
|
||||||
3.12.10 in the system venv and **Torch 2.10.0.dev** (nightly, repo
|
|
||||||
download.pytorch.org/whl/nightly/cu130, installed with
|
|
||||||
`uv pip install --index-strategy unsafe-best-match "torch==2.10.0.dev<date>+cu130"
|
|
||||||
--index-url https://download.pytorch.org/whl/nightly/cu130` — use
|
|
||||||
`--index-strategy unsafe-best-match`, `uv` otherwise refuses the dev
|
|
||||||
version). Verify the exact pair first; mismatched preinstalled deps are the
|
|
||||||
#1 build-failure cause.
|
|
||||||
|
|
||||||
Then install the **matching vllm dev wheel**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv pip install --index-strategy unsafe-best-match \
|
|
||||||
--index-url https://wheels.vllm.ai/<commit-sha>/vllm-0.6.6.dev0+g<shortsha>.cu130-cp312-cp312-linux_x86_64.whl \
|
|
||||||
--extra-index-url https://wheels.vllm.ai/nightly/cu130
|
|
||||||
```
|
|
||||||
|
|
||||||
The wheel URL pattern is fixed:
|
|
||||||
`https://wheels.vllm.ai/<full-commit>/vllm-0.6.6.dev0+g<8-char>.cu130-cp312-cp312-linux_x86_64.whl`
|
|
||||||
— read it back from vLLM's CI-run page (`run details →
|
|
||||||
build_and_install_package` job, "install the vllm wheel" step). **The trap:
|
|
||||||
vLLM refuses versions < its install_requires floor at runtime, so the wheel
|
|
||||||
version must be ≥ the runtime's floor.** A wheel built at a lower base
|
|
||||||
(0.5.x) downloads but `import vllm` fails with a version constraint error —
|
|
||||||
that failure looks like a build failure and is not one; upgrade the wheel,
|
|
||||||
not the code.
|
|
||||||
|
|
||||||
If `uv pip install -e . --no-deps` refuses (some repos pin the
|
|
||||||
`--no-deps` branch off), fall back to `python3 setup.py` inside the venv,
|
|
||||||
after `python -m venv --copies .venv && source .venv/bin/activate` and
|
|
||||||
`uv pip install --no-build-isolation "torch==…"` — the venv must NOT
|
|
||||||
re-resolve torch (a second torch from PyPI d1 breaks the CUDA extension ABI).
|
|
||||||
|
|
||||||
## ROCm build (gfx1151 via TheRock nightly)
|
|
||||||
|
|
||||||
Same venv discipline, ROCm wheels from TheRock's nightly index:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv pip install --index-strategy unsafe-best-match \
|
|
||||||
--index-url https://rocm.nightlies.amd.com/v2/gfx1151-dcgpu-supports_fbsgens_split_0717-rc1/ \
|
|
||||||
"torch==2.10.0.dev<date>+rocm" "triton==3.6.0" "pytorch-triton==3.6.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then `vllm` itself (`uv pip install -e . --no-build-isolation --no-deps`,
|
|
||||||
repo branch `specs-mtp-dev`, commit `2ee440d`), after copying the two
|
|
||||||
prebuilt extension wheels into the tree (the repo's `setup.py` skips
|
|
||||||
compiling them when found). The ROCm build also needs
|
|
||||||
`install-fastrak-gfx1151.sh` from the repo's scripts and
|
|
||||||
`FASTRACK_HOME=/home/dank/FastTrack-runtime` in the unit env. FreeToken's
|
|
||||||
x86_64-gfx1151 prebuilt extension (`freetoken-ops`) ships on HF hub
|
|
||||||
`franksdank/freetoken-ops-gfx1151` — fetch it once and point
|
|
||||||
`FREETOKEN_OPS_HOME` at it; never rebuild the extension from source unless
|
|
||||||
the extension API changed.
|
|
||||||
|
|
||||||
`--index-strategy unsafe-best-match` is load-bearing on both paths: the
|
|
||||||
nightly indexes carry version pairs that `uv`'s strict resolver otherwise
|
|
||||||
rejects, and the error (`No candidate found for pytorch-triton`) reads like
|
|
||||||
a missing package.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [[Model Swap and Rollback]] — the swap/deploy story these builds serve
|
|
||||||
- [[Llama Cpp Serving Sizing]] — the harness measurement discipline
|
|
||||||
@@ -14,7 +14,7 @@ Studio refuse custom types at load (`unknown tensor type`); a fork-private type
|
|||||||
means exactly one engine can serve the file. Measured on this fleet repeatedly:
|
means exactly one engine can serve the file. Measured on this fleet repeatedly:
|
||||||
`Qwen3.8-27B-Q8_0_ROCMFPX_AGENT.gguf` is ftype 115 with 311 tensors of type 103
|
`Qwen3.8-27B-Q8_0_ROCMFPX_AGENT.gguf` is ftype 115 with 311 tensors of type 103
|
||||||
(`GGML_TYPE_Q8_0_ROCMFPX`) — only charlie12345/ROCmFPX reads it. The full type
|
(`GGML_TYPE_Q8_0_ROCMFPX`) — only charlie12345/ROCmFPX reads it. The full type
|
||||||
tables live in [[Llama Cpp Runtimes]] and [[Llama Cpp Code Diff]]
|
tables live in and
|
||||||
(ggml_type 100–108, ftype families 100–106 / 110–113 / 118).
|
(ggml_type 100–108, ftype families 100–106 / 110–113 / 118).
|
||||||
|
|
||||||
## 1. Read the header, do not guess
|
## 1. Read the header, do not guess
|
||||||
@@ -69,14 +69,14 @@ filenames lie about quantization and about MTP/vision heads.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Validate before trusting: the parse is right iff the tensor-info block ends
|
Validate before trusting: the parse is right iff the tensor-info block ends
|
||||||
somewhere sane and names look real (`blk.0.attn_q.weight`,
|
somewhere sane and names look real (`blk.0.attn_q.weight`
|
||||||
`token_embd.weight`, `output_norm.weight`). EOF throw = wrong enum or
|
`token_embd.weight`, `output_norm.weight`). EOF throw = wrong enum or
|
||||||
offset — do not work around it.
|
offset — do not work around it.
|
||||||
|
|
||||||
3. **Diff what engines actually bind on**: `general.architecture`; tensor
|
3. **Diff what engines actually bind on**: `general.architecture`; tensor
|
||||||
count; tensor name list in order; per-tensor `ggml_type` and dims; geometry
|
count; tensor name list in order; per-tensor `ggml_type` and dims; geometry
|
||||||
KVs (`<arch>.block_count`, `.embedding_length`, `.feed_forward_length`,
|
KVs (`<arch>.block_count`, `.embedding_length`, `.feed_forward_length`
|
||||||
`.attention.head_count[_kv]`, `.rope.*`, `.ssm.*`, `.context_length`,
|
`.attention.head_count[_kv]`, `.rope.*`, `.ssm.*`, `.context_length`
|
||||||
`.nextn_predict_layers`); MTP tensors (`nextn` in the name); vision tensors
|
`.nextn_predict_layers`); MTP tensors (`nextn` in the name); vision tensors
|
||||||
(`visual`, `v_` prefixes). A same-family fine-tune quantized by the same
|
(`visual`, `v_` prefixes). A same-family fine-tune quantized by the same
|
||||||
tool usually comes out byte-identical in every one of these — that is the
|
tool usually comes out byte-identical in every one of these — that is the
|
||||||
@@ -117,8 +117,8 @@ re-download.
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[Llama Cpp Runtimes]] — which fork ships which custom types
|
- which fork ships which custom types
|
||||||
- [[Llama Cpp Code Diff]] — the enum tables and code-level differences
|
- the enum tables and code-level differences
|
||||||
* [[kingjones-q8agent-rocmfpx]] — the era that served the ftype-115 file, and
|
* the era that served the ftype-115 file, and
|
||||||
[[frank-dragoon]] for what runs there now
|
[[frank-dragoon]] for what runs there now
|
||||||
- [[Llama Cpp Serving Sizing]] — pricing what a payload costs once it loads
|
- [[Llama Cpp Serving Sizing]] — pricing what a payload costs once it loads
|
||||||
@@ -93,7 +93,7 @@ of bytes), so it is tempting. Before proposing it:
|
|||||||
(compare GGUF `tokenizer.ggml.tokens`, never trust the family name), or it
|
(compare GGUF `tokenizer.ggml.tokens`, never trust the family name), or it
|
||||||
must be a head **trained for that target** (EAGLE/DFlash/MTP are
|
must be a head **trained for that target** (EAGLE/DFlash/MTP are
|
||||||
conditioned on the target's hidden states and are not portable between
|
conditioned on the target's hidden states and are not portable between
|
||||||
models);
|
models)
|
||||||
- a plain drafter costs **K sequential forwards per round**, so it must be
|
- a plain drafter costs **K sequential forwards per round**, so it must be
|
||||||
10–50× faster than the target. Break-even with `r = t_draft/t_target`:
|
10–50× faster than the target. Break-even with `r = t_draft/t_target`:
|
||||||
`rate_ratio = A/(K·r + 1)`, `A = (1−p^(K+1))/(1−p)`; at `r ≈ 0.4–0.5`
|
`rate_ratio = A/(K·r + 1)`, `A = (1−p^(K+1))/(1−p)`; at `r ≈ 0.4–0.5`
|
||||||
@@ -107,10 +107,10 @@ of bytes), so it is tempting. Before proposing it:
|
|||||||
(draft gets its own device list) plus `-ngld`/`-otd`/`-ctkd -ctvd`.
|
(draft gets its own device list) plus `-ngld`/`-otd`/`-ctkd -ctvd`.
|
||||||
|
|
||||||
The llama.cpp half of that bullet is checkable upstream: `--spec-draft-device`
|
The llama.cpp half of that bullet is checkable upstream: `--spec-draft-device`
|
||||||
(`-devd` / `--device-draft`, line 4259), `--spec-draft-ngl` (`-ngld`, 4268),
|
(`-devd` / `--device-draft`, line 4259), `--spec-draft-ngl` (`-ngld`, 4268)
|
||||||
`--spec-draft-override-tensor` (`-otd`, 4137) and `--spec-draft-type-k/v`
|
`--spec-draft-override-tensor` (`-otd`, 4137) and `--spec-draft-type-k/v`
|
||||||
(`-ctkd`/`-ctvd`, 4111/4124) all exist in
|
(`-ctkd`/`-ctvd`, 4111/4124) all exist in
|
||||||
`common/arg.cpp` ([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/common/arg.cpp),
|
`common/arg.cpp` ([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/common/arg.cpp)
|
||||||
read 2026-10-18).
|
read 2026-10-18).
|
||||||
|
|
||||||
## 6. Expressing a split (llama.cpp, the usual vehicle)
|
## 6. Expressing a split (llama.cpp, the usual vehicle)
|
||||||
@@ -122,8 +122,8 @@ read 2026-10-18).
|
|||||||
|
|
||||||
- buffer types match by name at load (`ggml_backend_buft_name`), so device
|
- buffer types match by name at load (`ggml_backend_buft_name`), so device
|
||||||
names work as `-ot` values;
|
names work as `-ot` values;
|
||||||
- `-cmoe`/`-ncmoe` are **CPU-only** (`--cpu-moe` / `--n-cpu-moe`,
|
- `-cmoe`/`-ncmoe` are **CPU-only** (`--cpu-moe` / `--n-cpu-moe`
|
||||||
`common/arg.cpp:2764,2771`); device-side MoE placement is `-ot`;
|
`common/arg.cpp:2764,2771`); device-side MoE placement is `-ot`
|
||||||
- several backends in one binary is fine (CUDA + Vulkan is the practical
|
- several backends in one binary is fine (CUDA + Vulkan is the practical
|
||||||
pair; HIP + CUDA do not coexist *(unverified 2026-10-18 — upstream build docs
|
pair; HIP + CUDA do not coexist *(unverified 2026-10-18 — upstream build docs
|
||||||
do not settle the combination either way)*) — `GPU_BACKEND_A=OFF` in the
|
do not settle the combination either way)*) — `GPU_BACKEND_A=OFF` in the
|
||||||
@@ -155,7 +155,7 @@ Rank and report; prefer the boring answer when it wins:
|
|||||||
1. **Two independent endpoints** (zero shared traffic) — often the real
|
1. **Two independent endpoints** (zero shared traffic) — often the real
|
||||||
answer to "what's a useful combination", especially over a narrow link.
|
answer to "what's a useful combination", especially over a narrow link.
|
||||||
2. **One process, weights-resident, KV co-located with attention on the
|
2. **One process, weights-resident, KV co-located with attention on the
|
||||||
fast pool** — promising and quantifiable (+% decode at long context);
|
fast pool** — promising and quantifiable (+% decode at long context)
|
||||||
needs a dual-backend build and a baseline measurement.
|
needs a dual-backend build and a baseline measurement.
|
||||||
3. **Hot-expert cache / per-token weight streaming** — affordable only if
|
3. **Hot-expert cache / per-token weight streaming** — affordable only if
|
||||||
the miss set per token is small (a few experts; check
|
the miss set per token is small (a few experts; check
|
||||||
@@ -169,5 +169,5 @@ Rank and report; prefer the boring answer when it wins:
|
|||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[PCIe Link and Atomics]] — the link and collective walls underneath
|
- [[PCIe Link and Atomics]] — the link and collective walls underneath
|
||||||
- [[Llama Cpp Multi-Device Splits]] — the deployed split shapes
|
- the deployed split shapes
|
||||||
- [[Llama Cpp Serving Sizing]] — KV and pool arithmetic
|
- [[Llama Cpp Serving Sizing]] — KV and pool arithmetic
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "Dankserver uses NVIDIA GPU with system RAM for hybrid processing."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
timestamp: 2026-08-28
|
|
||||||
---
|
|
||||||
|
|
||||||
# Hybrid CPU GPU Processing Dankserver
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Dankserver has **no NVIDIA GPU**: the RTX 3090 left the box on 2026-09-15 and the host now runs two AMD Radeon AI PRO R9700s (`lspci | grep -ci nvidia` → 0, read 2026-10-18 — [[Dankserver System Specs]]). This page's vLLM-era hybrid guidance is reference material, not the serving path: the live service is radiance 1.0.13 as `frank-archon`, and its hybrid mechanism is a pinned host pool (43.88 GiB per rank, `scripts/frank-archon.env:184`) plus the CPU-resident 47.68 GiB n-gram/PLE table, not vLLM KV swapping ([[frank-archon]]).
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Model Weights
|
|
||||||
|
|
||||||
In the setup this page describes, model weights stay on GPU and the GPU must hold all parameters. As a statement about vLLM it is no longer true: current vLLM ships CPU weight offload as `cpu_offload_gb`, plus the grouped prefetch path (`offload_group_size`) — `vllm/config/offload.py:23,54` ([source](https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/config/offload.py), read 2026-10-18). What remains true is the capacity arithmetic: offloaded weights cost PCIe traffic per token (see [[PCIe Link and Atomics]] §2).
|
|
||||||
|
|
||||||
### KV Cache
|
|
||||||
|
|
||||||
KV cache moves between GPU and CPU:
|
|
||||||
|
|
||||||
1. Active KV cache pages stay in GPU VRAM
|
|
||||||
2. Inactive pages swap to CPU RAM
|
|
||||||
3. Swapped pages write to disk swap space if RAM is full
|
|
||||||
4. Pages reload to GPU when needed
|
|
||||||
|
|
||||||
### Compute Distribution
|
|
||||||
|
|
||||||
- GPU handles all matrix multiplication and attention computation
|
|
||||||
- CPU handles tokenization, detokenization, and input processing
|
|
||||||
- CPU handles KV cache swap operations
|
|
||||||
- GPU worker processes run on CPU cores via multiprocessing
|
|
||||||
|
|
||||||
## Optimal Settings for Dankserver
|
|
||||||
|
|
||||||
### GPU Memory Utilization
|
|
||||||
|
|
||||||
Set `--gpu-memory-utilization` based on your GPU:
|
|
||||||
|
|
||||||
| GPU | Recommended Value |
|
|
||||||
|-----|-------------------|
|
|
||||||
| 24 GB (RTX 3090/4090) | 0.7-0.8 |
|
|
||||||
| 40 GB (A100) | 0.8-0.9 |
|
|
||||||
| 80 GB (A100 80GB) | 0.85-0.95 |
|
|
||||||
| 80 GB (H100) | 0.9-0.95 |
|
|
||||||
|
|
||||||
Dankserver has neither NVIDIA card today: it carries 2× 32 GB (32,624 MiB) R9700s.
|
|
||||||
The live engine does not use this knob at all — it claims an explicit per-card
|
|
||||||
pair instead (`--vram-weights-mib 25600` / `--vram-kv-mib 5120`,
|
|
||||||
`scripts/frank-archon.env:230-231`).
|
|
||||||
|
|
||||||
Lower values leave more GPU memory for KV cache. This reduces swap frequency.
|
|
||||||
|
|
||||||
### Swap Space / KV Offload
|
|
||||||
|
|
||||||
Current vLLM has no `--swap-space` engine argument, and the CPU KV-offload knob pair
|
|
||||||
is owned by [[RAM CPU Offloading in VLLM]] (that page carries the upstream
|
|
||||||
`CacheConfig` verification). In Podman, mount the host cache read-write for KV offload
|
|
||||||
writes. The table below shows historical `--swap-space` guidance as a sizing reference.
|
|
||||||
|
|
||||||
| System RAM | Recommended Swap |
|
|
||||||
|------------|------------------|
|
|
||||||
| 32 GB | 8-16 GB |
|
|
||||||
| 64 GB | 16-32 GB |
|
|
||||||
| 128 GB | 32-64 GB |
|
|
||||||
| 256 GB | 64-128 GB |
|
|
||||||
|
|
||||||
More swap space allows longer contexts and more concurrent requests.
|
|
||||||
|
|
||||||
### Tensor Parallelism
|
|
||||||
|
|
||||||
Use tensor parallelism for large models:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve meta-llama/Llama-3.1-70B-Instruct \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--gpu-memory-utilization 0.85
|
|
||||||
```
|
|
||||||
|
|
||||||
### KV Cache Data Type
|
|
||||||
|
|
||||||
Use FP8 KV cache for Ampere and newer NVIDIA GPUs. On this host's RDNA4 cards the live engine runs an fp8 KV cache too (`kv fp8` — `scripts/frank-archon.env:264`, which also records that this engine takes only bf16|fp8 for the whole cache).
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.9
|
|
||||||
```
|
|
||||||
|
|
||||||
FP8 KV cache reduces memory usage by 50% compared to BF16.
|
|
||||||
|
|
||||||
## NUMA Optimization
|
|
||||||
|
|
||||||
Dankserver reports **one** NUMA node today (`lscpu` → `NUMA node(s): 1`, `NUMA node0 CPU(s): 0-7`, read 2026-10-18), so NUMA binding buys nothing on this host; these flags matter on multi-node topologies:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve model \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--numa-bind
|
|
||||||
```
|
|
||||||
|
|
||||||
This pins each GPU worker to its nearest NUMA node. This reduces memory access latency.
|
|
||||||
|
|
||||||
For explicit NUMA binding:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve model \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--numa-bind \
|
|
||||||
--numa-bind-nodes 0 0 1 1
|
|
||||||
```
|
|
||||||
|
|
||||||
## CPU Resource Requirements
|
|
||||||
|
|
||||||
VLLM needs CPU cores for:
|
|
||||||
|
|
||||||
- 1 API server process
|
|
||||||
- 1 engine core process
|
|
||||||
- N GPU worker processes (one per GPU)
|
|
||||||
|
|
||||||
Minimum physical cores = 2 + N (where N is GPU count).
|
|
||||||
|
|
||||||
For Dankserver, which has **2** GPUs (2× R9700) and 8 cores (`lscpu`, read
|
|
||||||
2026-10-18):
|
|
||||||
|
|
||||||
- Minimum 4 physical CPU cores by the 2 + N rule
|
|
||||||
- Recommended 8+ physical CPU cores — the host has exactly 8 (SMT off)
|
|
||||||
|
|
||||||
## Performance Tuning
|
|
||||||
|
|
||||||
### Chunked Prefill
|
|
||||||
|
|
||||||
Chunked prefill is enabled by default. Tune with:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve model \
|
|
||||||
--max-num-batched-tokens 8192
|
|
||||||
```
|
|
||||||
|
|
||||||
Higher values improve throughput. Lower values improve latency.
|
|
||||||
|
|
||||||
### CUDA Graphs
|
|
||||||
|
|
||||||
CUDA graphs improve throughput. Disable for faster startup:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve model \
|
|
||||||
--enforce-eager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compilation Cache
|
|
||||||
|
|
||||||
Reuse torch.compile artifacts:
|
|
||||||
|
|
||||||
```
|
|
||||||
export VLLM_CACHE_ROOT=~/.cache/vllm
|
|
||||||
vllm serve model
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy cache between boots for faster startup.
|
|
||||||
|
|
||||||
### Skip Memory Profiling
|
|
||||||
|
|
||||||
Skip memory profiling on repeated boots:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve model \
|
|
||||||
--kv-cache-memory <value_from_first_boot_log>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Check GPU memory:
|
|
||||||
|
|
||||||
```
|
|
||||||
rocm-smi
|
|
||||||
```
|
|
||||||
|
|
||||||
Check for preemption warnings in VLLM logs.
|
|
||||||
|
|
||||||
Check CPU utilization:
|
|
||||||
|
|
||||||
```
|
|
||||||
htop
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Commands
|
|
||||||
|
|
||||||
### Single GPU, Medium Model
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-35B-A3B \
|
|
||||||
--gpu-memory-utilization 0.85 \
|
|
||||||
--max-num-batched-tokens 4096 \
|
|
||||||
--max-num-seqs 128
|
|
||||||
```
|
|
||||||
|
|
||||||
### Single GPU, FP8 Model
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.9 \
|
|
||||||
--max-num-batched-tokens 8192
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-GPU, Large Model
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve meta-llama/Llama-3.1-70B-Instruct \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--gpu-memory-utilization 0.9 \
|
|
||||||
--max-model-len 8192
|
|
||||||
```
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[VLLM Index]]
|
|
||||||
- [[RAM CPU Offloading in VLLM]]
|
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]]
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: LLM Bench Windows and Measurement
|
|
||||||
description: "Opening and restoring benchmark windows on fleet hosts, honest methodology, and the usage-counter law under drafting."
|
|
||||||
tags: [benchmarking, llama-bench, ab-testing, speculative, vllm, fleet]
|
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
|
||||||
---
|
|
||||||
|
|
||||||
# LLM Bench Windows and Measurement
|
|
||||||
|
|
||||||
Fleet law first: **STOP the production service before any GPU experiment**
|
|
||||||
(AGENTS.md Benchmarking Policy) — a bench beside a live service starves the
|
|
||||||
GPU and corrupts results; an unisolated GPU experiment has precedent for
|
|
||||||
wedging a host until power cycle. Reference harnesses:
|
|
||||||
`scripts/bench-batch-ubatch.sh`, `scripts/autoresearch.sh`,
|
|
||||||
`scripts/autoresearch/frank-archon-arms.sh`.
|
|
||||||
|
|
||||||
## 1. The window structure (one config per arm, a driver owns the window)
|
|
||||||
|
|
||||||
1. **Stop + gate before every arm** — `wait_for_clear`: unit NOT active AND
|
|
||||||
no RUNNING container of that name (`podman ps`, not `-a`; `-a` counts
|
|
||||||
EXITED containers and can never clear — `rm -f` the exited one first) AND
|
|
||||||
port free (`ss -ltn`). The graceful-stop race cost a whole arm 2026-10-01:
|
|
||||||
`ExecStop=podman stop -t 120` drains up to 2 min; a driver that sleeps 3 s
|
|
||||||
then polls `/health` gets **200 from the still-running PRODUCTION engine**
|
|
||||||
— the arm "passes" with production's boot log, then `podman exec` dies
|
|
||||||
with `container state improper`.
|
|
||||||
2. **Boot the arm as the service, not a scratch port** (no scratch ports on
|
|
||||||
LLM hosts — one card, one server). The serve script takes its config via
|
|
||||||
env-file override (`FRANK_ZEALOT_ENV_FILE`, `FRANK_ARCHON_AR_ENV_FILE`)
|
|
||||||
pointing at a staged copy; the checkout is never mutated.
|
|
||||||
3. **Fast-fail the health wait**: poll `podman ps | grep -qx <name>` inside
|
|
||||||
the loop — a refused boot dies in one poll instead of 20 min. Capture the
|
|
||||||
serve script's FULL stdout on failure (a `tail -8` loses engine-init root
|
|
||||||
causes; raise happens above the tail).
|
|
||||||
4. **Measure** (§4), capture boot-log engine facts (KV pool, arena slots,
|
|
||||||
SpecDecoding lines) — they survive measure crashes.
|
|
||||||
5. **Teardown + converge**: kill by explicit PID/pgid (never pattern kills),
|
|
||||||
`podman rm -f` scratch, wait `MemAvailable ≥ ~230 GiB` on dankserver
|
|
||||||
before the next boot; restore production from the EXIT trap and verify the
|
|
||||||
**conjunction oracle**: health 200 + `is-active` + `is-enabled` +
|
|
||||||
mask-symlink clean. A bare `systemctl start` is not a restore oracle.
|
|
||||||
(`systemctl mask` FAILS on a relinked unit — log it, explicit stop is
|
|
||||||
enough: systemd never auto-restarts an explicitly stopped unit.)
|
|
||||||
6. **Warm boots cycle ~2 min** (page cache), cold ~8 min — size timeouts for
|
|
||||||
mixed speeds.
|
|
||||||
|
|
||||||
**Destructive tails (2026-10-01):** a scratch script whose tail does
|
|
||||||
`systemctl stop` + `podman rm -f <PRODUCTION container>` fires later if a
|
|
||||||
probe loop survived the driver kill — it can destroy a just-deployed
|
|
||||||
production engine (signature: deploy exits green, then seconds-to-minutes
|
|
||||||
later `Exited (0)`, `Deactivated successfully`, clean exit = removed, not
|
|
||||||
crashed). Before ANY deploy: `pgrep -af` every scratch script of the session
|
|
||||||
and kill strays by explicit PID; killing the dev-side job does NOT kill the
|
|
||||||
host-side `deploy-remote.sh` (two of them once managed the same unit).
|
|
||||||
|
|
||||||
Two failure shapes inside the window that read as "the bench died":
|
|
||||||
|
|
||||||
- **Stale reads from a stopping container.** `podman stop` answers while the
|
|
||||||
engine inside is still flushing; a `podman logs` taken in that window
|
|
||||||
returns the boot-prefix only. Poll `podman ps` for `Exited (0)` (or
|
|
||||||
`podman wait`) before reading logs, or you will write "no results" into
|
|
||||||
the table for a run that has them.
|
|
||||||
- **A D-state worker wedges the stop.** A bench child that wedged in the
|
|
||||||
kernel ignores SIGTERM, so `podman stop -t N` burns its full timeout and
|
|
||||||
then the container is `Stopping` forever. Kill by container first
|
|
||||||
(`podman kill <ctr>` → SIGKILL path), verify the process is gone from
|
|
||||||
`podman ps` AND `nvidia-smi`, and only then re-serve. Never let the
|
|
||||||
window script proceed to a redeploy with a zombie bench worker holding
|
|
||||||
VRAM.
|
|
||||||
|
|
||||||
## 2. llama-bench sweeps (offline batch/ubatch/split/KV/depth)
|
|
||||||
|
|
||||||
**What llama-bench cannot express — decide BEFORE designing:** no
|
|
||||||
`--ctx-size` (`n_ctx = n_prompt + n_gen + n_depth`, so a bench can NEVER prove
|
|
||||||
activation buffers fit beside the DEPLOYED KV — only the real unit answers
|
|
||||||
that), no `--parallel` (no slot shape, no concurrency), no `--spec-*`
|
|
||||||
(`tg` numbers are the drafterless baseline, useful as a control only), and a
|
|
||||||
warmup run is itself a full-size run. `-d N` is a depth PREFIX (paid once per
|
|
||||||
cell via a `llama_state_seq_*` save/restore); without it a `tg` run is at
|
|
||||||
~zero depth and meaningless for long conversations.
|
|
||||||
|
|
||||||
**The separator trap:** `/` separates devices WITHIN one configuration, `,`
|
|
||||||
separates sweep values — for both `-dev` and `-ts`. `-ts 36,29` parses as the
|
|
||||||
single value 36 → everything on one card. `--tensor-split 36,29` (server) →
|
|
||||||
`-ts 36/29` (bench). Ranges exist: `-ub 512-2048+256`.
|
|
||||||
|
|
||||||
**Harness shape:** one invocation per cell (an infeasible cell calls
|
|
||||||
`exit(1)` and kills every later cell of a cross-product invocation — and
|
|
||||||
infeasibility is usually the question); TSV row per cell as it completes +
|
|
||||||
`bench.json` (`-o json`) + `bench.log` (`--progress`) + a 2 s `rocm-smi
|
|
||||||
--showmeminfo vram` VRAM trace per cell (a cell that allocated less than
|
|
||||||
intended looks fast); resume by skipping non-empty cell outputs; stop the
|
|
||||||
service in the harness, restore from `trap … EXIT`, verify afterwards. Match
|
|
||||||
the deployment's load flags exactly (`-ngl -sm -ts -fa -ctk/-ctv -t`).
|
|
||||||
|
|
||||||
**Run under a transient unit, not a shell:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemd-run --unit=<name> --collect --no-block --property=Type=oneshot \
|
|
||||||
--property=LimitMEMLOCK=infinity --property=User=dank \
|
|
||||||
/home/dank/serverAppConfigs/scripts/<harness>.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Deploy units set `LimitMEMLOCK=infinity` for ROCm staging; an SSH shell has
|
|
||||||
8 MiB and fails the load for an unrelated reason. This also survives ssh
|
|
||||||
disconnects. **Boot rungs under the production environment, not a bench
|
|
||||||
shell** — a rootless shell probe booted 3×262144 bf16 that production refused
|
|
||||||
(systemd `User=` + memlock change what the pinned-expert path holds):
|
|
||||||
`sudo systemd-run --uid=<user> --unit=<name> -p LimitMEMLOCK=infinity -p
|
|
||||||
Restart=no env VARFILE=… /path/serve.sh`.
|
|
||||||
|
|
||||||
**Verify before burning hours:** dry-run against a STUB binary with
|
|
||||||
`SERVICE=<nonexistent-name>` (the trap/stop never reached), tiny grid,
|
|
||||||
including one forced-`exit 3` cell — prove the TSV row, failure capture,
|
|
||||||
resume logic. Validate the real flag set in ~1 s by pointing `-m` at a
|
|
||||||
nonexistent file. **Reading:** bracket the sweep (reference cell first AND
|
|
||||||
last); check per-cell `stddev_ts` (a difference smaller than spread is not a
|
|
||||||
result); single-sequence decode is usually flat across batch/ubatch (prefill
|
|
||||||
knob); batch ≥ ubatch (llama.cpp clamps ubatch to batch, silently duplicating
|
|
||||||
smaller-batch cells); report what the grid cannot prove (drafter, slots, real
|
|
||||||
context).
|
|
||||||
|
|
||||||
## 3. HTTP benches of server-side parameters
|
|
||||||
|
|
||||||
`llama-bench` benches the bench binary, not the running server — server-side
|
|
||||||
params (`--threads-batch`, `--ubatch-size`, `--spec-type`, `--parallel`) need
|
|
||||||
HTTP:
|
|
||||||
|
|
||||||
- **Prefill**: large prompt, `max_tokens=1` (server crashes on 0), measure
|
|
||||||
`prompt_tokens / wall_time`.
|
|
||||||
- **Decode**: `max_tokens=16384` minimum, NEVER 1 (warmup, cache growth and
|
|
||||||
spec acceptance all stabilize over thousands of tokens); measure
|
|
||||||
`predicted_per_second` from `timings` or `completion_tokens / wall_time`;
|
|
||||||
temperature 0.
|
|
||||||
|
|
||||||
Known fleet harnesses: `scripts/bench-threads-batch-http.py`,
|
|
||||||
`scripts/bench-decode-threads-batch.py`, `scripts/bench-spec-decode.py`.
|
|
||||||
sglang-specific: `max_tokens=1` on `/v1/chat/completions` with the qwen3
|
|
||||||
reasoning parser streams zero deltas — use `/v1/completions`.
|
|
||||||
|
|
||||||
## 4. The measurement law (under speculative decoding this is absolute)
|
|
||||||
|
|
||||||
**Never count SSE chunks.** Rejected drafts break the 1:1 chunk↔token
|
|
||||||
mapping; chunk counting reported 8.3 tok/s for a config whose server counters
|
|
||||||
said 23–26 — a 3x false regression that nearly abandoned the change that
|
|
||||||
worked. Always:
|
|
||||||
|
|
||||||
```python
|
|
||||||
decode_tok_s = usage.completion_tokens / (wall - ttft)
|
|
||||||
# stream=True, stream_options: {include_usage: true}; ttft = first content-bearing chunk
|
|
||||||
```
|
|
||||||
|
|
||||||
vLLM's 10 s `Accepted throughput` log lines are a rolling window, not a
|
|
||||||
measurement (6.2 → 13.1 across adjacent windows on identical settings) — use
|
|
||||||
them for acceptance STATISTICS only (`Avg Draft acceptance rate`, `Mean
|
|
||||||
acceptance length`, per-position rates). Warm pass + ≥3 runs, median;
|
|
||||||
temperature 0; ≥256 tokens. **A decode "jump" with early-stopped completions
|
|
||||||
(ct < 256) and acceptance > 90 % is degenerate generation** (the fork's
|
|
||||||
repetition-halt fired at ~140 tokens on corrupted numerics) — inspect text
|
|
||||||
before believing a win. Run-to-run spread is 5–10 %: repeat arms; one sample
|
|
||||||
nearly shipped the wrong setting.
|
|
||||||
|
|
||||||
## 5. Honest methodology
|
|
||||||
|
|
||||||
- **Match the corpus to the workload.** Token soup under-reports speculative
|
|
||||||
decoding (2.9–3.6 tok/update vs 3.86–5.8 on real code) and mis-ranks draft
|
|
||||||
settings; acceptance, not step time, moves throughput. Build the corpus
|
|
||||||
from the repo's own sources: `{"prompt": "<instruction>\n\n<file body>"}`,
|
|
||||||
`--custom-output-len`.
|
|
||||||
- **Salt every run** with a unique per-arm token in the first bytes — it is
|
|
||||||
the prefix-cache key (and a proxy routing key). Without it arm 2 measures
|
|
||||||
arm 1's warm cache.
|
|
||||||
- **Two workload shapes need two clients:** distinct prompts (batch
|
|
||||||
behaviour, prefill parallelism) and sequential turns with growing history
|
|
||||||
(TTFT of the first streamed chunk per turn — the only measurement that
|
|
||||||
exposes prefix-cache behaviour). These pull in opposite directions: a cold
|
|
||||||
batch spreads prefill across replicas, a conversation's next turn only pays
|
|
||||||
if it lands where its prefix is cached.
|
|
||||||
- **Read the engine's ledger, not the card:** `Available KV cache memory` /
|
|
||||||
`GPU KV cache size: N tokens` / `Maximum concurrency for M tokens per
|
|
||||||
request: X.Xx` is the capacity number. vLLM's `Prefix cache hit rate:` line
|
|
||||||
is a LIFETIME average across every prompt the replica ever saw — send the
|
|
||||||
same prompt twice to an idle engine and compare TTFTs (measured 22.3 s cold
|
|
||||||
→ 1.4 s warm for a 50k prefix). `GPU KV cache usage` says the pool is not
|
|
||||||
full; it does NOT say a prefix survived. The KV budget is NOT monotonic in
|
|
||||||
chunk size (5.44 GiB at CHUNK 8192, 4.22 at 16384, 4.16 at 4096) — measure
|
|
||||||
three points before treating a knob as a lever.
|
|
||||||
- **Soak before believing:** 15–30 min of concurrent families, count failures
|
|
||||||
AND latency; report hit/miss split and medians with min/max — the median of
|
|
||||||
a bimodal distribution describes neither mode.
|
|
||||||
- **Verification that is not the benchmark:** run the client the fleet
|
|
||||||
actually uses; and a deployed unit is not proven until an unattended reboot
|
|
||||||
brings it back.
|
|
||||||
- **Record the "no" answers** with numbers and mechanism — placement balance,
|
|
||||||
concurrency admission and chunk-size tweaks all measured flat or worse
|
|
||||||
here; the wall was arithmetic (cached prefixes + live sequences share one
|
|
||||||
pool).
|
|
||||||
|
|
||||||
## 6. Sweep hygiene (the arms that produced wrong verdicts)
|
|
||||||
|
|
||||||
- **Prefetch uniformity:** every arm in a sweep carries the same
|
|
||||||
expert-prefetch setting. The 2026-09-29 campaign measured losing arms with
|
|
||||||
prefetch ON and incumbents OFF; untaxed, verdicts inverted (no-spec
|
|
||||||
12.11 → 41.47; gather 32×64 parity → +18.7 %). Never re-read a taxed
|
|
||||||
verdict.
|
|
||||||
- **Knob-landed preflight:** for every `-e` knob, confirm
|
|
||||||
`podman exec <ctr> printenv <VAR>` — a knob the fork silently ignores
|
|
||||||
produces a "parity" arm that is really the default (v1 of the gather sweep
|
|
||||||
lost all 3 arms this way; serve-script hooks are additive and can drop
|
|
||||||
vars).
|
|
||||||
- **Always run an explicit OFF arm:** the 2026-09-29 campaign's only win was
|
|
||||||
deleting a baseline's silent `--expert-prefetch 1` (+40–51 %). Challengers
|
|
||||||
compare against default-off, not "incumbent + one flag".
|
|
||||||
- **Pin the capacity bar:** `--kv-cache-memory-bytes` (contract) rather than
|
|
||||||
autofit (remainder) so pool numbers are reproducible; compute need =
|
|
||||||
tokens × measured B/token (hybrid models: KV dtype does NOT double mamba
|
|
||||||
states — measure, don't assume 2x).
|
|
||||||
- **Report decode AND prefill (TTFT) per rung:** a decode-only view nearly
|
|
||||||
deployed a 2x-prefill-tax config. Per-depth tables, never a single median —
|
|
||||||
one fork's gather win grows with depth (54 t/s @1k, 39.5 @short).
|
|
||||||
- Container recreate (`podman rm -f`) wipes `podman cp`'d harnesses — re-copy
|
|
||||||
`mtp_measure.py`/`bench_iter.py` after EVERY recreate, or mount them.
|
|
||||||
|
|
||||||
## 7. Bash laws that ate windows (each cost an arm)
|
|
||||||
|
|
||||||
1. `${VAR:+SPEC=$1}` in front of a command expands to a bare word →
|
|
||||||
`SPEC=3: command not found`, nothing runs. Pass literal assignments:
|
|
||||||
`SPEC="${1:-}" bash serve.sh`; the callee's `${SPEC:+--flag}` skips empty.
|
|
||||||
2. `${2WAY:-0}` is fatal `bad substitution` (digit-leading name), and inside
|
|
||||||
an if/else it aborts ONLY that compound statement — the script CONTINUES,
|
|
||||||
every arm tears down clean with zero measurements. Detection law: `ls`
|
|
||||||
the per-arm `.measure.txt` files before trusting any summary; arm echoes
|
|
||||||
are not evidence.
|
|
||||||
3. `local a=$1 b=$x` expands `$x` before the builtin assigns — under `set -u`
|
|
||||||
it aborts AFTER the harness stopped production. Two statements.
|
|
||||||
4. `sed -i` renames the file; already-running bash keeps the OLD inode —
|
|
||||||
mid-flight patches miss arms launched in the preceding minutes. Patch
|
|
||||||
early or run a make-up window.
|
|
||||||
5. Never heredoc python through ssh→zsh→bash — write to a file, ship it
|
|
||||||
(`ssh host 'cat > /path && chmod +x /path' < local.sh`); nested quoting
|
|
||||||
fails silently. Same trap in `git commit -m`: backticks become command
|
|
||||||
substitution.
|
|
||||||
6. No comments INSIDE a backslash-continued command (the continuation joins
|
|
||||||
the comment); verify multi-line command construction with `bash -n` AND a
|
|
||||||
render test.
|
|
||||||
|
|
||||||
## 8. Results location and recovery (frank-archon arms)
|
|
||||||
|
|
||||||
| copy | path |
|
|
||||||
|---|---|
|
|
||||||
| per-arm evidence | `$ARMS_DIR` = `~/.jobs/<run-stamp>/` (`.rc`, `.done` beside it) |
|
|
||||||
| summary | `$ARMS_DIR/summary.tsv` = `arm rc decode_tps decode_tps_t0.7 kv_tokens boot_s` + `<arm>.out/.env/.overlay/.rc/.metrics` |
|
|
||||||
| server-side last window | `/home/dank/.autoresearch-frank-archon/run.json` |
|
|
||||||
| durable memo | `.../last/run.json` + `last/hash` (promoted only after bench AND verified restore) |
|
|
||||||
| fallback | `~/.omp/agent/sessions/<ws>/<session>/<N>.{shake,bash}.log` (permanent) |
|
|
||||||
|
|
||||||
`/tmp/frank-archon-arms-*` and `/tmp/armstub/` are stub self-tests —
|
|
||||||
constants 99.9/88.8 identical across arms by construction; **identical
|
|
||||||
numbers across arms == stub or bug, never a measurement.** After compaction,
|
|
||||||
re-harvest with `grep -rhoE 'decode_tps[a-z0-9_.]*=[0-9.]+'` over the session
|
|
||||||
logs. A window costs ~20 min warm; a detached runner's timeout ≥ 3600 s per
|
|
||||||
window.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [[RDNA4 MoE Expert Offload VRAM Budget]] — the plan arithmetic being swept
|
|
||||||
- [[DFlash DSpark Speculative Decoding]] — spec A/B protocol and shapes
|
|
||||||
- [[LLM Performance Diagnosis]] — profiling and attribution methods
|
|
||||||
- [[Remote Job Runner]] — detached execution of long sweeps
|
|
||||||
- [[frank-medium-llamacpp-dflash2]] — the measured ubatch-optimum history
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
type: reference
|
type: reference
|
||||||
title: LLM Performance Diagnosis
|
title: LLM Performance Diagnosis
|
||||||
description: "Probing why a served model is slow: the flat-depth probe, offload duty-cycle arithmetic, and profiler attribution."
|
description: "Probing why a served model is slow: the flat-depth probe, offload duty-cycle arithmetic, and step-shape attribution."
|
||||||
tags: [performance, profiling, moe, pcie, prefix-cache, routing, telemetry]
|
tags: [performance, profiling, moe, pcie, prefix-cache, routing, telemetry]
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
timestamp: 2026-10-19T00:00:00-07:00
|
||||||
---
|
---
|
||||||
|
|
||||||
# LLM Performance Diagnosis
|
# LLM Performance Diagnosis
|
||||||
@@ -21,7 +21,7 @@ the cost; the problem is per-token expert movement and KV/attention tuning is
|
|||||||
ruled out entirely. A genuinely context-bound server shows a clear downward
|
ruled out entirely. A genuinely context-bound server shows a clear downward
|
||||||
slope. Record cold vs warm separately — a small gap means the cache is
|
slope. Record cold vs warm separately — a small gap means the cache is
|
||||||
working. The canonical flat run (fp8 expert-offload shape, a few hundred to
|
working. The canonical flat run (fp8 expert-offload shape, a few hundred to
|
||||||
sixteen thousand tokens of context) is in [[Tcclaviger Fork Ops]]'s profile.
|
sixteen thousand tokens of context) is in 's profile.
|
||||||
|
|
||||||
## 2. Duty-cycle arithmetic (the "GPU not fully utilized" answer)
|
## 2. Duty-cycle arithmetic (the "GPU not fully utilized" answer)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ Worked example (GLM-5.3-Flash, 2×GPU, offload, decode 9.9 tok/s):
|
|||||||
|
|
||||||
No amount of link arithmetic would have produced those two conclusions: the
|
No amount of link arithmetic would have produced those two conclusions: the
|
||||||
suspect was 1 %, and the mass was pointwise glue from an uncompiled trunk —
|
suspect was 1 %, and the mass was pointwise glue from an uncompiled trunk —
|
||||||
whose own boot log names the cause (`FULL_DECODE_ONLY` override). It also
|
whose own boot log names the cause (`FULL_DECODE_ONLY` override). It
|
||||||
predicted a negative result in advance (spec decode cannot help a shape whose
|
predicted a negative result in advance (spec decode cannot help a shape whose
|
||||||
per-token cost scales with the token count) which matched an unexplained flat
|
per-token cost scales with the token count) which matched an unexplained flat
|
||||||
MTP measurement.
|
MTP measurement.
|
||||||
@@ -117,7 +117,7 @@ per-request telemetry — it measures the real workload and costs nothing.
|
|||||||
- One `max_tokens=200` request is production traffic and fine; anything that
|
- One `max_tokens=200` request is production traffic and fine; anything that
|
||||||
pins the card is a bench → stop-and-mask first. Check WHICH instance you
|
pins the card is a bench → stop-and-mask first. Check WHICH instance you
|
||||||
are reading (`systemctl status | head -12`; task ids jump across
|
are reading (`systemctl status | head -12`; task ids jump across
|
||||||
restarts). The engine's own ledger (`KV self size`, `n_slots`,
|
restarts). The engine's own ledger (`KV self size`, `n_slots`
|
||||||
`n_ctx_slot`) outranks your model of it.
|
`n_ctx_slot`) outranks your model of it.
|
||||||
|
|
||||||
## 5. The fork's data path (before blaming the link)
|
## 5. The fork's data path (before blaming the link)
|
||||||
@@ -130,7 +130,7 @@ tokens, so the step can silently cross the threshold onto the slow path as
|
|||||||
draft depth grows. Check the threshold's premise: a default tuned at
|
draft depth grows. Check the threshold's premise: a default tuned at
|
||||||
12.4 MiB/expert does not transfer to 2.344 MiB/expert, where copy-in can be
|
12.4 MiB/expert does not transfer to 2.344 MiB/expert, where copy-in can be
|
||||||
several times faster. TunableOp ghost-skip: `CLAV_TUNABLEOP_SWEEP=1` alone
|
several times faster. TunableOp ghost-skip: `CLAV_TUNABLEOP_SWEEP=1` alone
|
||||||
does NOTHING under expert offload (`CLAV_TUNABLEOP_WITH_OFFLOAD=1` required);
|
does NOTHING under expert offload (`CLAV_TUNABLEOP_WITH_OFFLOAD=1` required)
|
||||||
check for the skip line before concluding autotuning ran. Gather-geometry
|
check for the skip line before concluding autotuning ran. Gather-geometry
|
||||||
knobs read at import time → each arm is a full reload.
|
knobs read at import time → each arm is a full reload.
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ analyze; `--analyze-only`).
|
|||||||
Measure with a **sequential-turn client**, not only a batch benchmark — a
|
Measure with a **sequential-turn client**, not only a batch benchmark — a
|
||||||
batch of distinct prompts parallelises prefill across replicas and hides the
|
batch of distinct prompts parallelises prefill across replicas and hides the
|
||||||
problem. The two failure modes: round-robin loses every other cache hit
|
problem. The two failure modes: round-robin loses every other cache hit
|
||||||
(90k-token contexts: warm-turn TTFT 2676 ms round-robin vs 1096 ms pinned);
|
(90k-token contexts: warm-turn TTFT 2676 ms round-robin vs 1096 ms pinned)
|
||||||
pinning-everything loses a card (16 requests: 18.6 s spread vs 30.1 s
|
pinning-everything loses a card (16 requests: 18.6 s spread vs 30.1 s
|
||||||
pinned). **The rule that gets both: route by request shape** — a request
|
pinned). **The rule that gets both: route by request shape** — a request
|
||||||
carrying more than one message is a continuation (agent loop re-sending its
|
carrying more than one message is a continuation (agent loop re-sending its
|
||||||
@@ -209,13 +209,79 @@ is not a per-request verdict (probe directly: same prompt twice to an idle
|
|||||||
replica — 22.3 s cold → 1.4 s warm); salt corpora; record sample sizes;
|
replica — 22.3 s cold → 1.4 s warm); salt corpora; record sample sizes;
|
||||||
`vllm bench serve` needs `--tokenizer <local path>`; verify metric names
|
`vllm bench serve` needs `--tokenizer <local path>`; verify metric names
|
||||||
against the live endpoint before dashboards. Test shape: drive the routing
|
against the live endpoint before dashboards. Test shape: drive the routing
|
||||||
function directly with synthetic bodies (stdlib-only policy module);
|
function directly with synthetic bodies (stdlib-only policy module)
|
||||||
conversation identity is the FIRST N KiB of prompt — a shorter tested prefix
|
conversation identity is the FIRST N KiB of prompt — a shorter tested prefix
|
||||||
hashes differently per turn and pinning silently never happens.
|
hashes differently per turn and pinning silently never happens.
|
||||||
|
|
||||||
|
## 9. Decode cadence under a prefill (the step-shape census)
|
||||||
|
|
||||||
|
"Decode turns to sludge the moment a long prompt arrives, then recovers" is not a
|
||||||
|
link or a gather story — on a chunked-prefill engine the unit of interference is
|
||||||
|
**the step**. A step that carries a prefill chunk takes the chunk's time, and every
|
||||||
|
decode row sharing that step waits that long for its next token. Chunk size is
|
||||||
|
`min(remaining, --checkpoint-interval, --max-num-batched-tokens)`, so on a hybrid
|
||||||
|
model the interval caps it before the budget does.
|
||||||
|
|
||||||
|
Census it without touching the model — the engine's own counters (`/stats`,
|
||||||
|
`/metrics`, cumulative since boot):
|
||||||
|
|
||||||
|
```
|
||||||
|
decode-only step = step_ns_decode / steps_decode
|
||||||
|
shared step = (step_ns - step_ns_decode) / (steps_prefill + steps_mixed)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then bucket a day of `decode_tokens_per_second` by `prefill_tokens_per_second`
|
||||||
|
(Prometheus `query_range`, step 120 s) and **check the lag**: correlate decode with
|
||||||
|
prefill at lag 0 and again at 2-14 minutes. Lag-0-only ⇒ step-time contention, the
|
||||||
|
chunk is holding the device. A lagged tail ⇒ residency churn — the prefill's working
|
||||||
|
set evicted something decode needs (expert slab, KV) and decode pays after the
|
||||||
|
prefill is over. `preemptions` separates the third shape: a preempted-and-recomputed
|
||||||
|
sequence stops producing entirely and then re-prefills from token 0, which is a
|
||||||
|
stall, not sludge.
|
||||||
|
|
||||||
|
Knobs, in the order worth trying them (radiance GUIDE §4.5):
|
||||||
|
|
||||||
|
- `--prefill-interleave N` — after a chunk step taken beside decoders, the next N
|
||||||
|
steps carry decode rows only and admit nothing. Decoders keep their cadence; the
|
||||||
|
prompt pays that in TTFT. Counts as `engine_steps_held_total`.
|
||||||
|
- `--busy-prefill-chunk MODE` — the chunk's share of a step that has other work:
|
||||||
|
`auto` (default) reserves a row plus the draft window per decode row behind it and
|
||||||
|
an eighth of the budget per waiting prefill/request, `off` lets the chunk take the
|
||||||
|
whole step (admission never runs while it does), `N` pins the share. Never under
|
||||||
|
the prefill stager's smallest pass — the startup line names it, and below it every
|
||||||
|
routed layer reads its pooled experts synchronously, which costs the prefill far
|
||||||
|
more than sharing saves. Counts as `engine_prefill_chunks_capped_total`.
|
||||||
|
- `--max-num-batched-tokens` — the blunt rung, and the only one older pins have. The vendor's own
|
||||||
|
curve prices **halving the shared chunk at roughly a fifth of prefill throughput** (a 48-layer MoE
|
||||||
|
at 200k on two cards — knob pricing, not this endpoint's number, so treat it as the order of
|
||||||
|
magnitude a chunk decision costs). Raising it past the checkpoint interval buys nothing and loses
|
||||||
|
expert residency to the arena.
|
||||||
|
- `--prefill-span N` — the inverse trade: a *lone* prefill carries N intervals a step while every
|
||||||
|
shared step is planned at a smaller shared budget, which on an expert-tiered box lends that arena
|
||||||
|
back to the expert slab. The vendor's own measurement says the win is **long-prompt-only**: nothing
|
||||||
|
at short prompts, a large TTFT gain at the far end of the window.
|
||||||
|
|
||||||
|
Two traps. **The arena/slab coupling makes a shared step doubly expensive on an
|
||||||
|
expert-tiered shape**: the full-plan arena takes VRAM the resident expert slab would have had, so the
|
||||||
|
decode rows inside that step also read experts over the link — a smaller shared budget helps twice.
|
||||||
|
And **re-check
|
||||||
|
`prefix_hit_rate` / `prefix_evictions` after any chunk change**: the arena and the KV
|
||||||
|
pool are the same memory, and the failure looks like turn two taking as long as turn
|
||||||
|
one.
|
||||||
|
|
||||||
|
Availability is a version question, not a config question: `--busy-prefill-chunk` and
|
||||||
|
`--prefill-interleave` are absent from older pins — prove it on the running container
|
||||||
|
(`podman exec <c> <binary> --help | grep -iE "prefill|chunk"`), never from the vendored
|
||||||
|
tree, and check the source clone's depth before dating a flag from `git log -S`
|
||||||
|
(a `--depth 1` clone reports its own tip for every search).
|
||||||
|
|
||||||
|
Run on: [[frank-archon]] — the census was taken on its 3-slot radiance 1.0.13 era (budget 2048 =
|
||||||
|
interval 2048); the live 1.8.1 shape carries the two chunk knobs. The measured census and the verdict
|
||||||
|
for this fleet's shape are that page's.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[LLM Bench Windows and Measurement]] — the window discipline and usage-counter law
|
- the window discipline and usage-counter law
|
||||||
- [[PCIe Link and Atomics]] — link probing and the streaming-vs-compute ceiling
|
- [[PCIe Link and Atomics]] — link probing and the streaming-vs-compute ceiling
|
||||||
- [[RDNA4 MoE Expert Offload VRAM Budget]] — the plan behind duty-cycle terms
|
- [[RDNA4 MoE Expert Offload VRAM Budget]] — the plan behind duty-cycle terms
|
||||||
- [[vLLM Porting and Compile Triage]] — the compile fix for the pointwise-glue bound
|
- [[frank-archon]] — the endpoint where §9's step-shape census was run
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Llama Cpp Multi-Device Splits
|
|
||||||
description: "What llama.cpp can express across devices: the no-collective layer-split path, balancing, and drafter KV sizing."
|
|
||||||
tags: [llama.cpp, multi-gpu, layer-split, pcie, sizing, speculative]
|
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
|
||||||
---
|
|
||||||
|
|
||||||
# Llama Cpp Multi-Device Splits
|
|
||||||
|
|
||||||
## 1. Expressibility law — classify the arrangement before running anything
|
|
||||||
|
|
||||||
llama.cpp expresses exactly two things across devices, and they are not
|
|
||||||
interchangeable:
|
|
||||||
|
|
||||||
| arrangement | how | expressible? |
|
|
||||||
|---|---|---|
|
|
||||||
| **Layer split** — layers 0..n on dev A, rest on dev B | `-sm layer -ts <a>/<b>` | **yes**, no collectives needed; works cross-vendor (CUDA + Vulkan measured 2026-09-16) |
|
|
||||||
| **Per-tensor placement** — attention+KV on A, experts on B | `-ot <regex>=<dev>` | **NO.** `--override-tensor` sets `has_tensor_overrides()`, a REQUIRED term of the scheduler's `pipeline_parallel` flag (only read site: `llama-context.cpp`, in the `pipeline_parallel = …` expression). That flag sets `n_copies = parallel ? MAX : 1` in `ggml_backend_sched_new`; with `n_copies == 1` a pre-allocated weight on a non-primary device has no backend and the graph aborts at `ggml-backend.cpp:941` (`pre-allocated tensor (blk.0.ffn_down_exps.weight) in a buffer (Vulkan1) that cannot run the operation (NONE)` — `(NONE)` is the leaf's own op name). |
|
|
||||||
|
|
||||||
Upstream anchors, re-read 2026-10-18 against `master`:
|
|
||||||
`src/llama-context.cpp:517-526` computes `pipeline_parallel` with
|
|
||||||
`!model.has_tensor_overrides()` as a required term;
|
|
||||||
`ggml/src/ggml-backend.cpp:1089` carries the verbatim abort
|
|
||||||
`pre-allocated tensor (%s) in a buffer (%s) that cannot run the operation (%s)`
|
|
||||||
(the `:941` anchor above is where it sat in the `37b53fd` build this was
|
|
||||||
measured on); and `common/arg.cpp` defines `-sm/--split-mode` (2821),
|
|
||||||
`-fit/--fit` (2882), `--spec-type` (4295), `--spec-draft-device` (4259),
|
|
||||||
`--spec-draft-n-max` (4161), `-fa/--flash-attn` (1758) — every flag in §4 is
|
|
||||||
real upstream, not fork-only
|
|
||||||
([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/common/arg.cpp)).
|
|
||||||
|
|
||||||
Failure modes observed (mainline `37b53fd`): per-tensor `-ot exps=Vulkan1`
|
|
||||||
with `-dev CUDA0,Vulkan1` → scheduler abort; layer-scoped `-ot` with a
|
|
||||||
cross-backend pair → model load fails with only `failed to load model`;
|
|
||||||
`-ot x=CPU` **works** because the CPU backend is last in the list — **the CPU
|
|
||||||
is the only legitimate partner in a heterogeneous split.** On a CUDA+AMD host
|
|
||||||
a card can be the trunk OR the offload host, never the complement of an
|
|
||||||
AMD-resident peer. Pure Vulkan changes nothing here — the gate is the
|
|
||||||
override, not the driver mix.
|
|
||||||
|
|
||||||
If the plan needs tensors on a specific device: stop, the answer is no for
|
|
||||||
any vendor mix. Expressible alternatives, in order: (1) two independent
|
|
||||||
endpoints, one model per device; (2) single-vendor layer split; (3) device +
|
|
||||||
CPU per-tensor offload (mainline pinning guard: `GGML_CUDA_NO_PINNED`; ik fork
|
|
||||||
was `GGML_CUDA_NO_PINNED_WEIGHTS` — without it a CPU-overridden tensor can
|
|
||||||
land in a pinned `CUDA_Host` buffer and page-lock ~108 GiB).
|
|
||||||
|
|
||||||
Layer-split arithmetic to check before promising: a whole layer carries its
|
|
||||||
experts (MoE) — compute GiB/layer, not per param-group; only some layers carry
|
|
||||||
KV (every 4th on the hybrid), so a share of layers is only a share of KV; big
|
|
||||||
non-repeating tensors (PLE tables) land on the main device and cannot move —
|
|
||||||
they cap how small the primary can get.
|
|
||||||
|
|
||||||
## 2. No collectives? The atomics wall and the layer-split answer
|
|
||||||
|
|
||||||
Decide with one line before designing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo lspci -vv -s <card2> | grep AtomicOpsCtl # ReqEn- ⇒ no collectives, ever
|
|
||||||
lspci -tv # a Routing- bridge is a hardware wall
|
|
||||||
```
|
|
||||||
|
|
||||||
`ReqEn-` kills vLLM entirely — TP needs a per-layer allreduce AND PP needs a
|
|
||||||
device-backend `send`/`recv` of the hidden state
|
|
||||||
(`vllm/distributed/parallel_state.py`) — i.e. the same RCCL primitive that
|
|
||||||
fails on an atomics-less card. llama.cpp's `--split-mode layer` needs no
|
|
||||||
collective: layers distribute in order, only the boundary activation crosses
|
|
||||||
PCIe once per forward pass. Full probe/repair detail:
|
|
||||||
[[PCIe Link and Atomics]].
|
|
||||||
|
|
||||||
## 3. Build: pin by feature, bundle the runtime at the exact depth
|
|
||||||
|
|
||||||
- Pin the ref by FEATURE list (e.g. DFlash2 + qwen35 MTP on 2026-09-16 →
|
|
||||||
`ggml-org/llama.cpp#27342`); never float. Verify a fork build the same way
|
|
||||||
(`ls <venv>/…/spec_decode/ # dflash/ vs dflash2/`).
|
|
||||||
- `-DCMAKE_HIP_ARCHITECTURES=<gfx only>` — a sibling arch links, loads, then
|
|
||||||
segfaults. `-DGGML_HIP_RCCL=OFF` when there is no atomics path (only tensor
|
|
||||||
mode degrades to the butterfly fallback; layer split unaffected).
|
|
||||||
- **TRAP — `cp -r SRC DST/` flattens when `DST` does not exist** and drops one
|
|
||||||
directory level. rocBLAS/hipBLASLt look for device code at a FIXED depth
|
|
||||||
(`../lib/rocblas/library`, `../lib/hipblaslt/library/<arch>`); flattened,
|
|
||||||
the engine loads every GiB, answers `/health`, and dies on the FIRST request
|
|
||||||
with `Cannot read .../TensileLibrary.dat: No such file or directory for GPU
|
|
||||||
arch`. `mkdir -p DST && cp -r SRC/. DST/`, then
|
|
||||||
`patchelf --set-rpath '$ORIGIN/../lib' build/bin/llama-*`.
|
|
||||||
- Dual-backend build (CUDA + Vulkan is the practical pair; HIP+CUDA do not
|
|
||||||
coexist): `-DGGML_CUDA=ON -DGGML_VULKAN=ON` in one tree; Vulkan needs
|
|
||||||
`glslc` + libvulkan headers. **Device names are not what you expect:**
|
|
||||||
`--list-devices` lists the discrete card under BOTH backends (`CUDA0` AND
|
|
||||||
`Vulkan0`) and the APU LAST (`Vulkan1`) — a wrong `-ot` name makes
|
|
||||||
llama.cpp print the valid set and exit (self-diagnosing).
|
|
||||||
- What the existing build can do: `GPU_BACKEND_*=OFF` lines in its
|
|
||||||
CMakeCache.
|
|
||||||
|
|
||||||
## 4. Serve flags that matter on a split
|
|
||||||
|
|
||||||
```bash
|
|
||||||
HIP_VISIBLE_DEVICES=0,1 llama-server \
|
|
||||||
--device ROCm0,ROCm1 --split-mode layer --tensor-split 1,1 \
|
|
||||||
--gpu-layers 999 --fit off --flash-attn on \
|
|
||||||
--ctx-size <n> --parallel 1 --cache-type-k f16 --cache-type-v f16 \
|
|
||||||
--jinja --metrics --cache-prompt \
|
|
||||||
--spec-type draft-dflash --spec-draft-model <dflash2.gguf> \
|
|
||||||
--spec-draft-device ROCm1 --spec-draft-n-max 7
|
|
||||||
```
|
|
||||||
|
|
||||||
- `--fit off` + explicit `--ctx-size`: auto-fit must not resize a context set
|
|
||||||
on purpose. **`--parallel N` DIVIDES `--ctx-size`** — one slot at large ctx
|
|
||||||
is the concurrency policy, concurrent agent loops queue.
|
|
||||||
- Pin the drafter to the card holding the target's LAST layers
|
|
||||||
(`--spec-draft-device`); under tensor mode an unpinned draft joins the
|
|
||||||
meta-device KV allocation and aborts the KV build. `--spec-draft-n-max` must
|
|
||||||
be ≤ the drafter's `block_size - 1` (DFlash2: 8 ⇒ 7).
|
|
||||||
- Qwen3.8-family templates RAISE on `reasoning_effort=high` (500) — probe
|
|
||||||
low/medium/high/xhigh and map harness levels before declaring ready.
|
|
||||||
|
|
||||||
## 5. Split balancing: measure, never interpolate
|
|
||||||
|
|
||||||
**A `-ts` step is ONE LAYER, and hybrid stacks do not have uniform layers.**
|
|
||||||
qwen35 is 16 × (3 Gated DeltaNet + 1 Gated Attention): one step ≈ 0.43 GiB on
|
|
||||||
a DeltaNet boundary, ≈ 1.43 GiB on a full-attention one (weights + that
|
|
||||||
layer's own f16 KV) — measured on frank-medium. The gap can jump 2.9 GiB in a
|
|
||||||
single step; the best integer split can sit 0.2 GiB from balance while its
|
|
||||||
neighbours are 1+ GiB off. An affine fit reproduces the deployed point and
|
|
||||||
overstates gaps in between (`tests/frank_medium_unit.bats` encodes the fit as
|
|
||||||
a gate; treat a NEGATIVE fitted constant as a unit bug).
|
|
||||||
|
|
||||||
Knobs that move weight and REQUIRE a re-measure: `--ubatch-size` (compute
|
|
||||||
buffers are **~2x heavier on the output/drafter card** — 512→1536 added
|
|
||||||
~3.3 GiB there vs ~1.7 GiB elsewhere), a drafter swap (weights + draft KV),
|
|
||||||
`--ctx-size`/`--parallel` (KV). Total free VRAM is invariant across splits —
|
|
||||||
only the distribution moves; use that to sanity-check a suspicious row.
|
|
||||||
|
|
||||||
**Procedure (one split per load):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
WORK=/tmp/split.env; cp scripts/serve-<svc>.env $WORK
|
|
||||||
export FRANK_MEDIUM_ENV_FILE=$WORK
|
|
||||||
for ts in 39,26 38,27 37,28 36,29 35,30; do
|
|
||||||
sed -i "s|^TENSOR_SPLIT=.*|TENSOR_SPLIT=$ts|" $WORK
|
|
||||||
nohup bash scripts/serve-<svc>.sh >/tmp/split.log 2>&1 &
|
|
||||||
# poll /health (or process exit), then:
|
|
||||||
rocm-smi --showmeminfo vram | grep 'VRAM Total Used' # sample ~6 s after health
|
|
||||||
pkill -x llama-server; sleep 6
|
|
||||||
done
|
|
||||||
```
|
|
||||||
|
|
||||||
Load → sample → kill, NO requests sent (allocation only). Run under `nohup`
|
|
||||||
and poll from a separate call (a load is 30–90 s). Pick the smallest
|
|
||||||
inter-card gap that clears the per-card floor with room to spare; one `-ts`
|
|
||||||
step ≈ 1.5 GiB at ~6k batch / 262k ctx on 30 GiB cards — enough to bracket,
|
|
||||||
not to solve. Prefer balance: the busy card's headroom bounds the context
|
|
||||||
(`ctx_extra ≈ (floor − measured_free) / per-position cost on the busier
|
|
||||||
card`). A faulted split (`SDMA0 UTCL2 page fault` → `illegal memory access` →
|
|
||||||
`ggml_abort`) is OFF-LIMITS — re-measure, never re-tune; more headroom does
|
|
||||||
not prevent a boundary-copy fault.
|
|
||||||
|
|
||||||
## 6. Drafter KV: sized from the TARGET, bounded by its window policy
|
|
||||||
|
|
||||||
`common/speculative.cpp` builds the draft context with
|
|
||||||
`cparams.n_ctx = llama_n_ctx(ctx_tgt)` — `common/speculative.cpp:2663` on
|
|
||||||
`master`, re-read 2026-10-18 — **no flag caps it.** So:
|
|
||||||
|
|
||||||
```
|
|
||||||
draft KV = ctx_tgt × block_count × 2 (K,V) × kv_heads × key_length × dtype
|
|
||||||
```
|
|
||||||
|
|
||||||
…unless the drafter declares a sliding window. Measured at 262,144 target
|
|
||||||
ctx: DFlash2 (`sliding_window = 2048`) ≈ 40 MiB; a same-shape 5-layer drafter
|
|
||||||
with NO window = 20 KiB/position = **5.0 GiB** — a four-layer swing in the
|
|
||||||
balance point, and swapping them invalidated an already-balanced split.
|
|
||||||
|
|
||||||
**Failure signature:** the engine loads the whole model, answers `/health`,
|
|
||||||
and dies on the FIRST draft decode with `ROCm error: out of memory` /
|
|
||||||
`cudaMalloc failed` inside `common_speculative_impl_draft_dflash::process` —
|
|
||||||
a sizing failure, not a broken drafter; under a self-healing unit it presents
|
|
||||||
as a crash-loop. Read `sliding_window`, `block_count`, `head_count_kv`,
|
|
||||||
`key_length` from the drafter's GGUF header first. A compact reader (the
|
|
||||||
three format traps: no KV alignment — v3 alignment applies only to the tensor
|
|
||||||
section; arrays are `type(9) → element_type(u32) → count(u64)`, element type
|
|
||||||
FIRST; the enum is `7=BOOL 9=ARRAY`, 64-bit = 10/11/12, and 8=STRING/9=ARRAY
|
|
||||||
is the v2/v3 file order, NOT gguf.h's 2/3):
|
|
||||||
|
|
||||||
```python
|
|
||||||
import struct, sys
|
|
||||||
def rd(path):
|
|
||||||
f = open(path, 'rb'); assert f.read(4) == b'GGUF'
|
|
||||||
struct.unpack('<I', f.read(4)); struct.unpack('<Q', f.read(8))
|
|
||||||
nk = struct.unpack('<Q', f.read(8))[0]
|
|
||||||
def s():
|
|
||||||
n = struct.unpack('<Q', f.read(8))[0]; return f.read(n).decode('utf-8', 'replace')
|
|
||||||
SCALAR = {0:1,1:1,2:2,3:2,4:4,5:4,6:4,7:1,10:8,11:8,12:8}
|
|
||||||
FMT = {0:'<B',1:'<b',2:'<H',3:'<h',4:'<I',5:'<i',6:'<f',7:'<?',10:'<Q',11:'<q',12:'<d'}
|
|
||||||
keys = {}
|
|
||||||
for _ in range(nk):
|
|
||||||
k = s(); t = struct.unpack('<I', f.read(4))[0]
|
|
||||||
if t == 8: keys[k] = s()
|
|
||||||
elif t in SCALAR: keys[k] = struct.unpack(FMT[t], f.read(SCALAR[t]))[0]
|
|
||||||
else:
|
|
||||||
et = struct.unpack('<I', f.read(4))[0]; n = struct.unpack('<Q', f.read(8))[0]
|
|
||||||
if et == 8: keys[k] = [s() for _ in range(n)]
|
|
||||||
elif et in SCALAR: keys[k] = ('arr', et, n); f.read(n * SCALAR[et])
|
|
||||||
else: raise Exception('unsupported arr elem type %d' % et)
|
|
||||||
return keys
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Heterogeneous placement triage (go/no-go with arithmetic)
|
|
||||||
|
|
||||||
- **Link:** `LnkCap` vs `LnkSta` at every hop; expect **idle-downshift** (a
|
|
||||||
Gen4 x4 dock idles at Gen1 x4, ~1 GB/s) — measure under load. Per-hop cost
|
|
||||||
vs hidden state (5120-dim f16 = 10 KiB/hop ≈ 1.3 µs at Gen4 x4): bytes are
|
|
||||||
almost never the constraint; **hop count is** — every boundary is a host
|
|
||||||
sync, and a per-op-split runtime can hit 100 splits/token at ~5 % SM. Read
|
|
||||||
`graph splits` from the loader log.
|
|
||||||
- **KV is usually the bottleneck:** `KV/token = kv_bytes_per_position ×
|
|
||||||
context` (attention reads the whole cache every token). Highest-leverage
|
|
||||||
placement: **KV on the fastest-memory device together with its attention
|
|
||||||
compute** — never KV alone over the link (GBs over 7.88 GB/s = hundreds of
|
|
||||||
ms/token). Check fit: `kv_positions × slots × bytes + attention_trunk ≤
|
|
||||||
VRAM`.
|
|
||||||
- **What placement never buys:** prefill is compute-bound and expert weights
|
|
||||||
are read once per batch — moving them buys nothing and adds a serial stage.
|
|
||||||
Calibration: Strix Halo + 3090, moving 12.5 % of expert mass to the GPU
|
|
||||||
gave decode +12 % but prefill 192 → 32 tok/s. The harmonic
|
|
||||||
`1/(f_big/BW_slow + f_fast/BW_fast)` reproduces the decode gain within ~2
|
|
||||||
points (assumes sequential stages — true at batch 1).
|
|
||||||
- **UMA check before assuming additivity:** `mem_info_vram_total` (BIOS
|
|
||||||
carve-out) vs `mem_info_gtt_total` (aperture onto SYSTEM RAM). A big GTT
|
|
||||||
window means the "GPU memory" IS the same LPDDR: adding a stage there adds
|
|
||||||
a compute engine, not capacity/bandwidth. Only discrete VRAM is additive.
|
|
||||||
- **Speculation as the "cheap" split:** bandwidth-free (token ids per round)
|
|
||||||
but needs the exact tokenizer for a plain drafter or a head trained for
|
|
||||||
that target; a plain drafter costs K sequential forwards (break-even needs
|
|
||||||
p ≈ 0.7+ at r≈0.4–0.5, K=3); compare against the INCUMBENT (a target
|
|
||||||
already committing ~1.7 tok/round on its own MTP head is a high bar); and
|
|
||||||
check the runtime accepts external drafters at all (closed engines 400
|
|
||||||
unknown drafter names by design). Details: [[DFlash DSpark Speculative Decoding]].
|
|
||||||
- **Hazards:** keep paged/sidecar tables where they are read from; keep mmap
|
|
||||||
ON for huge-embedding models (anonymous memory → cgroup kill); CUDA graph
|
|
||||||
capture can abort on unusual graph shapes; make the FIRST measurement a
|
|
||||||
single-device baseline of the runtime on the new device — a split's
|
|
||||||
arithmetic is worthless if the baseline loses to the incumbent; a split
|
|
||||||
consumes the fast device entirely — check what it was serving.
|
|
||||||
|
|
||||||
**Verdict ranking:** (1) two independent endpoints — often the real answer;
|
|
||||||
(2) one process, weights-resident, KV co-located with attention on the fast
|
|
||||||
pool; (3) hot-expert cache only if `misses × bytes × tok/s` fits the link and
|
|
||||||
overlaps — no mainstream runtime implements it; (4) name what does NOT help:
|
|
||||||
TP across unequal devices, KV on the far device, loading over the link,
|
|
||||||
prefill-motivated placements.
|
|
||||||
|
|
||||||
## 8. Harness syntax and verification
|
|
||||||
|
|
||||||
`llama-bench` separates a LIST with `/` and SWEEP values with `,`:
|
|
||||||
`-dev CUDA0,Vulkan1` is TWO single-device runs, never one two-device run;
|
|
||||||
`-ts 0.85,0.15` is two sweep values each becoming `[0.85, 0, …]` — device 0
|
|
||||||
takes everything. Write `-dev A/B -ts x/y`. Read the table's own `dev` column;
|
|
||||||
any placement-dependent number needs an independent memory check
|
|
||||||
(`nvidia-smi` sampling while loaded). (This artifact produced a wrong fleet
|
|
||||||
conclusion 2026-09-16 — "CUDA + Vulkan cannot serve one model" — with two
|
|
||||||
bogus OOMs that were really all-111-GiB-on-the-24-GiB-card.)
|
|
||||||
|
|
||||||
Verification battery: `/health`; both cards' memory ≈ half the weights each
|
|
||||||
(busier card also carries the drafter; compare `model_bytes/2 + drafter +
|
|
||||||
ctx×KV/2 + buffers`); raise ctx by N and watch BOTH cards rise by
|
|
||||||
N·bytes/2 — neither card's total may equal the model file (that arithmetic
|
|
||||||
rules out replication); `spec_decode_num_drafts_total` > 0 and a real
|
|
||||||
completion; a deep prompt (140k) proves a raised window is REAL, not
|
|
||||||
declared; repeat prompt → `timings.prompt_n` collapses (prefix cache live);
|
|
||||||
`/slots` shows expected `(id, n_ctx)` pairs; two concurrent requests with
|
|
||||||
`journalctl -o cat | grep 'slot print_timing'` proves drafting on BOTH slots
|
|
||||||
(default journal output truncates these lines).
|
|
||||||
|
|
||||||
Ops traps: a server started over ssh outlives stopping the runner (keeps the
|
|
||||||
port — `pkill -x <binary>` first); `systemctl start` of a root unit needs
|
|
||||||
sudo (polkit "Access denied" otherwise); old crash `ROCm error` lines live in
|
|
||||||
the journal forever — check `ActiveEnterTimestamp`/`NRestarts` before
|
|
||||||
treating a grep count as a live fault.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [[PCIe Link and Atomics]] — the atomics wall, link probing, repair
|
|
||||||
- [[Llama Cpp Serving Sizing]] — slot-pool and KV pricing
|
|
||||||
- [[DFlash DSpark Speculative Decoding]] — spec types, chaining, A/B
|
|
||||||
- [[frank-medium-llamacpp-dflash2]] — the layer-split era runbook
|
|
||||||
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — the measured history
|
|
||||||
@@ -19,7 +19,7 @@ guesswork.
|
|||||||
`n_ctx_slot = CTX_SIZE / PARALLEL`. A hybrid model with
|
`n_ctx_slot = CTX_SIZE / PARALLEL`. A hybrid model with
|
||||||
`kv_unified = 'false'` gives each slot its own cache AND a fixed per-slot
|
`kv_unified = 'false'` gives each slot its own cache AND a fixed per-slot
|
||||||
state buffer — slot count is a memory term, not free.
|
state buffer — slot count is a memory term, not free.
|
||||||
2. **Two boots on the real card** (production stopped, per the bench law),
|
2. **Two boots on the real card** (production stopped, per the bench law)
|
||||||
each near the target; record `nvidia-smi` used/total plus the boot log's
|
each near the target; record `nvidia-smi` used/total plus the boot log's
|
||||||
`n_slots`/`n_ctx_slot`: shape A = current/cheap, shape B = new demand.
|
`n_slots`/`n_ctx_slot`: shape A = current/cheap, shape B = new demand.
|
||||||
`non-KV = used − (tokens × KiB/token)`; the two rows separate
|
`non-KV = used − (tokens × KiB/token)`; the two rows separate
|
||||||
@@ -31,7 +31,7 @@ guesswork.
|
|||||||
slot gets an equal integer window. Boot and measure; if outside the band,
|
slot gets an equal integer window. Boot and measure; if outside the band,
|
||||||
walk one step — never re-derive from a formula.
|
walk one step — never re-derive from a formula.
|
||||||
4. **Propagate the per-slot number — it is the client contract:**
|
4. **Propagate the per-slot number — it is the client contract:**
|
||||||
`models.yml` `contextWindow` = `n_ctx_slot` (NOT the model's native window),
|
`models.yml` `contextWindow` = `n_ctx_slot` (NOT the model's native window)
|
||||||
or long conversations die with `400 exceed_context_size_error` instead of
|
or long conversations die with `400 exceed_context_size_error` instead of
|
||||||
compacting; compaction/summary budgets sized to the old window get
|
compacting; compaction/summary budgets sized to the old window get
|
||||||
re-checked; endpoint docs and the rollback table move too.
|
re-checked; endpoint docs and the rollback table move too.
|
||||||
@@ -66,13 +66,13 @@ co-tenants.
|
|||||||
- **Engine ledger first.** Halogen prints weights pinned (+ remaining 2 MiB
|
- **Engine ledger first.** Halogen prints weights pinned (+ remaining 2 MiB
|
||||||
contiguous blocks — the honest fragmentation signal: a block census that drops
|
contiguous blocks — the honest fragmentation signal: a block census that drops
|
||||||
across a restart means the model did not come back into the same hole; the
|
across a restart means the model did not come back into the same hole; the
|
||||||
before/after census this was learned on is that era's, on [[frank-dragoon]]),
|
before/after census this was learned on is that era's, on [[frank-dragoon]])
|
||||||
device budget, KV pool/working split, "left for everything
|
device budget, KV pool/working split, "left for everything
|
||||||
else". llama.cpp prints them at `llama-model.cpp:1827` (per-backend model buffer)
|
else". llama.cpp prints them at `llama-model.cpp:1827` (per-backend model buffer)
|
||||||
and `llama-context.cpp:692` (compute buffer) on the era's build; on today's
|
and `llama-context.cpp:692` (compute buffer) on the era's build; on today's
|
||||||
`master` the same `LLAMA_LOG_INFO` lines sit at `llama-model.cpp:2009`
|
`master` the same `LLAMA_LOG_INFO` lines sit at `llama-model.cpp:2009`
|
||||||
("model buffer size =") and `llama-context.cpp:842` ("compute buffer size =")
|
("model buffer size =") and `llama-context.cpp:842` ("compute buffer size =")
|
||||||
([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/src/llama-context.cpp),
|
([source](https://raw.githubusercontent.com/ggml-org/llama.cpp/master/src/llama-context.cpp)
|
||||||
re-read 2026-10-18) — but **`llama-bench`
|
re-read 2026-10-18) — but **`llama-bench`
|
||||||
suppresses loader INFO and a systemd unit did not put them in the journal**
|
suppresses loader INFO and a systemd unit did not put them in the journal**
|
||||||
(verified `grep -c` = 0). Recipe: one launch through `llama-cli`/`llama-server`
|
(verified `grep -c` = 0). Recipe: one launch through `llama-cli`/`llama-server`
|
||||||
@@ -92,7 +92,6 @@ co-tenants.
|
|||||||
projector, never weights alone. Do
|
projector, never weights alone. Do
|
||||||
the capacity subtraction before designing, on the card number the owner page
|
the capacity subtraction before designing, on the card number the owner page
|
||||||
quotes, not the advertised one.
|
quotes, not the advertised one.
|
||||||
- **Context lengths: derive, never remember.** `llama-bench` computes
|
|
||||||
`n_ctx = n_prompt + n_gen + n_depth` (defaults 512/128/0, type_k/v F16,
|
`n_ctx = n_prompt + n_gen + n_depth` (defaults 512/128/0, type_k/v F16,
|
||||||
flash_attn AUTO, split_mode LAYER). `-p 512,16384 -n 64` = contexts
|
flash_attn AUTO, split_mode LAYER). `-p 512,16384 -n 64` = contexts
|
||||||
576/16,448; a plain `tg` row has ~zero depth — `-d` is the only thing that
|
576/16,448; a plain `tg` row has ~zero depth — `-d` is the only thing that
|
||||||
@@ -116,7 +115,7 @@ of the whole sequence + constant recurrent state + the draft context's KV:
|
|||||||
`per_token = 2 × n_full_attn_layers × n_kv_heads × head_dim × bytes (K and V) +
|
`per_token = 2 × n_full_attn_layers × n_kv_heads × head_dim × bytes (K and V) +
|
||||||
draft_per_token`. The worked qwen35-hybrid instance of that formula (full layer
|
draft_per_token`. The worked qwen35-hybrid instance of that formula (full layer
|
||||||
count, f16 KV, MTP draft) and the slot-size table it prices are on
|
count, f16 KV, MTP draft) and the slot-size table it prices are on
|
||||||
[[frank-medium-llamacpp-dflash2]]; cross-check any such arithmetic against the
|
; cross-check any such arithmetic against the
|
||||||
journal's `prompt state size` lines — the two must agree.
|
journal's `prompt state size` lines — the two must agree.
|
||||||
|
|
||||||
**The journal is the instrument:** `exceeds cache size limit … skipping` =
|
**The journal is the instrument:** `exceeds cache size limit … skipping` =
|
||||||
@@ -171,6 +170,6 @@ part of that era's record on [[frank-dragoon]].
|
|||||||
|
|
||||||
- [[GGUF Compatibility]] — what a payload is before it is priced
|
- [[GGUF Compatibility]] — what a payload is before it is priced
|
||||||
- [[frank-zealot]] — the endpoint whose Eras rows are the two-boot priced shapes this method produced
|
- [[frank-zealot]] — the endpoint whose Eras rows are the two-boot priced shapes this method produced
|
||||||
- [[frank-medium-llamacpp-dflash2]] — the state-law worked example behind section 3
|
- the state-law worked example behind section 3
|
||||||
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
|
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
|
||||||
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — the multi-device side
|
- / — the multi-device side
|
||||||
@@ -51,9 +51,9 @@ express; per-tensor cross-device placement stays refused under
|
|||||||
## 2. Ground truth first
|
## 2. Ground truth first
|
||||||
|
|
||||||
- Read the HF model card (runtime pin, hard constraints, recommended args)
|
- Read the HF model card (runtime pin, hard constraints, recommended args)
|
||||||
+ the repo trio `scripts/<svc>.env`, `scripts/serve-<svc>.sh`,
|
+ the repo trio `scripts/<svc>.env`, `scripts/serve-<svc>.sh`
|
||||||
`scripts/<svc>.service` + the runbook under `serverAppConfigs-docs/LLM/`.
|
`scripts/<svc>.service` + the runbook under `serverAppConfigs-docs/LLM/`.
|
||||||
- Check the host before downloading anything: `ls /home/dank/models/`,
|
- Check the host before downloading anything: `ls /home/dank/models/`
|
||||||
`df -h /home`, `du -sh` — bench-era or prior-deployment files often already
|
`df -h /home`, `du -sh` — bench-era or prior-deployment files often already
|
||||||
exist.
|
exist.
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ arms possible — keep it intact.
|
|||||||
`images` is silently ignored; probe with ≥256 px).
|
`images` is silently ignored; probe with ≥256 px).
|
||||||
- vLLM specifics: first request after boot can take ~18 s (deferred Triton
|
- vLLM specifics: first request after boot can take ~18 s (deferred Triton
|
||||||
loads) — warm and discard; `--reasoning-parser` / tool parsers are part of
|
loads) — warm and discard; `--reasoning-parser` / tool parsers are part of
|
||||||
the contract ([[vLLM Boot Forensics and Config Interrogation]]).
|
the contract ().
|
||||||
|
|
||||||
## 7. systemd handoff — THE TRAP
|
## 7. systemd handoff — THE TRAP
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Pure measurement sessions that changed no state need only pin 1.
|
|||||||
ladder UD-IQ1_S 93.1 GB → UD-Q4_K_XL 199.7 GB, GSQ-RCO 3.5-bit −1.40 pp
|
ladder UD-IQ1_S 93.1 GB → UD-Q4_K_XL 199.7 GB, GSQ-RCO 3.5-bit −1.40 pp
|
||||||
MMLU-Pro vs Q8_0).
|
MMLU-Pro vs Q8_0).
|
||||||
- GGUF parsing gotcha: expert tensors are
|
- GGUF parsing gotcha: expert tensors are
|
||||||
`blk.N.ffn_{gate,up,down}_exps.weight` — 4 dot-parts INCLUDING `.weight`;
|
`blk.N.ffn_{gate,up,down}_exps.weight` — 4 dot-parts INCLUDING `.weight`
|
||||||
a `len(parts) == 3` filter matches zero layers with no parse error. Filter
|
a `len(parts) == 3` filter matches zero layers with no parse error. Filter
|
||||||
on `parts[0]=="blk" and parts[3]=="weight" and "exps" in parts[2]`.
|
on `parts[0]=="blk" and parts[3]=="weight" and "exps" in parts[2]`.
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ Pure measurement sessions that changed no state need only pin 1.
|
|||||||
When a host must serve two models at once (an iGPU runtime plus a second
|
When a host must serve two models at once (an iGPU runtime plus a second
|
||||||
card), or one endpoint is moving and a new one takes the freed hardware.
|
card), or one endpoint is moving and a new one takes the freed hardware.
|
||||||
|
|
||||||
**0. Decide the shape before touching hardware.** One provider per
|
**0. Decide the shape before touching hardware.** One provider
|
||||||
HOST:PORT in `~/.omp/agent/models.yml` (on the box where the harness runs)
|
HOST:PORT in `~/.omp/agent/models.yml` (on the box where the harness runs)
|
||||||
— a second endpoint is a new provider block, not a second model in the
|
— a second endpoint is a new provider block, not a second model in the
|
||||||
existing one. Read the budget as an inequality: for unified memory the
|
existing one. Read the budget as an inequality: for unified memory the
|
||||||
@@ -235,7 +235,7 @@ suffix — and check what pointed at them: bench harnesses, a chat template
|
|||||||
only the old runtime read, and `scripts/test-changed.sh` mappings are the
|
only the old runtime read, and `scripts/test-changed.sh` mappings are the
|
||||||
usual victims (a mapping to a retired test file fails the pre-commit
|
usual victims (a mapping to a retired test file fails the pre-commit
|
||||||
wrapper silently for weeks). One bats file per service asserting
|
wrapper silently for weeks). One bats file per service asserting
|
||||||
INVARIANTS (`pool == 2 × ctx`, KV bytes ≤ card budget, port separation),
|
INVARIANTS (`pool == 2 × ctx`, KV bytes ≤ card budget, port separation)
|
||||||
not constants; a negative grep must strip comments first
|
not constants; a negative grep must strip comments first
|
||||||
(`grep -vE '^\s*#' file | grep -q …`) or it matches the prose explaining
|
(`grep -vE '^\s*#' file | grep -q …`) or it matches the prose explaining
|
||||||
the very thing you removed.
|
the very thing you removed.
|
||||||
@@ -258,12 +258,12 @@ target — including whether `/metrics` answers — is [[frank-dragoon]]'s wire
|
|||||||
contract.
|
contract.
|
||||||
|
|
||||||
**4. Cutover, then verify by driving it.** `deploy.sh <target> --service
|
**4. Cutover, then verify by driving it.** `deploy.sh <target> --service
|
||||||
<large> --service <small>` (order matters); per endpoint: `/health`,
|
<large> --service <small>` (order matters); per endpoint: `/health`
|
||||||
`/v1/models` reports the alias, a chat completion returns content AND
|
`/v1/models` reports the alias, a chat completion returns content AND
|
||||||
`reasoning_content`, a `tools` request returns `tool_calls`, an image
|
`reasoning_content`, a `tools` request returns `tool_calls`, an image
|
||||||
request is answered (or refused with a named reason), the runtime's own
|
request is answered (or refused with a named reason), the runtime's own
|
||||||
speculation counters move, and the unit is `enabled` and `active`. All three LLM
|
speculation counters move, and the unit is `enabled` and `active`. All three LLM
|
||||||
endpoints are **root** units today (`scripts/frank-archon.service`,
|
endpoints are **root** units today (`scripts/frank-archon.service`
|
||||||
`scripts/frank-dragoon.service`, `scripts/frank-zealot.service`), so linger is
|
`scripts/frank-dragoon.service`, `scripts/frank-zealot.service`), so linger is
|
||||||
not what gates them; linger matters for rootless quadlets. Read the engine's boot
|
not what gates them; linger matters for rootless quadlets. Read the engine's boot
|
||||||
ledger for a silent clamp. Finally spend one
|
ledger for a silent clamp. Finally spend one
|
||||||
@@ -282,6 +282,6 @@ predecessor "stopped" (the port race above).
|
|||||||
- [[GGUF Compatibility]] — which runtime can load a payload at all
|
- [[GGUF Compatibility]] — which runtime can load a payload at all
|
||||||
- [[Llama Cpp Serving Sizing]] — pricing the shape once it loads
|
- [[Llama Cpp Serving Sizing]] — pricing the shape once it loads
|
||||||
- [[RDNA4 MoE Expert Offload VRAM Budget]] — vLLM-side fit arithmetic
|
- [[RDNA4 MoE Expert Offload VRAM Budget]] — vLLM-side fit arithmetic
|
||||||
- [[LLM Bench Windows and Measurement]] — measuring the new deployment
|
- measuring the new deployment
|
||||||
* [[frank-archon]] — the live Dankserver endpoint; the tcclaviger vLLM era
|
* [[frank-archon]] — the live Dankserver endpoint; the tcclaviger vLLM era
|
||||||
(2026-09-29 → 10-05) is a row in its Eras table
|
(2026-09-29 → 10-05) is a row in its Eras table
|
||||||
@@ -61,13 +61,13 @@ H2D rate`; `ceiling = 1 / link_time × resident-fraction saved (LRU hits)`.
|
|||||||
Compare against the compute-in-RAM path at host RAM bandwidth (256 GB/s
|
Compare against the compute-in-RAM path at host RAM bandwidth (256 GB/s
|
||||||
LPDDR5X on the Strix Halo — 128 GB LPDDR5x-8000 on a 256-bit bus,
|
LPDDR5X on the Strix Halo — 128 GB LPDDR5x-8000 on a 256-bit bus,
|
||||||
[amd.com spec page](https://www.amd.com/en/products/processors/laptop/ryzen/ai-300-series/amd-ryzen-ai-max-plus-395.html)).
|
[amd.com spec page](https://www.amd.com/en/products/processors/laptop/ryzen/ai-300-series/amd-ryzen-ai-max-plus-395.html)).
|
||||||
**If the link is >10x slower than the memory behind it (x4 is 31x here),
|
**If the link is >10x slower than the memory behind it (x4 is 31x here)
|
||||||
streaming loses by construction and no tuning rescues it; doubling the link
|
streaming loses by construction and no tuning rescues it; doubling the link
|
||||||
only doubles a losing number.**
|
only doubles a losing number.**
|
||||||
|
|
||||||
Worked example: the Dankclaw Gen4 x4 run of 2026-09-16 — measured H2D/D2H
|
Worked example: the Dankclaw Gen4 x4 run of 2026-09-16 — measured H2D/D2H
|
||||||
rates, the 3 MiB slab cost, and the streaming-vs-compute comparison it decided
|
rates, the 3 MiB slab cost, and the streaming-vs-compute comparison it decided
|
||||||
— is recorded on [[x4-pair-experiment]] §The link, measured. Read it once and
|
— is recorded on §The link, measured. Read it once and
|
||||||
apply the rule above: on that link the streaming design lost by 3–5x, and the
|
apply the rule above: on that link the streaming design lost by 3–5x, and the
|
||||||
x8 ceiling would only have halved the loss.
|
x8 ceiling would only have halved the loss.
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ time, so encode them:
|
|||||||
+ `hipSetDevice` only. First line: `setvbuf(stdout, NULL, _IONBF, 0);` — a
|
+ `hipSetDevice` only. First line: `setvbuf(stdout, NULL, _IONBF, 0);` — a
|
||||||
segfaulting process loses block-buffered stdout and hides the crash point.
|
segfaulting process loses block-buffered stdout and hides the crash point.
|
||||||
- Build with an explicit arch: `HIP_TARGETS=gfx1201 hipcc -O2
|
- Build with an explicit arch: `HIP_TARGETS=gfx1201 hipcc -O2
|
||||||
--offload-arch=gfx1201 -o p p.hip`; compile with `| grep -E "error:"`,
|
--offload-arch=gfx1201 -o p p.hip`; compile with `| grep -E "error:"`
|
||||||
never `| tail -N` (truncation hid a missing `#include <cstring>`).
|
never `| tail -N` (truncation hid a missing `#include <cstring>`).
|
||||||
- `hipMemset`/`hipDeviceSynchronize` are `[nodiscard]` — use or cast.
|
- `hipMemset`/`hipDeviceSynchronize` are `[nodiscard]` — use or cast.
|
||||||
- Mount the whole `/dev/dri` into ROCm containers; individual `renderD*`
|
- Mount the whole `/dev/dri` into ROCm containers; individual `renderD*`
|
||||||
@@ -109,28 +109,28 @@ static double one(size_t bytes, bool pinned, int iters) {
|
|||||||
if (hipMalloc(&dev, bytes) != hipSuccess) return -1;
|
if (hipMalloc(&dev, bytes) != hipSuccess) return -1;
|
||||||
if (pinned) { if (hipHostMalloc(&host, bytes, hipHostMallocDefault) != hipSuccess) return -1; }
|
if (pinned) { if (hipHostMalloc(&host, bytes, hipHostMallocDefault) != hipSuccess) return -1; }
|
||||||
else { host = malloc(bytes); }
|
else { host = malloc(bytes); }
|
||||||
memset(host, 0x5a, bytes);
|
memset(host, 0x5a, bytes)
|
||||||
hipEvent_t a, b; hipEventCreate(&a); hipEventCreate(&b);
|
hipEvent_t a, b; hipEventCreate(&a); hipEventCreate(&b)
|
||||||
for (int i = 0; i < 3; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice);
|
for (int i = 0; i < 3; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice)
|
||||||
hipDeviceSynchronize();
|
hipDeviceSynchronize()
|
||||||
hipEventRecord(a, 0);
|
hipEventRecord(a, 0)
|
||||||
for (int i = 0; i < iters; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice);
|
for (int i = 0; i < iters; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice)
|
||||||
hipEventRecord(b, 0); hipEventSynchronize(b);
|
hipEventRecord(b, 0); hipEventSynchronize(b)
|
||||||
float ms = 0; hipEventElapsedTime(&ms, a, b);
|
float ms = 0; hipEventElapsedTime(&ms, a, b)
|
||||||
double gb = (double)bytes * iters / (ms / 1000.0) / 1e9;
|
double gb = (double)bytes * iters / (ms / 1000.0) / 1e9;
|
||||||
if (!pinned) free(host); else hipHostFree(host);
|
if (!pinned) free(host); else hipHostFree(host)
|
||||||
hipFree(dev); hipEventDestroy(a); hipEventDestroy(b);
|
hipFree(dev); hipEventDestroy(a); hipEventDestroy(b)
|
||||||
return gb;
|
return gb;
|
||||||
}
|
}
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
setvbuf(stdout, NULL, _IONBF, 0);
|
setvbuf(stdout, NULL, _IONBF, 0)
|
||||||
if (hipSetDevice((argc > 1) ? atoi(argv[1]) : 0) != hipSuccess) return 1;
|
if (hipSetDevice((argc > 1) ? atoi(argv[1]) : 0) != hipSuccess) return 1;
|
||||||
size_t sz[] = {2459136UL, 3UL<<20, 16UL<<20, 64UL<<20, 256UL<<20, 1UL<<30};
|
size_t sz[] = {2459136UL, 3UL<<20, 16UL<<20, 64UL<<20, 256UL<<20, 1UL<<30};
|
||||||
for (int i = 0; i < 6; i++) {
|
for (int i = 0; i < 6; i++) {
|
||||||
size_t b = sz[i];
|
size_t b = sz[i];
|
||||||
int iters = (b >= (1UL<<30)) ? 10 : (b >= (256UL<<20)) ? 20 : 100;
|
int iters = (b >= (1UL<<30)) ? 10 : (b >= (256UL<<20)) ? 20 : 100;
|
||||||
printf(" %10.3f MiB | pinned %6.2f | pageable %6.2f\n",
|
printf(" %10.3f MiB | pinned %6.2f | pageable %6.2f\n",
|
||||||
b/1048576.0, one(b,true,iters), one(b,false,iters));
|
b/1048576.0, one(b,true,iters), one(b,false,iters))
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ sudo lspci -vv -s <bridge> | grep AtomicOpsCap # want Routing+
|
|||||||
slot on CPU root ports (atomics fine), the rest behind the chipset (usually
|
slot on CPU root ports (atomics fine), the rest behind the chipset (usually
|
||||||
not). Bandwidth for TP is small anyway: ~10 MB per token-step for a 27B at
|
not). Bandwidth for TP is small anyway: ~10 MB per token-step for a 27B at
|
||||||
batch 4 is a millisecond-scale figure at x4 — compute-bound, not
|
batch 4 is a millisecond-scale figure at x4 — compute-bound, not
|
||||||
interconnect-bound (full derivation: [[Dual-R9700-Cooperation]] §Bandwidth).
|
interconnect-bound (full derivation: §Bandwidth).
|
||||||
|
|
||||||
**Probe matrix (bare 2-rank `torch.distributed`, one process per GPU, 25 s PG
|
**Probe matrix (bare 2-rank `torch.distributed`, one process per GPU, 25 s PG
|
||||||
timeout) — one probe answers several design questions:**
|
timeout) — one probe answers several design questions:**
|
||||||
@@ -216,11 +216,11 @@ reporting an outage (`gitea` vs `gitea-server`).
|
|||||||
complex; x4 suffices for TP.
|
complex; x4 suffices for TP.
|
||||||
2. Board whose CPU x16 bifurcates x8/x8 (done 2026-09-20 on dankserver — the
|
2. Board whose CPU x16 bifurcates x8/x8 (done 2026-09-20 on dankserver — the
|
||||||
per-device chain census, including both endpoints reading `ReqEn+`, is
|
per-device chain census, including both endpoints reading `ReqEn+`, is
|
||||||
[[Dual-R9700-Cooperation]]'s). Re-checked 2026-10-18 without root: both root
|
's). Re-checked 2026-10-18 without root: both root
|
||||||
ports report `current_link_width=8` at `32.0 GT/s`
|
ports report `current_link_width=8` at `32.0 GT/s`
|
||||||
(`/sys/bus/pci/devices/0000:00:01.{1,3}/current_link_width`), and the live
|
(`/sys/bus/pci/devices/0000:00:01.{1,3}/current_link_width`), and the live
|
||||||
engine states direct P2P between the cards.
|
engine states direct P2P between the cards.
|
||||||
3. llama.cpp `--split-mode layer` — no collectives at all ([[Llama Cpp Multi-Device Splits]]); costs the GGUF conversion plus paged attention /
|
3. llama.cpp `--split-mode layer` — no collectives at all (); costs the GGUF conversion plus paged attention /
|
||||||
continuous batching / spec decode.
|
continuous batching / spec decode.
|
||||||
4. Independent TP=1 replicas + routing proxy — cards never share one model.
|
4. Independent TP=1 replicas + routing proxy — cards never share one model.
|
||||||
|
|
||||||
@@ -230,7 +230,7 @@ is gated by a CUDA-sm-keyed table (`{9.0, 10.0, 10.3, 10.7}`; ROCm reports
|
|||||||
`CUSTOM_ALL_REDUCE_MAX_SIZES` in
|
`CUSTOM_ALL_REDUCE_MAX_SIZES` in
|
||||||
`vllm/distributed/device_communicators/all_reduce_utils.py`, whose keys on
|
`vllm/distributed/device_communicators/all_reduce_utils.py`, whose keys on
|
||||||
today's `main` are exactly `"9.0"`, `"10.0"`, `"10.3"`, `"10.7"`
|
today's `main` are exactly `"9.0"`, `"10.0"`, `"10.3"`, `"10.7"`
|
||||||
([source](https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/distributed/device_communicators/all_reduce_utils.py),
|
([source](https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/distributed/device_communicators/all_reduce_utils.py)
|
||||||
read 2026-10-18); nothing keys on a ROCm-reported `12.0`. — and patching it is
|
read 2026-10-18); nothing keys on a ROCm-reported `12.0`. — and patching it is
|
||||||
insufficient because TP init's `broadcast`/`barrier`/logits `all_gather` have
|
insufficient because TP init's `broadcast`/`barrier`/logits `all_gather` have
|
||||||
no non-RCCL path. A
|
no non-RCCL path. A
|
||||||
@@ -249,6 +249,6 @@ kernel's declared args (`roc-obj-extract` + `llvm-readelf --notes`), look for
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[Llama Cpp Multi-Device Splits]] — the layer-split path around the wall
|
- the layer-split path around the wall
|
||||||
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — measured history
|
- / — measured history
|
||||||
- [[frank-archon]] — the gather economics of the retired tcclaviger vLLM era (2026-09-29 → 10-05)
|
- [[frank-archon]] — the gather economics of the retired tcclaviger vLLM era (2026-09-29 → 10-05)
|
||||||
@@ -1,454 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Ethernet Bandwidth Math
|
|
||||||
description: "Bytes-per-token math for pipeline versus tensor parallelism, and the link budgets that follow from it."
|
|
||||||
tags:
|
|
||||||
- pipeline-parallelism
|
|
||||||
- research
|
|
||||||
timestamp: 2026-10-19
|
|
||||||
---
|
|
||||||
|
|
||||||
# Bandwidth Analysis: Multi-Node LLM Inference over Ethernet
|
|
||||||
|
|
||||||
## 0. Measured on the Qwen3.8-27B PP deployment (2026-08-17)
|
|
||||||
|
|
||||||
Everything in this section was measured on the live deployment
|
|
||||||
([[Qwen3.8 PP Deployment]]) while sampling `/proc/net/dev` at 50 Hz on
|
|
||||||
both ends. The theoretical tables below use H=4096/L=32; the real model
|
|
||||||
is larger (see constants), which raises the per-token bytes but not the
|
|
||||||
conclusions.
|
|
||||||
|
|
||||||
### 0.1 Verified constants (from the GGUF + env, not guessed)
|
|
||||||
|
|
||||||
| Constant | Value | Source |
|
|
||||||
| -------- | ----- | ------ |
|
|
||||||
| `hidden_size` (embedding_length) | 5120 | GGUF metadata (`qwen35.embedding_length`) |
|
|
||||||
| Blocks | 65 (45 on worker, 20 on head) | `--tensor-split 0.68,0.32` |
|
|
||||||
| FA layers | 16 total, interval 4 (11 worker / 5 head) | GGUF `full_attention_interval` |
|
|
||||||
| KV per FA layer per token | 3136 B (K q8_0 1088 + V f16 2048) | env file comment, confirmed |
|
|
||||||
| ubatch | 256 | `UBATCH_SIZE` in `frank-dragoon.env` |
|
|
||||||
| MTP verify step positions | 5 (1 + n_max 4) | `SPEC_DRAFT_N_MAX=4` |
|
|
||||||
| Link (old path) | 1 GbE via USB-C adapter on claw | `enx00e04cc2e1e5`, negotiated 1000 Mb/s |
|
|
||||||
| Link (current path) | 2.5 GbE eno1 to eno1 | negotiated 2500 Mb/s both ends |
|
|
||||||
| iperf3, 2.5 GbE path | 281 MB/s claw->server, 294 MB/s server->claw | measured |
|
|
||||||
| ping RTT, 1 GbE USB path | min 0.239 / avg 0.742 / max 1.085 / mdev 0.334 ms | measured |
|
|
||||||
| ping RTT, 2.5 GbE path | min 0.224 / avg 0.585 / max 0.883 / mdev 0.211 ms | measured |
|
|
||||||
|
|
||||||
The USB-C adapter added ~15 us to RTT (0.239 vs 0.224 ms min) and more
|
|
||||||
jitter (mdev 0.334 vs 0.211 ms).
|
|
||||||
|
|
||||||
### 0.2 Measured wire bytes (the number that matters)
|
|
||||||
|
|
||||||
Raw hidden state per position per direction = 5120 x 2 = **10.0 KB**.
|
|
||||||
The *measured* bytes per position per direction are ~4x that:
|
|
||||||
|
|
||||||
| Direction | Measured | Raw hidden |
|
|
||||||
| --------- | -------- | ---------- |
|
|
||||||
| head -> worker | 39.8 KB/position | 10.0 KB |
|
|
||||||
| worker -> head | 38.4 KB/position | 10.0 KB |
|
|
||||||
|
|
||||||
The extra ~30 KB/position is the serialized RPC graph (per-position
|
|
||||||
nodes for all 45 worker blocks: attention, KV append, SSM ops)
|
|
||||||
plus protocol framing. It scales with position count just like the
|
|
||||||
payload, so it does not shrink away.
|
|
||||||
|
|
||||||
Per ubatch (256 positions) the measured crossing is **20.5 MB**
|
|
||||||
(10.4 MB head->worker + 10.1 MB worker->head).
|
|
||||||
|
|
||||||
### 0.3 Measured throughput and the validated model
|
|
||||||
|
|
||||||
Measured prefill (fresh 13,959-token prompt, 14k context, 2.5 GbE
|
|
||||||
path): **457.9 t/s** (30,485 ms, journal task 11924). Decomposition:
|
|
||||||
|
|
||||||
| Term | 2.5 GbE (measured anchor) |
|
|
||||||
| ---- | ------------------------- |
|
|
||||||
| wire per ubatch (20.5 MB) | 70 ms |
|
|
||||||
| compute + overhead per ubatch (residual) | 489 ms |
|
|
||||||
| ubatch total | 559 ms = 2.18 ms/token |
|
|
||||||
|
|
||||||
Using that residual and only swapping the wire time per link speed:
|
|
||||||
|
|
||||||
| Link | wire/ubatch | predicted prefill | avg link use | line-rate burst duty |
|
|
||||||
| ---- | ----------- | ----------------- | ------------ | -------------------- |
|
|
||||||
| 1 GbE (old USB path) | 174 ms | **386 t/s** | 15.8% | 26.2% |
|
|
||||||
| 2.5 GbE (current) | 70 ms | 458 t/s (matches measured) | 6.4% | 12.5% |
|
|
||||||
| 10 GbE | 17 ms | **505 t/s** | 1.6% | 3.4% |
|
|
||||||
|
|
||||||
Delta: 1 -> 2.5 GbE is **+18.6% prefill**; 2.5 -> 10 GbE is **+9.4%**.
|
|
||||||
|
|
||||||
### 0.4 The KV snapshot: where the link really saturates
|
|
||||||
|
|
||||||
On task release, `llama-server` saves the conversation to the prompt
|
|
||||||
cache. The KV for the 11 FA layers living on the worker GPU must be
|
|
||||||
copied **worker -> head** for that snapshot, and the slot is BLOCKED
|
|
||||||
while it runs (`PARALLEL=1` = one user queues behind it).
|
|
||||||
|
|
||||||
Measured for a 98,100-token conversation: 3,454 MB copied (model check:
|
|
||||||
98100 x 11 x 3136 B = 3.38 GB, matches), at line rate (254 MB/s avg,
|
|
||||||
308 MB/s peak).
|
|
||||||
|
|
||||||
| Link | snapshot duration (slot blocked) |
|
|
||||||
| ---- | -------------------------------- |
|
|
||||||
| 1 GbE | ~31 s |
|
|
||||||
| 2.5 GbE | 13.6 s (measured; journal: "prompt cache update took 14418 ms") |
|
|
||||||
| 10 GbE | ~5 s |
|
|
||||||
|
|
||||||
This is the only phase that runs the link at ~100% utilization for a
|
|
||||||
long time. Prefill runs at 12-26% of line rate in bursts (per ubatch)
|
|
||||||
and ~6-16% on average; decode runs at ~2% or less.
|
|
||||||
|
|
||||||
### 0.5 Decode (MTP verify step, 5 positions)
|
|
||||||
|
|
||||||
| Link | net time per step | share of 164 ms step | decode t/s |
|
|
||||||
| ---- | ----------------- | -------------------- | ---------- |
|
|
||||||
| 1 GbE | 4.0 ms (RTT 0.74 + wire 3.4) | 2.4% | ~23.0 |
|
|
||||||
| 2.5 GbE | 1.9 ms (RTT 0.59 + wire 1.4) | 1.1% | 23.47 (measured) |
|
|
||||||
| 10 GbE | 0.6 ms (RTT ~0.25 + wire 0.3) | 0.4% | ~23.5 |
|
|
||||||
|
|
||||||
Decode is latency-touched but bandwidth-trivial. Average wire rate
|
|
||||||
~19 Mbit/s in each direction.
|
|
||||||
|
|
||||||
### 0.6 Verdict for the 10 GbE USB-C adapter question
|
|
||||||
|
|
||||||
- The 1 GbE -> 2.5 GbE upgrade (the big win) is already captured for
|
|
||||||
free: the claw's native 2.5 GbE `eno1` now carries the RPC path
|
|
||||||
(the 1 GbE USB adapter was unplugged 2026-08-17; routing moved the
|
|
||||||
path, the RPC TCP re-attached on the new interface).
|
|
||||||
- 2.5 -> 10 GbE buys +9.4% prefill and 13.6 s -> 5 s snapshot blocking.
|
|
||||||
It requires the L2 device at 192.168.0.1 to be 10G-capable, or a
|
|
||||||
direct cable between the two machines (dedicated link, second IP
|
|
||||||
pair, or a lower-metric static route).
|
|
||||||
- At current usage (PARALLEL=1, decode-heavy), 2.5 GbE is comfortable.
|
|
||||||
|
|
||||||
## 1. Bandwidth Math: Bytes per Token per Layer Boundary
|
|
||||||
|
|
||||||
### 1.1 Pipeline Parallelism (PP)
|
|
||||||
|
|
||||||
During autoregressive generation, each transformer layer produces a hidden state of shape `(batch_size, hidden_size)`. At a pipeline stage boundary, this tensor must be communicated between nodes.
|
|
||||||
|
|
||||||
**Forward pass only (inference):**
|
|
||||||
```
|
|
||||||
bytes_per_boundary = batch_size × hidden_size × 2 [fp16 = 2 bytes]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key detail:** During *generation* (one token at a time), seq_len = 1 at each boundary crossing. The KV cache stays local. The hidden state is `(batch, hidden_size)` — independent of context length.
|
|
||||||
|
|
||||||
**For a 27B dense model (e.g., Qwen3.6-27B):**
|
|
||||||
- Hidden size: H = 4096 (standard for Llama/Qwen-style architectures).
|
|
||||||
Note: the Qwen3.8-27B actually deployed on this fleet has H = 5120
|
|
||||||
(GGUF-verified); see section 0, which supersedes these theoretical
|
|
||||||
numbers with measurements.
|
|
||||||
- Per-token, per-boundary: `4096 × 2 = 8,192 bytes = 8 KB`
|
|
||||||
- With Nₚₚ stage boundaries: total = `Nₚₚ × 8 KB/tok`
|
|
||||||
|
|
||||||
**For a 36B MoE model (e.g., Qwen3.6-35B-A3B):**
|
|
||||||
- Hidden size: H = 6144 (common for this scale)
|
|
||||||
- Layers: L = 48 (typical)
|
|
||||||
- Per-token, per-boundary: `6144 × 2 = 12,288 bytes = 12 KB`
|
|
||||||
- With Nₚₚ stage boundaries: total = `Nₚₚ × 12 KB/tok`
|
|
||||||
|
|
||||||
**Worked examples (1 boundary = 2 PP stages):**
|
|
||||||
```
|
|
||||||
27B, batch=1: 1 boundary × 4096 × 2 bytes = 8,192 bytes/tok = 8 KB
|
|
||||||
27B, batch=64: 1 boundary × 4096 × 64 × 2 bytes = 524,288 = 512 KB/tok
|
|
||||||
36B, batch=1: 1 boundary × 6144 × 2 bytes = 12,288 bytes/tok = 12 KB
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Tensor Parallelism (TP) — for comparison
|
|
||||||
|
|
||||||
In tensor parallelism, each MLP and attention layer performs an all-reduce on the hidden state:
|
|
||||||
|
|
||||||
```
|
|
||||||
bytes_per_layer = batch_size × hidden_size × 2 [one all-reduce per layer]
|
|
||||||
total_bytes_per_tok = L × batch_size × hidden_size × 2 [L all-reduces total]
|
|
||||||
```
|
|
||||||
|
|
||||||
**For 27B, H=4096, L=32, TP=2, batch=1:**
|
|
||||||
```
|
|
||||||
32 layers × 4096 × 1 × 2 bytes = 262,144 bytes = 256 KB/tok
|
|
||||||
(16 KB per layer all-reduce × 32 layers)
|
|
||||||
```
|
|
||||||
|
|
||||||
**For 36B, H=6144, L=48, TP=2, batch=1:**
|
|
||||||
```
|
|
||||||
48 layers × 6144 × 1 × 2 bytes = 589,824 bytes = 576 KB/tok
|
|
||||||
(12 KB per layer all-reduce × 48 layers)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 Scaling with batch size
|
|
||||||
|
|
||||||
| Model | H | L | tok/s | Batch | PP boundary bytes/tok | TP total bytes/tok |
|
|
||||||
|-------|-----|-----|-------|-------|----------------------|--------------------|
|
|
||||||
| 27B | 4096 | 32 | 10 | 1 | 8 KB | 256 KB |
|
|
||||||
| 27B | 4096 | 32 | 50 | 1 | 8 KB | 256 KB |
|
|
||||||
| 27B | 4096 | 32 | 100 | 1 | 8 KB | 256 KB |
|
|
||||||
| 27B | 4096 | 32 | 10 | 16 | 128 KB | 4,096 KB |
|
|
||||||
| 27B | 4096 | 32 | 50 | 16 | 128 KB | 4,096 KB |
|
|
||||||
| 27B | 4096 | 32 | 100 | 16 | 128 KB | 4,096 KB |
|
|
||||||
| 27B | 4096 | 32 | 10 | 64 | 512 KB | 16,384 KB |
|
|
||||||
| 27B | 4096 | 32 | 50 | 64 | 512 KB | 16,384 KB |
|
|
||||||
| 27B | 4096 | 32 | 100 | 64 | 512 KB | 16,384 KB |
|
|
||||||
| 36B | 6144 | 48 | 10 | 1 | 12 KB | 576 KB |
|
|
||||||
| 36B | 6144 | 48 | 50 | 1 | 12 KB | 576 KB |
|
|
||||||
| 36B | 6144 | 48 | 100 | 1 | 12 KB | 576 KB |
|
|
||||||
| 36B | 6144 | 48 | 10 | 16 | 192 KB | 9,216 KB |
|
|
||||||
| 36B | 6144 | 48 | 50 | 16 | 192 KB | 9,216 KB |
|
|
||||||
| 36B | 6144 | 48 | 100 | 16 | 192 KB | 9,216 KB |
|
|
||||||
| 36B | 6144 | 48 | 10 | 64 | 768 KB | 36,864 KB |
|
|
||||||
| 36B | 6144 | 48 | 50 | 64 | 768 KB | 36,864 KB |
|
|
||||||
| 36B | 6144 | 48 | 100 | 64 | 768 KB | 36,864 KB |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Required Bandwidth vs. Ethernet Capabilities
|
|
||||||
|
|
||||||
### 2.1 Ethernet link capacities
|
|
||||||
|
|
||||||
| Link | Raw Rate | Usable TCP (approx.) | MB/s usable |
|
|
||||||
|------|----------|---------------------|-------------|
|
|
||||||
| 1 GbE | 1,000 Mbit/s | ~940 Mbit/s | ~118 MB/s |
|
|
||||||
| 2.5 GbE | 2,500 Mbit/s | ~2,350 Mbit/s | ~294 MB/s |
|
|
||||||
| 10 GbE | 10,000 Mbit/s | ~9,400 Mbit/s | ~1,175 MB/s |
|
|
||||||
|
|
||||||
### 2.2 Required bandwidth (Mbps) by model, batch, and throughput
|
|
||||||
|
|
||||||
**27B model (H=4096), 1 PP boundary:**
|
|
||||||
|
|
||||||
| tok/s | Batch=1 | Batch=16 | Batch=64 |
|
|
||||||
|-------|---------|----------|----------|
|
|
||||||
| 10 | 0.13 Mbit/s | 2.05 Mbit/s | 8.19 Mbit/s |
|
|
||||||
| 50 | 0.65 Mbit/s | 10.24 Mbit/s | 40.96 Mbit/s |
|
|
||||||
| 100 | 1.31 Mbit/s | 20.48 Mbit/s | 81.92 Mbit/s |
|
|
||||||
|
|
||||||
**36B model (H=6144), 1 PP boundary:**
|
|
||||||
|
|
||||||
| tok/s | Batch=1 | Batch=16 | Batch=64 |
|
|
||||||
|-------|---------|----------|----------|
|
|
||||||
| 10 | 0.20 Mbit/s | 3.07 Mbit/s | 12.29 Mbit/s |
|
|
||||||
| 50 | 0.98 Mbit/s | 15.36 Mbit/s | 61.44 Mbit/s |
|
|
||||||
| 100 | 1.97 Mbit/s | 30.72 Mbit/s | 122.88 Mbit/s |
|
|
||||||
|
|
||||||
**27B model, TP (L=32 layers all-reduce):**
|
|
||||||
|
|
||||||
| tok/s | Batch=1 | Batch=16 | Batch=64 |
|
|
||||||
|-------|---------|----------|----------|
|
|
||||||
| 10 | 2.05 Mbit/s | 32.77 Mbit/s | 131.07 Mbit/s |
|
|
||||||
| 50 | 10.24 Mbit/s | 163.84 Mbit/s | 655.36 Mbit/s |
|
|
||||||
| 100 | 20.48 Mbit/s | 327.68 Mbit/s | 1,310.72 Mbit/s |
|
|
||||||
|
|
||||||
### 2.3 Feasibility
|
|
||||||
|
|
||||||
Thresholds: **feasible** = under 50% of link capacity, **marginal** = 50–90%,
|
|
||||||
**saturated** = over 90%. Cross-checking §2.2 against §2.1: every PP case is
|
|
||||||
feasible on every link class — even the heaviest, 27B at batch=64 and 100
|
|
||||||
tok/s (81.92 Mbit/s), is feasible on 1 GbE — and every TP case is feasible
|
|
||||||
except batch=64:
|
|
||||||
|
|
||||||
**27B TP batch=64 at 100 tok/s: 1310.72 Mbit/s > 1 GbE usable (1000 Mbit/s) — link-saturated on 1 GbE**
|
|
||||||
|
|
||||||
**27B TP batch=64 at 100 tok/s: 1310.72 Mbit/s ≈ 2.5 GbE usable (2500 Mbit/s) — ~52% saturated**
|
|
||||||
|
|
||||||
**Key finding:** PP is feasible across all ethernet classes for all batch sizes up to at least 100 tok/s. TP becomes bottlenecked on 1 GbE at batch=64 and 100 tok/s.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Why PP is the Only Realistic Option over Ethernet
|
|
||||||
|
|
||||||
### 3.1 Communication cost comparison
|
|
||||||
|
|
||||||
| Scheme | Communication per tok per rank | 27B, H=4096, batch=1 | 36B, H=6144, batch=1 |
|
|
||||||
|--------|------|---|---|
|
|
||||||
| PP (Nₚₚ boundaries) | Nₚₚ × H × 2 | Nₚₚ × 8 KB | Nₚₚ × 12 KB |
|
|
||||||
| TP (L layers all-reduce) | L × H × 2 | 32 × 8 KB = 256 KB | 48 × 12 KB = 576 KB |
|
|
||||||
|
|
||||||
PP communication per token is **L/Nₚₚ times smaller** than TP communication per token (factor of ~16× for 2 PP stages).
|
|
||||||
|
|
||||||
### 3.2 Latency-bound nature of autoregressive generation
|
|
||||||
|
|
||||||
During autoregressive generation, each layer computes one token at a time. In TP, every layer requires an all-reduce (2× ring latency or 1× tree latency per layer). For L=32 layers, that's 32 synchronization points per token.
|
|
||||||
|
|
||||||
In PP, activations cross the stage boundary only once per layer (or L/Nₚₚ times). The critical path is:
|
|
||||||
|
|
||||||
```
|
|
||||||
Stage 0 computes → send hidden state across boundary → Stage 1 computes → ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This is a single bottleneck per token, with latency proportional to network RTT × number of stages, not L.
|
|
||||||
|
|
||||||
### 3.3 Bandwidth utilization
|
|
||||||
|
|
||||||
With PP, each boundary crossing transfers `H × 2` bytes. For 8 KB at 1 GbE, the transfer completes in ~68 µs. This is negligible compared to compute time (milliseconds per layer on a GPU). The link is underutilized.
|
|
||||||
|
|
||||||
With TP, each layer transfers `H × 2` bytes but this happens L times. For 256 KB total at 1 GbE, that's ~2 ms of pure transfer time — potentially comparable to compute time on a fast GPU.
|
|
||||||
|
|
||||||
### 3.4 Real-world evidence
|
|
||||||
|
|
||||||
- Petals uses PP exclusively over the public internet and achieves usable inference at 1–2 tok/s even with 100 Mbit/s links [1].
|
|
||||||
- llama.cpp RPC splits layers across nodes in pipeline order, not tensor sharding [2].
|
|
||||||
- exo uses TP but achieves speedup only with RDMA over Thunderbolt (~50 Gbit/s) [3].
|
|
||||||
- Petals paper Table 3: inference performance does **not depend much on bandwidth** (1 Gbit/s vs 100 Mbit/s gives nearly identical results) but **degrades significantly with higher latency** [1].
|
|
||||||
|
|
||||||
**Conclusion:** For gigabit-class ethernet, PP is the only practical multi-node parallelism strategy for inference. TP can work on 10 GbE for small batches but saturates at 1 GbE with moderate batch sizes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Real-World Multi-Node Inference Benchmarks
|
|
||||||
|
|
||||||
### 4.1 Summary table
|
|
||||||
|
|
||||||
| System | Model | Hardware | # Nodes | Network | tok/s (batch=1) | Notes |
|
|
||||||
|--------|-------|----------|---------|---------|-----------------|-------|
|
|
||||||
| Petals (local) | BLOOM-176B | 3× A100 80GB | 3 | 1 Gbit/s, <5 ms RTT | 1.71 | PP, 22 stages, quantized [1] |
|
|
||||||
| Petals (local) | BLOOM-176B | 3× A100 80GB | 3 | 100 Mbit/s, <5 ms RTT | 1.66 | ~3% slowdown vs 1 Gbit/s [1] |
|
|
||||||
| Petals (local) | BLOOM-176B | 3× A100 80GB | 3 | 100 Mbit/s, 100 ms RTT | 1.23 | Latency matters more than BW [1] |
|
|
||||||
| Petals (wan) | BLOOM-176B | 14 consumer GPUs | 14 | Real internet, 100–1000 Mbit/s | 0.83 | Europe+N. America [1] |
|
|
||||||
| Petals | Llama 2 70B | consumer GPUs | various | Internet | up to 6 | Interactive chat [4] |
|
|
||||||
| Petals | Falcon 180B | consumer GPUs | various | Internet | up to 4 | Interactive chat [4] |
|
|
||||||
| llama.cpp RPC | Qwen3-235B | 4× M3 Ultra Mac Studio | 4 | Ethernet (2.5 GbE) | ~20 | Scales down with more nodes [3] |
|
|
||||||
| llama.cpp RPC | DeepSeek v3.1 | 4× M3 Ultra Mac Studio | 4 | Thunderbolt 5 | ~25 | Scales down with more nodes [3] |
|
|
||||||
| llama.cpp RPC | Kimi K2 (1T) | 4× M3 Ultra Mac Studio | 4 | Thunderbolt 5 | ~30 | A32B sparse, 32B active [3] |
|
|
||||||
| Exo (RDMA) | Qwen3-235B | 4× M3 Ultra Mac Studio | 4 | TB5 RDMA (~50 GbE) | 32 | TP, scales UP with more nodes [3] |
|
|
||||||
| Exo (RDMA) | DeepSeek v3.1 | 4× M3 Ultra Mac Studio | 4 | TB5 RDMA (~50 GbE) | ~30 | 671B dense TP [3] |
|
|
||||||
| vLLM PP | Llama-3-70B | 2× A100 80GB | 2 | NVLink + Ethernet | ~15 | PP across nodes, TP within [5] |
|
|
||||||
|
|
||||||
### 4.2 Detailed notes
|
|
||||||
|
|
||||||
**Petals [1]:**
|
|
||||||
- Uses pipeline parallelism with dynamic 8-bit quantization (halves bandwidth)
|
|
||||||
- For BLOOM-176B (H=14336), per-boundary communication is ~14 KB/tok with quantization
|
|
||||||
- Key finding: "For inference, performance does not depend much on bandwidth or sequence length but degrades with higher latency" — RTT matters more than throughput
|
|
||||||
|
|
||||||
**llama.cpp RPC [2]:**
|
|
||||||
- Model weights and KV cache distributed across nodes proportionally to memory
|
|
||||||
- Default: pipeline-style distribution of layers (first layers on node A, last on node B)
|
|
||||||
- Each node runs `ggml-rpc-server`, main host uses `--rpc` flag
|
|
||||||
- RDMA transport supported (RoCEv2), negotiated automatically with libibverbs
|
|
||||||
- Over ethernet, performance decreases with more nodes (each extra node adds a hop)
|
|
||||||
- Over RDMA/TB5, performance scales up with more nodes (less latency per hop)
|
|
||||||
|
|
||||||
**Exo [3]:**
|
|
||||||
- Uses tensor parallelism with MLX distributed backend
|
|
||||||
- With RDMA over Thunderbolt 5: up to 1.8× speedup on 2 devices, 3.2× on 4 devices
|
|
||||||
- Topology-aware auto-parallelism considers latency/bandwidth between links
|
|
||||||
- Thunderbolt 5 provides ~50–60 GbE real-world throughput (RDMA)
|
|
||||||
|
|
||||||
**Key insight from all systems:** Ethernet (1 Gbit/s) is sufficient for PP inference of 20–40B models because per-token activation traffic is small (8–12 KB/boundary). The bottleneck is **latency** (RTT between nodes), not bandwidth.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Overheads
|
|
||||||
|
|
||||||
### 5.1 TCP/IP overhead
|
|
||||||
|
|
||||||
| Overhead | Amount |
|
|
||||||
|----------|--------|
|
|
||||||
| Ethernet header + trailer | 18 bytes (14 + 4 FCS) |
|
|
||||||
| IPv4 header (no options) | 20 bytes |
|
|
||||||
| TCP header (no options) | 20 bytes |
|
|
||||||
| Total per packet | ~58 bytes overhead |
|
|
||||||
| MTU (jumbo frames 9000) | 9000 bytes payload → 0.6% overhead |
|
|
||||||
| MTU (1500 standard) | 1500 bytes payload → 3.9% overhead |
|
|
||||||
|
|
||||||
**Practical impact:** For 8 KB transfers (27B PP boundary), use jumbo frames where possible to minimize TCP overhead. For small transfers (< 1.5 KB), a single TCP packet is used and the overhead is 58/8192 = 0.7% per transfer — negligible.
|
|
||||||
|
|
||||||
### 5.2 RPC/serialization overhead
|
|
||||||
|
|
||||||
- llama.cpp RPC: Protobuf serialization adds ~10–20% overhead for small tensors
|
|
||||||
- Petals: PyTorch tensor serialization over hivemind/gRPC, adds serialization time
|
|
||||||
- vLLM PP: gRPC with protobuf, similar overhead
|
|
||||||
- Serialization scales with tensor size: negligible for 8 KB+ tensors
|
|
||||||
|
|
||||||
### 5.3 Pipeline bubble overhead
|
|
||||||
|
|
||||||
With Nₚₚ pipeline stages and microbatching, the pipeline bubble fraction is:
|
|
||||||
|
|
||||||
```
|
|
||||||
bubble_fraction = (Nₚₚ - 1) / (Nₚₚ + microbatches - 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
For 2 stages, 1 microbatch (single tok): bubble = 1/2 = 50% idle time.
|
|
||||||
For 2 stages, 8 microbatches: bubble = 1/9 = 11%.
|
|
||||||
|
|
||||||
In autoregressive generation with batch=1 and no microbatching, each stage processes one token sequentially:
|
|
||||||
- Stage 0 finishes, sends hidden state, waits for Stage 1
|
|
||||||
- Stage 1 processes, returns result, Stage 0 processes next token
|
|
||||||
- Effective utilization ≈ 1/Nₚₚ if no pipelining overlap
|
|
||||||
|
|
||||||
**Mitigation:** Process multiple tokens before waiting. Send next token to Stage 0 while Stage 1 is computing — pipelining reduces bubble.
|
|
||||||
|
|
||||||
### 5.4 Per-hop LAN latency
|
|
||||||
|
|
||||||
| Network | Typical RTT | One-way latency |
|
|
||||||
|---------|------------|-----------------|
|
|
||||||
| 1 GbE (Cat6) | 30–50 µs | 15–25 µs |
|
|
||||||
| 2.5 GbE (Cat6) | 30–50 µs | 15–25 µs |
|
|
||||||
| 10 GbE (Cat6a) | 20–30 µs | 10–15 µs |
|
|
||||||
| RoCEv2 (IB/25GbE) | 1–5 µs | <5 µs |
|
|
||||||
| RDMA (Thunderbolt 5) | 10–30 µs round-trip end-to-end | — |
|
|
||||||
|
|
||||||
**Impact on PP inference:** With Nₚₚ = 2 stages and batch=1, one token takes:
|
|
||||||
```
|
|
||||||
t_compute + t_roundtrip + t_compute
|
|
||||||
```
|
|
||||||
|
|
||||||
At 30 µs RTT and ~2 ms layer compute time per GPU: the network adds ~30 µs, which is 1.5% of total. Negligible at LAN speeds.
|
|
||||||
|
|
||||||
With Nₚₚ = 4 and 30 µs per-hop RTT: ~120 µs network, still <6% of compute time.
|
|
||||||
|
|
||||||
### 5.5 Quantization overhead
|
|
||||||
|
|
||||||
- Petals uses dynamic blockwise 8-bit quantization: halves hidden state bandwidth from 16-bit to 8-bit [1]
|
|
||||||
- For 27B: 8 KB → 4 KB per boundary with 8-bit quantization
|
|
||||||
- Accuracy impact: <1% on standard benchmarks [1]
|
|
||||||
- Quantization/dequantization adds ~1–5 ms per stage on modern GPUs (negligible vs. compute)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Conclusions and Recommendations
|
|
||||||
|
|
||||||
### For the 27B / 36B models on gigabit-class ethernet:
|
|
||||||
|
|
||||||
1. **1 GbE is sufficient for PP inference** up to at least 100 tok/s at batch=64 (81.92 Mbit/s required, well under 1000 Mbit/s capacity). Even with TCP/IP and RPC overheads, margin is >90%.
|
|
||||||
|
|
||||||
2. **Pipeline parallelism is the correct choice** over ethernet — PP sends 8–12 KB per boundary vs. 256–576 KB for TP, a 32× advantage for 2 PP stages.
|
|
||||||
|
|
||||||
3. **Latency matters more than bandwidth** — the Petals paper demonstrates that dropping from 1 Gbit/s to 100 Mbit/s only reduces throughput by ~3%, while increasing RTT from 5 ms to 100 ms reduces it by ~28%.
|
|
||||||
|
|
||||||
4. **With 8-bit quantization**, bandwidth requirements halve, providing additional safety margin.
|
|
||||||
|
|
||||||
5. **Jumbo frames recommended** to minimize TCP overhead on larger transfers.
|
|
||||||
|
|
||||||
6. **For batch sizes ≥ 16**, 1 GbE remains viable for PP but consider 2.5 GbE for headroom and faster model loading.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
[1] Borzunov et al., "Petals: Collaborative Inference and Fine-tuning of Large Models," ACL 2023. arXiv:2209.01188. Available at: https://arxiv.org/pdf/2209.01188 — Primary benchmark source: Table 3 shows inference throughput at various bandwidths and latencies for BLOOM-176B. Key finding: inference insensitive to bandwidth, sensitive to latency. Recommended 25 Mbit/s bidirectional bandwidth.
|
|
||||||
|
|
||||||
[2] ggml-org/llama.cpp RPC documentation. Available at: https://github.com/ggml-org/llama.cpp/blob/master/tools/rpc/README.md — Architecture for distributed LLM inference via RPC. Supports RDMA transport (RoCEv2), automatic negotiation, local tensor caching.
|
|
||||||
|
|
||||||
[3] exo-explore/exo README. Available at: https://github.com/exo-explore/exo — Multi-node LLM inference on Apple Silicon with TP and RDMA over Thunderbolt 5. Benchmarks with Qwen3-235B, DeepSeek v3.1, Kimi K2 on 4× M3 Ultra Mac Studio.
|
|
||||||
|
|
||||||
[4] Petals chatbot demo and health monitor. Available at: https://chat.petals.dev, https://health.petals.dev — Real-world performance of Llama 2 70B (up to 6 tok/s) and Falcon 180B (up to 4 tok/s) on public swarm.
|
|
||||||
|
|
||||||
[5] vLLM pipeline parallel documentation. Available at: https://docs.vllm.ai/en/stable/dev/engine/llm_engine.html — vLLM supports PP across nodes using gRPC with CUDA-graph support for low-latency stage transitions.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix: Full bandwidth formulae
|
|
||||||
|
|
||||||
### Pipeline and Tensor Parallelism (generation)
|
|
||||||
|
|
||||||
`N` is the batch size; `N = 1` is the batch=1 form.
|
|
||||||
|
|
||||||
```
|
|
||||||
bytes_per_boundary_per_tok = hidden_size × N × 2 [fp16, PP boundary]
|
|
||||||
bytes_per_layer_allreduce = hidden_size × N × 2 [TP, one all-reduce per layer]
|
|
||||||
total_bytes_per_tok = num_boundaries × hidden_size × N × 2 [PP]
|
|
||||||
total_bytes_per_tok = num_layers × hidden_size × N × 2 [TP]
|
|
||||||
bandwidth_mbps = total_bytes_per_tok × tokens_per_second × 8 / 1,000,000
|
|
||||||
|
|
||||||
# With 8-bit quantization (like Petals): simply divide all fp16 byte counts by 2
|
|
||||||
bytes_per_boundary_per_tok = hidden_size × 1 [int8]
|
|
||||||
```
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Expected Performance
|
|
||||||
description: "Estimated split-model throughput over ethernet, with the benchmark anchors the estimates rest on."
|
|
||||||
tags:
|
|
||||||
- pipeline-parallelism
|
|
||||||
- performance
|
|
||||||
- estimate
|
|
||||||
timestamp: 2026-08-14
|
|
||||||
---
|
|
||||||
|
|
||||||
# Expected Performance
|
|
||||||
|
|
||||||
This page estimates the speed of a 25-40B model split across two machines
|
|
||||||
over ethernet. It uses the bandwidth math in [[Ethernet Bandwidth Math]]
|
|
||||||
and the mixed-vendor measurements in [[x4-pair-experiment]].
|
|
||||||
|
|
||||||
## The Bandwidth Ceiling Is Not the Limit
|
|
||||||
|
|
||||||
A pipeline boundary moves only the hidden state. For a 27B model (hidden
|
|
||||||
5120), that is 10240 bytes per token per boundary in fp16. Even at 64
|
|
||||||
tokens per second with batch 64, the traffic is about 262 Mbit/s. 1 GbE can
|
|
||||||
carry it. The real limits are latency, RPC overhead, and the slowest node.
|
|
||||||
|
|
||||||
The Petals paper measured this directly. Dropping from 1 Gbit/s to
|
|
||||||
100 Mbit/s changed throughput by 3 percent. Raising round-trip latency from
|
|
||||||
5 ms to 100 ms dropped throughput by 28 percent. Latency matters more than
|
|
||||||
bandwidth for pipeline inference.
|
|
||||||
|
|
||||||
## Real Benchmark Anchors
|
|
||||||
|
|
||||||
| Experiment | Model | Network | Decode (t/s) |
|
|
||||||
| ---------- | ----- | ------- | ------------ |
|
|
||||||
| Mac Studio + DGX Spark (Metal+CUDA) | Qwen2.5-72B Q4 | 10 GbE | 6 |
|
|
||||||
| Mac Studio + DGX Spark (Metal+CUDA) | Qwen2.5-7B Q4 | 10 GbE | 53 |
|
|
||||||
| RTX 4070 + MI50 + 3050 (CUDA+ROCm) | Qwen3-VL-30B Q5 | 1 GbE | 51.5 clean, 17.8 at 32k ctx |
|
|
||||||
| Two Strix Halos (ROCm RPC) | MiniMax-M2.5-139B Q8 | USB4 | 15.35 |
|
|
||||||
| AMD four-node Strix Halo (5 GbE) | Kimi K2.5 (1T) | 5 GbE | 8.30 |
|
|
||||||
| Tesla V100 + R9700x2 (Vulkan, one box) | Qwen3.6-35B-A3B Q8 | PCIe | 62 |
|
|
||||||
|
|
||||||
Pattern: RPC decode runs about 2-5 times slower than the best single node.
|
|
||||||
Prefill benefits from more compute across nodes. The 30B-class model over
|
|
||||||
1 GbE with warm cache reached 51.5 t/s on a short context and 17.8 t/s at
|
|
||||||
32k context.
|
|
||||||
|
|
||||||
## Estimate for Qwen3.6-27B Q8, Two Nodes
|
|
||||||
|
|
||||||
Scenario: RTX 3090 (24 GB VRAM, NVIDIA desktop) plus Strix Halo NUC
|
|
||||||
(Radeon 8060S, 30 GB physical RAM). The model is the dense 27B at Q8
|
|
||||||
(29-32 GB). Each node holds part of the weights and KV cache.
|
|
||||||
|
|
||||||
| Link | Decode (t/s) | Prefill (t/s) | Notes |
|
|
||||||
| ---- | ------------ | ------------- | ----- |
|
|
||||||
| 1 GbE | 8-15 | 200-500 | Latency-bound; per-token RPC round trip |
|
|
||||||
| 2.5 GbE | 10-18 | 250-600 | Some headroom; latency still dominant |
|
|
||||||
| 10 GbE | 12-20 | 300-800 | Closest to the published 10 GbE runs |
|
|
||||||
|
|
||||||
The dense 27B decodes slower per token than the MoE 30B A3B class. Expect
|
|
||||||
the lower half of the ranges at full context.
|
|
||||||
|
|
||||||
### With Speculative Decoding
|
|
||||||
|
|
||||||
Speculative decoding (MTP, DFlash, or ngram-mod) raises effective decode.
|
|
||||||
Tightwad measured 1.86x on a slow RPC pool. Estimate the effective rate at
|
|
||||||
1.4-1.8x the raw RPC decode:
|
|
||||||
|
|
||||||
| Link | Raw decode (t/s) | With spec (t/s) |
|
|
||||||
| ---- | ---------------- | --------------- |
|
|
||||||
| 1 GbE | 8-15 | 12-24 |
|
|
||||||
| 2.5 GbE | 10-18 | 15-28 |
|
|
||||||
| 10 GbE | 12-20 | 18-32 |
|
|
||||||
|
|
||||||
## Context That Fits
|
|
||||||
|
|
||||||
The two real machines give about 51 GiB of usable memory combined
|
|
||||||
(24 GiB VRAM plus about 27 GiB on the Strix Halo, which has 30 GiB physical
|
|
||||||
RAM; the 96 GiB driver ceiling is not physical). A 29-32 GB model leaves
|
|
||||||
about 20-22 GiB for KV cache. The Qwen3.6-27B KV cache costs about 85-100
|
|
||||||
KiB per token with q8_0 K and f16 V (measured on the large service at
|
|
||||||
602100 tokens = about 50 GB). That budget gives a total context pool of
|
|
||||||
roughly 200k-260k tokens across both nodes.
|
|
||||||
|
|
||||||
## Caveats
|
|
||||||
|
|
||||||
- Every node must run the exact same llama.cpp build. Version skew hangs
|
|
||||||
the handshake.
|
|
||||||
- The slowest node sets the pace for every token.
|
|
||||||
- Model load crosses the network once per start. Expect minutes, not
|
|
||||||
seconds, for the split load.
|
|
||||||
- These are estimates. The exact combination of an RTX 3090 desktop and a
|
|
||||||
Strix Halo NUC has no published benchmark yet.
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[Ethernet Bandwidth Math]]
|
|
||||||
- [[Qwen3.8 PP Deployment]]
|
|
||||||
- [[Sample Script Set]]
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "The real split deployment: which node is head and which is worker, the flags, and the verified gotchas."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
- pipeline
|
|
||||||
timestamp: 2026-09-03
|
|
||||||
---
|
|
||||||
|
|
||||||
> **SUPERSEDED 2026-08-17.** The Frank redo retired this topology.
|
|
||||||
> See [frank-redo-2026-08-17.md](../frank-redo-2026-08-17.md) and
|
|
||||||
> [deployment.md](../deployment.md) for the active configuration.
|
|
||||||
>
|
|
||||||
> **2026-10-18: nothing pipeline-parallel runs on the fleet.** No RPC listener
|
|
||||||
> exists on either host (`ss -ltn` shows no :50052), every artifact named below
|
|
||||||
> is gone (§Files), and the worker hardware left the box: the RTX 3090 was
|
|
||||||
> removed from Dankserver on 2026-09-15 (it now sits in Dankclaw) and the server
|
|
||||||
> runs 2× R9700. The live multi-GPU scheme on Dankserver is tensor parallelism
|
|
||||||
> inside one process ([[Dual-R9700-Cooperation]],
|
|
||||||
> [[frank-archon]]).
|
|
||||||
|
|
||||||
# Qwen3.8-27B Pipeline-Parallel Deployment (Dankclaw + Dankserver)
|
|
||||||
|
|
||||||
Split Qwen3.8-27B across the Strix Halo iGPU (head) and the RTX
|
|
||||||
3090 (worker) using llama.cpp RPC. This page is the design record and the
|
|
||||||
morning-of runbook. Research background:
|
|
||||||
[Qwen3.8 Strix optimizations](../qwen3.8-strix-optimizations.md).
|
|
||||||
|
|
||||||
## Production state (2026-08-17: Q6_K, q8_0/f16 KV, split 0.68)
|
|
||||||
|
|
||||||
This split is the production `frank-dragoon` service. Current
|
|
||||||
state, verified against the live journal and output smoke tests
|
|
||||||
(2026-08-17, load 02:06):
|
|
||||||
|
|
||||||
- Model: `Qwen3.8-27B-Uncensored-Q6_K.gguf` (22.43 GB, imatrix, no
|
|
||||||
bundled MTP head). Draft: `Qwen3.8-27B-Uncensored-draft-Q8_0.gguf`
|
|
||||||
(3.16 GB) via `--spec-draft-model`, running on ROCm0 only (1718 MiB;
|
|
||||||
zero worker impact — measured, not assumed).
|
|
||||||
- Split `0.68,0.32` = 45 of 65 blocks (0-44) on the 3090 — 69% of the
|
|
||||||
layer weight. Maximum for this config; see "Split limits" below.
|
|
||||||
- KV: K `q8_0` + V `f16` (mismatched), 784 MiB per FA layer: RPC0 8624
|
|
||||||
MiB (11 FA) + ROCm0 3920 MiB (5 FA). Flash Attention ENABLED despite
|
|
||||||
the mismatch (`resolve_fused_ops: Flash Attention enabled`) via the
|
|
||||||
GGML_CUDA_FA_ALL_QUANTS rebuild — see "FA-all-quants rebuild" below.
|
|
||||||
- Pool `262144`, `PARALLEL=1`, live `n_ctx_slot = 262144`.
|
|
||||||
* MTP: standalone draft head, acceptance 0.83 then 0.75, mean len 4.0-4.3,
|
|
||||||
per-position ~0.86/0.71/0.71/0.71 — stronger at every position than the old bundled
|
|
||||||
head, which measured 0.739/0.554/0.427/0.340.
|
|
||||||
- Spec chain as deployed: `--spec-draft-n-max 4` on the standalone draft, with
|
|
||||||
the `--spec-default` ngram-mod fallback at n_match 24 / n_min 48 / n_max 64
|
|
||||||
(the preset that survived the DSpark/DFlash retirements —
|
|
||||||
[[qwen3.8-spec-matrix]]).
|
|
||||||
- Batch 8192 / ubatch 256, `--direct-io`, `--fit off`, `--gpu-layers all`. The
|
|
||||||
pi `models.json` declared 262144 (`contextWindow = maxTokens`, the house
|
|
||||||
convention of that era).
|
|
||||||
- Head GPU side: **ROCm0** (no `--device` flag; HIP registers before
|
|
||||||
Vulkan).
|
|
||||||
- Worker steady state: 23854/24576 MiB used, ~273 MiB free (tight but
|
|
||||||
stable; all allocations are upfront).
|
|
||||||
- Unit `frank-dragoon.service`, launched through
|
|
||||||
`serve-pp-head.sh` + `frank-dragoon.env`.
|
|
||||||
|
|
||||||
## Service enablement (2026-08-17)
|
|
||||||
|
|
||||||
The PP pair is the only inference service enabled for boot. Both units
|
|
||||||
(`frank-dragoon` on Dankclaw, `frank-pp-worker` on Dankserver) are
|
|
||||||
enabled. The NPU unit (`frank-zealot` on Dankclaw) and all retired
|
|
||||||
units are disabled. Restart order: worker first, then head. The Dankserver
|
|
||||||
helper `frank-restart-small.sh` is obsolete (it restarts the removed
|
|
||||||
`frank-zealot` unit); restart the worker with
|
|
||||||
`sudo systemctl restart frank-pp-worker`. The Dankclaw helper
|
|
||||||
`frank-restart-large.sh` (sync + cache drop + restart) is correct for the
|
|
||||||
head. The "deploy.sh dankserver is broken until retargeted" note is long fixed:
|
|
||||||
`./scripts/deploy.sh dankserver --service frank-archon` is the documented
|
|
||||||
invocation today (`scripts/deploy.sh:38`, dispatch case
|
|
||||||
`dankserver:frank-archon` at `scripts/deploy.sh:149`). See
|
|
||||||
[[deployment]] for the full boot-policy table.
|
|
||||||
|
|
||||||
## FA-all-quants rebuild (2026-08-17)
|
|
||||||
|
|
||||||
Flash Attention with mismatched K/V cache types requires the
|
|
||||||
`GGML_CUDA_FA_ALL_QUANTS` compile definition. Without it,
|
|
||||||
`ggml/src/ggml-cuda/fattn.cu` gates FA off when `K->type != V->type`
|
|
||||||
(`#ifndef GGML_CUDA_FA_ALL_QUANTS`). With it, the K==V check is skipped
|
|
||||||
and all quantized-K combos are compiled. The head (HIP gfx1151) and the
|
|
||||||
worker (CUDA sm_86) compile the same sources, so the same CMake flag
|
|
||||||
covers both:
|
|
||||||
|
|
||||||
cmake -S . -B build -DGGML_CUDA_FA_ALL_QUANTS=ON
|
|
||||||
cmake --build build -j --target llama-server # dankclaw
|
|
||||||
cmake --build build -j --target ggml-rpc-server # dankserver
|
|
||||||
|
|
||||||
Both hosts at commit 9b05354ec (b10433). The flag was already cached
|
|
||||||
ON in the worker build (Aug 16 objects carry it); the head required a
|
|
||||||
full ggml-hip rebuild. Keep both binaries on the same commit — version
|
|
||||||
skew hangs the RPC handshake.
|
|
||||||
|
|
||||||
## Split limits (measured 2026-08-17, Q6_K + q8_0/f16 KV)
|
|
||||||
|
|
||||||
KV per FA layer at 262144 = 784 MiB (K q8_0 1088 B/token + V f16 2048
|
|
||||||
B/token). Layer pool 20334 MB (GGUF scan, exact), embd on the head side
|
|
||||||
(RPC0 buffer 10429.88 MiB at f=0.53 -> the split pool excludes embd).
|
|
||||||
|
|
||||||
- f=0.53 -> 35 blocks, 8 FA, KV 6272 MiB, 5721 MiB free. Phase 1.
|
|
||||||
- f=0.68 -> 45 blocks (0-44), 11 FA (3..43), KV 8624 MiB, 273 MiB
|
|
||||||
free. PRODUCTION (the max): 46 blocks leaves ~0.3 GiB; 48 blocks
|
|
||||||
(f >= 0.7197, the round(f x 66) boundary) adds FA block 47 -> KV
|
|
||||||
9408 -> OOM by ~0.85 GiB.
|
|
||||||
|
|
||||||
The KV-per-device count = round(f x 66) layers, independent of the
|
|
||||||
weight boundary (same rule as the Q8_K_XL config). FA blocks at
|
|
||||||
3,7,...,63.
|
|
||||||
|
|
||||||
## Split limits (measured 2026-08-17, Q8_K_XL + f16/f16 KV) — superseded
|
|
||||||
|
|
||||||
The 0.40 split (2026-08-16, 2x200700 pool) was KV-bound at 26 blocks.
|
|
||||||
With the pool shrunk to 262144, the KV per full-attention layer drops to
|
|
||||||
exactly 1024 MiB, freeing headroom. Measurements:
|
|
||||||
|
|
||||||
- 0.50,0.50 -> 32 blocks (0-31), 8 FA on RPC0, KV 8192 MiB, 1477 MiB
|
|
||||||
free. Healthy (2026-08-17 00:29 load).
|
|
||||||
- 0.55,0.45 -> 36 blocks (0-35): the 9th FA block, KV 9216 MiB ->
|
|
||||||
cudaMalloc OOM by ~740 MiB.
|
|
||||||
- 0.54,0.46 -> weights 35 blocks (0-34) but KV still 9216 MiB -> OOM.
|
|
||||||
The KV-per-device count rounds the split fraction over 66 layers
|
|
||||||
(round(f x 66)), independent of the weight boundary; the 9-FA flip
|
|
||||||
sits at f ~= 0.538, just below the weight threshold for block 35, so
|
|
||||||
the 35-block config is unreachable.
|
|
||||||
- 0.53,0.47 -> 34 blocks (0-33), 8 FA, KV 8192 MiB. Safe under every
|
|
||||||
rounding rule (round/floor/ceil of 0.53 x 66 = 34.98). Production.
|
|
||||||
|
|
||||||
GGUF block scan (exact sizes): FA blocks at 3,7,...,63 (409.6 MB each),
|
|
||||||
linear blocks 431.3 MB; total layer weight 27720 MB. The split pool
|
|
||||||
includes the MTP head for weights and token_embd for KV — the two
|
|
||||||
boundaries diverge, which is why 0.54 split them apart.
|
|
||||||
|
|
||||||
## Cache incident (2026-08-17): garbage output from a full worker disk
|
|
||||||
|
|
||||||
Symptom: after a split change, the service loaded "healthy" but every
|
|
||||||
completion degenerated to a repeated junk token (e.g. 48 x "/", empty
|
|
||||||
content, degenerate logits). Root cause chain:
|
|
||||||
|
|
||||||
1. The worker RPC tensor cache keys per session. Each model load writes
|
|
||||||
a fresh full copy (~15 GB); over ~5 sessions of two models it grew to
|
|
||||||
4434 files / 96 GB.
|
|
||||||
2. The Dankserver root disk filled to 100%. Cache writes started
|
|
||||||
failing, leaving 0-byte entries. A subsequent reload hit corrupt
|
|
||||||
cache entries and the worker served garbage weights for the
|
|
||||||
newly-needed layers without validation.
|
|
||||||
3. Fix: move the cache to .AGENTTRASH (then empty it), restart the
|
|
||||||
worker, free the disk, reload the head (full re-stream ~2.5 min).
|
|
||||||
A clean cache and a healthy disk produced correct output immediately
|
|
||||||
(acceptance 0.89 on the first smoke test).
|
|
||||||
|
|
||||||
Guard was `scripts/prune-rpc-cache.sh` on Dankserver (cron, 40 GiB cap). That
|
|
||||||
script is gone from the repo and from the host, yet the crontab line still
|
|
||||||
runs every 10 minutes against it
|
|
||||||
(`*/10 * * * * /home/dank/vLLM/scripts/prune-rpc-cache.sh`, `crontab -l`, read
|
|
||||||
2026-10-18 — the target does not exist, so it fails silently). Never let the
|
|
||||||
cache directory grow unchecked; a full disk silently corrupts it. Signature to
|
|
||||||
recognize: healthy load + degenerate repeated-token output.
|
|
||||||
|
|
||||||
## Topology
|
|
||||||
|
|
||||||
```
|
|
||||||
Dankclaw (head, 192.168.0.143) Dankserver (worker, 192.168.0.235)
|
|
||||||
llama-server b10433 ggml-rpc-server b10433
|
|
||||||
--rpc 192.168.0.235:50052 --host 192.168.0.235 --port 50052
|
|
||||||
model file (31.5 GB) -d CUDA0 -c (tensor cache)
|
|
||||||
[RPC0, ROCm0|Vulkan0] layers RTX 3090: layers 0-44 (45 of 65 blocks)
|
|
||||||
Strix: layers 45-64 + lm_head
|
|
||||||
hidden state 10 KiB/position/direction (H=5120, GGUF-verified);
|
|
||||||
measured wire bytes ~40 KB/position/direction (serialized graph
|
|
||||||
dominates); 20.5 MB per ubatch both directions. Details:
|
|
||||||
[[Ethernet Bandwidth Math]] section 0.
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Layer-sequential split** (true pipeline parallelism): layers 0..N run on
|
|
||||||
the worker, the rest on the head, in order. One hidden-state handoff per
|
|
||||||
token step. DECODE is RTT-bound, not bandwidth-bound (network is ~1-2.5%
|
|
||||||
of step time). PREFILL and the prompt-cache KV snapshot ARE
|
|
||||||
bandwidth-bound: the link runs at line rate for the wire burst of every
|
|
||||||
ubatch and ~100% during the snapshot copy. Measured numbers:
|
|
||||||
[[Ethernet Bandwidth Math]] section 0.
|
|
||||||
- **Why the head is on Dankclaw:** the model file is already there; the
|
|
||||||
worker streams its tensor share at load (no 31.5 GB copy to the server).
|
|
||||||
- **Both sides must run the identical build** (b10433 / 9b05354ec). The
|
|
||||||
worker build lived in a worktree `/home/dank/llama-cpp-rpc` on Dankserver so the
|
|
||||||
production b10121 build stayed untouched (that path no longer exists, read
|
|
||||||
2026-10-18). A version skew hangs the RPC handshake with no error.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
| File | Machine | Purpose |
|
|
||||||
|------|---------|---------|
|
|
||||||
| `scripts/frank-pp-worker.env` | server | worker tunables (bind, port, device, cache) |
|
|
||||||
| `scripts/serve-pp-worker.sh` | server | runs `ggml-rpc-server` from the b10433 worktree build |
|
|
||||||
| `scripts/frank-dragoon.env` | claw | head tunables (split, ctx, spec, KV, I/O); was `frank-pp-head.env` before the 2026-08-16 rename |
|
|
||||||
| `scripts/serve-pp-head.sh` | claw | runs `llama-server --rpc ...` (see "Gotchas") |
|
|
||||||
| `scripts/frank-dragoon.service` / `frank-pp-worker.service` | both | optional systemd units (manual install); the head unit was renamed from `frank-pp-head.service` on 2026-08-16 and is symlinked from the repo into `/etc/systemd/system` |
|
|
||||||
| `scripts/pp-test.sh` | any box with ssh to both | test orchestrator (preflight/start/verify/bench/stop-pp/restore) |
|
|
||||||
| `scripts/pp-bench.py` | claw | API driver; reports server-side `print_timing` |
|
|
||||||
|
|
||||||
|
|
||||||
**Every PP file in that table except `scripts/frank-dragoon.env` no longer
|
|
||||||
exists** (checkout read 2026-10-18: `frank-pp-worker.env`,
|
|
||||||
`serve-pp-worker.sh`, `serve-pp-head.sh`, `frank-pp-worker.service`,
|
|
||||||
`pp-test.sh`, `pp-bench.py` and `prune-rpc-cache.sh` are all absent), and
|
|
||||||
`scripts/frank-dragoon.env` is today the halogen-flash-server env, not a PP
|
|
||||||
head env ([[frank-dragoon]]).
|
|
||||||
|
|
||||||
## Gotchas (all verified in b10433 source / live tests, 2026-08-15)
|
|
||||||
|
|
||||||
1. **No `--device` on the head.** Setting `params.devices` makes
|
|
||||||
`llama_prepare_model_devices` use exactly that list; the RPC device is
|
|
||||||
silently dropped and `--rpc` becomes a no-op. Omit it; default selection
|
|
||||||
puts RPC first.
|
|
||||||
2. **The iGPU side is ROCm by default.** Backends register in fixed order
|
|
||||||
(HIP before Vulkan); the iGPU dedup rule keeps the first backend's view.
|
|
||||||
Live check showed `[RPC0, ROCm0]`. For a Vulkan side, use the second
|
|
||||||
head build `/home/dank/llama.cpp/build-vulkan-rpc` (HIP off; removed — the
|
|
||||||
path is gone on 2026-10-18) — set `HEAD_BIN` in the env. Trade: ROCm side =
|
|
||||||
faster prefill, ~3% slower decode (measured on this box).
|
|
||||||
3. **Pin `--tensor-split 1,1`.** The default split is proportional to
|
|
||||||
*reported free memory*: the Strix reports up to 96 GiB (GTT) vs the
|
|
||||||
3090's 24 GB, so the default lands ~20/80 on the slow device. 1,1 = 50/50 =
|
|
||||||
balanced per-layer latency. The 2026-08-15 4B smoke test that proved it
|
|
||||||
(production untouched): at a 0.2/0.8 split only 4 of 36 layers reached the
|
|
||||||
3090, and at a real split worker VRAM rose ~819 MiB with 95 t/s prefill /
|
|
||||||
58.8 t/s decode on the tiny model; link RTT ~0.3–0.5 ms.
|
|
||||||
4. **`-dio` on the head.** Large model + RPC hangs at load without direct
|
|
||||||
I/O.
|
|
||||||
5. **RPC is unauthenticated and unencrypted.** LAN only. The worker binds
|
|
||||||
0.0.0.0-equivalent by design (`--host 192.168.0.235`).
|
|
||||||
6. **Env var names.** Never `TEMP`, `HOST`, or `GPU_MAX_HEAP_SIZE`
|
|
||||||
(HIP runtime reads them; the 2026-08-13 `TEMP` incident).
|
|
||||||
7. **Page-cache state confounds loads.** `pp-test.sh start` drops caches on
|
|
||||||
the head before load (warm-cache loads crash fast; cold loads survive).
|
|
||||||
|
|
||||||
## Runbook (morning of 2026-08-16)
|
|
||||||
|
|
||||||
Run from any box with ssh access to both (repo path on claw:
|
|
||||||
`/home/dank/vllmScripts`, on server: `/home/dank/vLLM` — both still exist as
|
|
||||||
stale checkouts; the deployment checkout today is `/home/dank/serverAppConfigs`
|
|
||||||
on both hosts).
|
|
||||||
|
|
||||||
```
|
|
||||||
# 0. one-time: pull the kit on both boxes
|
|
||||||
ssh dank@dankclaw 'cd /home/dank/vllmScripts && git pull'
|
|
||||||
ssh dank@dankserver 'cd /home/dank/vLLM && git pull'
|
|
||||||
|
|
||||||
# 1. checks (read-only)
|
|
||||||
./scripts/pp-test.sh preflight
|
|
||||||
|
|
||||||
# 2. full run: stop prod -> start PP -> verify split -> bench -> restore
|
|
||||||
FRANK_SUDO_PW=<pw> ./scripts/pp-test.sh all
|
|
||||||
|
|
||||||
# or step by step:
|
|
||||||
FRANK_SUDO_PW=<pw> ./scripts/pp-test.sh start
|
|
||||||
./scripts/pp-test.sh verify # layer split + worker VRAM
|
|
||||||
./scripts/pp-test.sh bench 4 256 2 # ~4k prompt, 256 out, 2 reps
|
|
||||||
./scripts/pp-test.sh bench 16 256 1
|
|
||||||
FRANK_SUDO_PW=<pw> ./scripts/pp-test.sh stop-pp
|
|
||||||
FRANK_SUDO_PW=<pw> ./scripts/pp-test.sh restore
|
|
||||||
```
|
|
||||||
|
|
||||||
Config variants: edit `scripts/frank-dragoon.env` on the claw (git commit +
|
|
||||||
pull if the bench runs from the repo), then re-run `start` + `bench`:
|
|
||||||
|
|
||||||
- No spec baseline: `SPEC_NONE=1`
|
|
||||||
- Vulkan head side: `HEAD_BIN=/home/dank/llama.cpp/build-vulkan-rpc/bin/llama-server`
|
|
||||||
- Q4_0 MTP draft head: add `--spec-draft-model /home/dank/models/Qwen3.8-27B-MTP-Q4_0.gguf` to the serve script's SPEC block (env hook: see `SPEC_DRAFT_MODEL` below)
|
|
||||||
- n_max sweep: `SPEC_DRAFT_N_MAX=3|4|5`
|
|
||||||
- Plain Q8_0: `MODEL_FILE=Qwen3.8-27B-Q8_0.gguf` (file must be on the claw)
|
|
||||||
|
|
||||||
Verification checklist after each `start`:
|
|
||||||
|
|
||||||
1. `verify` shows `using device RPC0 ... using device ROCm0|Vulkan0`.
|
|
||||||
2. Layer assignment lines (`-lv 10`): worker gets the first ~32 layers at a
|
|
||||||
1,1 split.
|
|
||||||
3. `nvidia-smi` on the server showed ~15–16 GB used by the worker
|
|
||||||
(production small service + worker share). No NVIDIA card is in that host
|
|
||||||
today — the equivalent check is `rocm-smi --showmeminfo vram`.
|
|
||||||
4. Head GTT usage (rocm-smi) well under 96 GiB; model fully on devices
|
|
||||||
(`--gpu-layers all` offloads everything; CPU holds embeddings/lm_head glue).
|
|
||||||
|
|
||||||
## Expected numbers
|
|
||||||
|
|
||||||
Per-layer: 3090 ≈ 2–3× faster than the 8060S. 50/50 split → each token
|
|
||||||
step ≈ max(32 layers on 3090, 32 layers on Strix) + ~0.5 ms RTT.
|
|
||||||
|
|
||||||
- Production reference: 25 t/s @16k (Vulkan, spec, single node).
|
|
||||||
- Target: **≥ 30 t/s @4k, ≥ 20 t/s @16k** with spec.
|
|
||||||
- Decode floor: with ngram/mod acceptance, spec multiplies the raw rate;
|
|
||||||
raw (no spec) rate should clear ~15–18 t/s @16k for the target to hold.
|
|
||||||
|
|
||||||
## Rollback
|
|
||||||
|
|
||||||
`pp-test.sh restore` starts both production services. If a PP test wedges a
|
|
||||||
box: `systemctl restart <unit>` after killing the stray processes
|
|
||||||
(`pkill -x llama-server` / `pkill -x ggml-rpc-server` — never `pkill -f`
|
|
||||||
with a pattern matching your own ssh command line).
|
|
||||||
|
|
||||||
## Open questions for the test day
|
|
||||||
|
|
||||||
1. Does the MTP head (which sits in the last layer group, iGPU side) draft
|
|
||||||
cleanly with the verify pass crossing the RPC boundary? Watch acceptance
|
|
||||||
in the head log (`draft acceptance = X`).
|
|
||||||
2. Two parallel slots on the split (PARALLEL=2, CTX 65536): RPC is
|
|
||||||
serialized per compute — expect the 2-slot speedup to be weaker than
|
|
||||||
single-node. Measure, do not assume.
|
|
||||||
3. Prefill parity: ROCm-side head should prefill ≥ single-node ROCm; if not,
|
|
||||||
the ubatch crossing the RPC boundary is the suspect.
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Sample Script Set
|
|
||||||
description: "An educational two-node RPC split script set, for reading only — never run it or deploy it."
|
|
||||||
tags:
|
|
||||||
- pipeline-parallelism
|
|
||||||
- sample
|
|
||||||
- llamacpp
|
|
||||||
- educational
|
|
||||||
timestamp: 2026-08-14
|
|
||||||
---
|
|
||||||
|
|
||||||
# Sample Script Set — Two-Node Qwen3.6-27B Q8 Split
|
|
||||||
|
|
||||||
> WARNING. This page is for educational reading only. Do not run these
|
|
||||||
> scripts. Do not deploy them. They are a template, not a tested product.
|
|
||||||
> The RPC backend is a proof of concept that the upstream README calls
|
|
||||||
> "fragile and insecure". Running it on a production network is unsafe.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Run a Qwen3.6-27B Q8 model (about 29 to 32 GB of weights) across two
|
|
||||||
machines using llama.cpp RPC pipeline parallelism. Node A is an NVIDIA
|
|
||||||
desktop with an RTX 3090. Node B is a Strix Halo NUC (Ryzen AI MAX+ 395,
|
|
||||||
Radeon 8060S). Use the largest context that fits on both nodes together.
|
|
||||||
|
|
||||||
## How the Split Works
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
subgraph NodeA[NVIDIA Desktop — HEAD]
|
|
||||||
A1[llama-server<br/>--model 27B-Q8.gguf<br/>--rpc nodeB:50052]
|
|
||||||
end
|
|
||||||
subgraph NodeB[Strix Halo NUC — WORKER]
|
|
||||||
B1[ggml-rpc-server<br/>--host nodeB --port 50052<br/>--device CUDA0? no — ROCm]
|
|
||||||
end
|
|
||||||
A1 <-->|hidden states over ethernet| B1
|
|
||||||
```
|
|
||||||
|
|
||||||
Node A runs the head `llama-server`. Node B runs `ggml-rpc-server`, which
|
|
||||||
exposes its GPU as a remote device. The head splits weights and KV cache
|
|
||||||
across its own GPU and the remote device in proportion to free memory.
|
|
||||||
Each token generation passes the hidden state once across the boundary.
|
|
||||||
|
|
||||||
Key facts that shape the scripts:
|
|
||||||
|
|
||||||
- Both nodes must run the exact same llama.cpp build tag. Version skew
|
|
||||||
hangs the handshake.
|
|
||||||
- The head build needs its backend plus RPC. The worker build needs its
|
|
||||||
backend plus RPC. CUDA and ROCm do not mix inside one binary; each node
|
|
||||||
uses its own backend.
|
|
||||||
- Speculative decoding runs on the head. The draft model loads on the head.
|
|
||||||
- The model file must be present on every node with the same bytes.
|
|
||||||
|
|
||||||
## File Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
pipeline-pp/
|
|
||||||
README.md
|
|
||||||
build.sh # build llama.cpp with RPC on each node
|
|
||||||
workers/worker-strix.sh # run ggml-rpc-server on the Strix Halo NUC
|
|
||||||
head/head-nvidia.sh # run llama-server on the NVIDIA desktop
|
|
||||||
head/frank-27b-pp.env # tunables for the head
|
|
||||||
head/verify.sh # health and split check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Script
|
|
||||||
|
|
||||||
File: `build.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Build llama.cpp with RPC support. Run ONE build per node, with that
|
|
||||||
# node's own backend enabled. Educational only. Do not run in production.
|
|
||||||
set -euo pipefail
|
|
||||||
LLAMA_DIR="$1" # path to a llama.cpp checkout
|
|
||||||
NODE="$2" # "nvidia" or "strix"
|
|
||||||
|
|
||||||
cmake -S "$LLAMA_DIR" -B "$LLAMA_DIR/build-pp" \
|
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
|
||||||
-DGGML_RPC=ON \
|
|
||||||
$( [ "$NODE" = nvidia ] && echo -DGGML_CUDA=ON \
|
|
||||||
|| echo -DGGML_HIP=ON -DGGML_HIP_PLATFORM=amd ) \
|
|
||||||
-DGGML_CUDA_NO_GGVK=ON 2>/dev/null || true
|
|
||||||
cmake --build "$LLAMA_DIR/build-pp" --config Release -j
|
|
||||||
# Expected binaries: build-pp/bin/ggml-rpc-server, llama-server, llama-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes: the HEAD does not need a GPU if you offload everything to RPC
|
|
||||||
workers. Keep the head on the NVIDIA desktop so it can also use its local
|
|
||||||
3090 for part of the split.
|
|
||||||
|
|
||||||
## Worker Script (Node B, Strix Halo NUC)
|
|
||||||
|
|
||||||
File: `workers/worker-strix.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Expose the Strix Halo GPU as an RPC device for a remote head.
|
|
||||||
# Educational only. Do not run on an unprotected network.
|
|
||||||
set -euo pipefail
|
|
||||||
BIN=/home/dank/llama.cpp/build-pp/bin
|
|
||||||
# Bind to the LAN address. The default binds 127.0.0.1 only.
|
|
||||||
HOST=192.168.0.143 # the Strix Halo NUC address
|
|
||||||
PORT=50052
|
|
||||||
# -c enables a local model-file cache so the split load runs once, not per boot.
|
|
||||||
exec "$BIN/ggml-rpc-server" --host "$HOST" --port "$PORT" --cache
|
|
||||||
# Use --device to name the backend device explicitly if auto-detect fails:
|
|
||||||
# ggml-rpc-server --host $HOST --port $PORT --device gfx1151
|
|
||||||
```
|
|
||||||
|
|
||||||
Security note: `ggml-rpc-server` exposes all memory to any client that can
|
|
||||||
reach the port. Keep it on a trusted LAN. Do not bind it to the public
|
|
||||||
internet.
|
|
||||||
|
|
||||||
## Head Tunables (Node A, NVIDIA Desktop)
|
|
||||||
|
|
||||||
File: `head/frank-27b-pp.env`
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Educational tunables for the two-node split. Do not deploy.
|
|
||||||
MODEL_DIR=/home/dank/models
|
|
||||||
MODEL_FILE=Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf # ~32 GB
|
|
||||||
# MUCH smaller: use Q8_0 (29.12 GB) if the split must fit more KV.
|
|
||||||
# Smaller still: Q6_K ~24 GB leaves several GB more room for KV.
|
|
||||||
MODEL="$MODEL_DIR/$MODEL_FILE"
|
|
||||||
|
|
||||||
# Worker (Strix Halo NUC). Multiple --rpc entries allowed; one here.
|
|
||||||
RPC_SERVERS=192.168.0.143:50052
|
|
||||||
|
|
||||||
# Context. 262144 is the native max for this model. Drop to 200000 if
|
|
||||||
# memory pressure is too high. This is the single-slot ceiling.
|
|
||||||
CTX_SIZE=262144
|
|
||||||
PARALLEL=1
|
|
||||||
|
|
||||||
# KV cache on GPU. q8_0 K / f16 V saves memory over f16/f16.
|
|
||||||
CACHE_TYPE_K=q8_0
|
|
||||||
CACHE_TYPE_V=f16
|
|
||||||
|
|
||||||
# Generation batching. Decode is latency-bound over RPC; keep batch modest.
|
|
||||||
BATCH_SIZE=1024
|
|
||||||
UBATCH_SIZE=512
|
|
||||||
|
|
||||||
# Compute. Match the physical core count on the head box.
|
|
||||||
THREADS=7
|
|
||||||
|
|
||||||
# Sampling (HauhauCS thinking-general defaults).
|
|
||||||
TEMPERATURE=1.0
|
|
||||||
TOP_K=20
|
|
||||||
TOP_P=0.95
|
|
||||||
MIN_P=0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Head Script (Node A, NVIDIA Desktop)
|
|
||||||
|
|
||||||
File: `head/head-nvidia.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Run the split model head on the NVIDIA desktop.
|
|
||||||
# Educational only. Do not run in production.
|
|
||||||
set -euo pipefail
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source "$SCRIPT_DIR/frank-27b-pp.env"
|
|
||||||
|
|
||||||
BIN=/home/dank/llama.cpp/build-pp/bin
|
|
||||||
PORT=7770
|
|
||||||
|
|
||||||
RPC_ARGS=()
|
|
||||||
for s in $RPC_SERVERS; do RPC_ARGS+=(--rpc "$s"); done
|
|
||||||
|
|
||||||
# Pick ONE spec variant by uncommenting the matching pair below.
|
|
||||||
|
|
||||||
# --- Variant 1: MTP (recommended by research) ---------------------------
|
|
||||||
# SPEC="--spec-type draft-mtp --spec-draft-model $MODEL_DIR/Qwen3.6-27B-MTP-ONLY-Q4_K_M.gguf --spec-draft-n-max 3"
|
|
||||||
|
|
||||||
# --- Variant 2: DFlash (block-diffusion draft) ---------------------------
|
|
||||||
# SPEC="--spec-type draft-dflash --spec-draft-model $MODEL_DIR/Qwen3.6-27B-DFlash-Q8_0.gguf --spec-draft-n-max 8"
|
|
||||||
|
|
||||||
# --- Variant 3: ngram-mod (built-in, no draft file) ----------------------
|
|
||||||
# SPEC="--spec-type ngram-mod --spec-ngram-mod-n-min 48 --spec-ngram-mod-n-max 64 --spec-ngram-mod-n-match 24"
|
|
||||||
|
|
||||||
# --- Variant 4: no speculation --------------------------------------------
|
|
||||||
SPEC="--spec-type none"
|
|
||||||
|
|
||||||
exec "$BIN/llama-server" \
|
|
||||||
--model "$MODEL" \
|
|
||||||
"${RPC_ARGS[@]}" \
|
|
||||||
${SPEC} \
|
|
||||||
--ctx-size "$CTX_SIZE" \
|
|
||||||
--parallel "$PARALLEL" \
|
|
||||||
--cache-type-k "$CACHE_TYPE_K" \
|
|
||||||
--cache-type-v "$CACHE_TYPE_V" \
|
|
||||||
--batch-size "$BATCH_SIZE" \
|
|
||||||
--ubatch-size "$UBATCH_SIZE" \
|
|
||||||
--threads "$THREADS" \
|
|
||||||
--host 0.0.0.0 --port "$PORT" \
|
|
||||||
--alias frank-27b-pp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Spec Variant Summary
|
|
||||||
|
|
||||||
| Variant | draft model file | --spec-type | Notes |
|
|
||||||
| ------- | ---------------- | ----------- | ----- |
|
|
||||||
| MTP | `Qwen3.6-27B-MTP-ONLY-Q4_K_M.gguf` (~3B) | `draft-mtp` | Parallel-safe, best in benchmarks |
|
|
||||||
| DFlash | `Qwen3.6-27B-DFlash-Q8_0.gguf` | `draft-dflash` | Known premature-EOS bug |
|
|
||||||
| ngram-mod | none (built-in) | `ngram-mod` | Zero VRAM cost |
|
|
||||||
| DSpark | vLLM only | (not in llama.cpp) | Use vLLM PP instead; see below |
|
|
||||||
|
|
||||||
DSpark does not exist in llama.cpp. It is a vLLM sparse-attention draft. If
|
|
||||||
you want DSpark, use vLLM pipeline parallelism, which has its own limits
|
|
||||||
(no spec decode with PP as of 0.15.x). The llama.cpp path here covers
|
|
||||||
MTP, DFlash, and ngram-mod.
|
|
||||||
|
|
||||||
## Verify (Node A)
|
|
||||||
|
|
||||||
File: `head/verify.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Check the split model is up and that both nodes are in the split.
|
|
||||||
set -euo pipefail
|
|
||||||
PORT=7770
|
|
||||||
# Health must return "ok" after model load (minutes, not seconds).
|
|
||||||
curl -sf "http://127.0.0.1:$PORT/health"
|
|
||||||
echo
|
|
||||||
# Show the device split from the server log.
|
|
||||||
curl -sf "http://127.0.0.1:$PORT/props" | head -40
|
|
||||||
# Expected: the model and KV cache distributed across CUDA0 (node A)
|
|
||||||
# and the remote RPC device (node B).
|
|
||||||
```
|
|
||||||
|
|
||||||
## Context Sizing Math
|
|
||||||
|
|
||||||
The model is about 29 to 32 GB. The two machines give about 51 GiB of
|
|
||||||
usable memory. That leaves about 20 to 22 GiB for KV cache. The
|
|
||||||
Qwen3.6-27B KV cache costs about 85 to 100 KiB per token at q8_0 K /
|
|
||||||
f16 V. The budget therefore gives a pool of roughly 200k to 260k tokens.
|
|
||||||
A single 262144-token slot is the practical ceiling for this split.
|
|
||||||
|
|
||||||
To run more context, shrink the model quantization. Q6_K (about 24 GB)
|
|
||||||
frees several GB for KV. Do not try to run the 96 GiB driver figure on the
|
|
||||||
Strix Halo as real memory. It is an overcommit ceiling; the physical RAM
|
|
||||||
is 30 GiB.
|
|
||||||
|
|
||||||
## What This Does Not Cover
|
|
||||||
|
|
||||||
- The remote draft-model-over-RPC path is not confirmed. The draft stays
|
|
||||||
on the head.
|
|
||||||
- `--tensor-split` is available to force a manual layer split. The sample
|
|
||||||
uses the automatic proportional split.
|
|
||||||
- RDMA (RoCEv2) transport exists but needs a compatible NIC. It adds a few
|
|
||||||
percent over TCP.
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[Qwen3.8 PP Deployment]]
|
|
||||||
- [[Expected Performance]]
|
|
||||||
- [[Ethernet Bandwidth Math]]
|
|
||||||
- [[DFlash DSpark Speculative Decoding]]
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Pipeline Parallelism
|
|
||||||
|
|
||||||
This section covers running one large model across two machines. It focuses
|
|
||||||
on pipeline parallelism (layer split) over ethernet. The research answers
|
|
||||||
what speeds are possible and which tools work. The sample scripts show a
|
|
||||||
two-node Qwen3.6-27B Q8 split for reading only.
|
|
||||||
|
|
||||||
## Terminology
|
|
||||||
|
|
||||||
Pipeline parallelism (PP) splits model layers across machines. Each machine
|
|
||||||
computes its layers and passes hidden states to the next machine. Tensor
|
|
||||||
parallelism (TP) splits each layer across machines and requires an
|
|
||||||
all-reduce per layer. PP is the only realistic option over ethernet.
|
|
||||||
|
|
||||||
Speculative decoding is a separate feature. It uses a small draft model (or
|
|
||||||
n-grams) to propose tokens that the main model verifies in one pass. It can
|
|
||||||
run together with PP in llama.cpp.
|
|
||||||
|
|
||||||
## The Two Runtimes
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
subgraph llamaRPC[llama.cpp RPC]
|
|
||||||
A1[ggml-rpc-server on node B] --> A2[llama-server on node A<br/>--rpc nodeB:50052]
|
|
||||||
end
|
|
||||||
subgraph vllmPP[vLLM PP]
|
|
||||||
B1[vllm serve node 0<br/>tp=1 pp=2 rank 0] <-->|hidden states| B2[vllm serve node 1<br/>tp=1 pp=2 rank 1]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
heterogeneous nodes over ethernet.
|
|
||||||
multiprocessing and does not compose with speculative decoding yet.
|
|
||||||
|
|
||||||
## What the Research Found
|
|
||||||
|
|
||||||
- [[Ethernet Bandwidth Math]] — Bytes-per-token math for pipeline versus tensor parallelism, and the link budgets that follow from it.
|
|
||||||
(about 10 KiB per token for a 27B model). 1 GbE supports hundreds of
|
|
||||||
tokens per second on paper; the real limit is latency and RPC overhead.
|
|
||||||
works today via llama.cpp RPC with separate CUDA and ROCm builds. The
|
|
||||||
exact NVIDIA-desktop + Strix-Halo-NUC pair has not been benchmarked with
|
|
||||||
published numbers yet.
|
|
||||||
methods (MTP, DFlash, ngram-mod) work with RPC. DSpark is vLLM-only.
|
|
||||||
- [[Expected Performance]] — Estimated split-model throughput over ethernet, with the benchmark anchors the estimates rest on.
|
|
||||||
across an RTX 3090 and a Strix Halo NUC over 1/2.5/10 GbE.
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
- [[Qwen3.8 PP Deployment]] — The real split deployment: which node is head and which is worker, the flags, and the verified gotchas.
|
|
||||||
(Strix), worker on Dankserver (RTX 3090), b10433 on both sides, scripts in
|
|
||||||
`scripts/`, morning-of test runbook. Verified gotchas: no `--device` with
|
|
||||||
`--rpc`, default split is proportional to reported free memory (pin 1,1),
|
|
||||||
the iGPU side defaults to the ROCm backend.
|
|
||||||
|
|
||||||
## Sample Scripts
|
|
||||||
|
|
||||||
- [[Sample Script Set]] — An educational two-node RPC split script set, for reading only — never run it or deploy it.
|
|
||||||
Qwen3.6-27B-Q8_K_P with MTP, DFlash, and ngram-mod variants, sized for
|
|
||||||
the largest context that fits. For educational reading only. Do not run
|
|
||||||
it in production.
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[DFlash DSpark Speculative Decoding]]
|
|
||||||
- [[frank-zealot]]
|
|
||||||
- [[Dankserver System Specs]]
|
|
||||||
- [[Dankclaw System Specs]]
|
|
||||||
|
|
||||||
## Pages
|
|
||||||
|
|
||||||
| Page | Owns |
|
|
||||||
|---|---|
|
|
||||||
@@ -83,18 +83,18 @@ Laws that steer everything here:
|
|||||||
### Ablation / abliteration (directional ablation)
|
### Ablation / abliteration (directional ablation)
|
||||||
- Arditi et al. 2024, arXiv 2406.11717: refusal is mediated by a single
|
- Arditi et al. 2024, arXiv 2406.11717: refusal is mediated by a single
|
||||||
direction; orthogonalizing weights against it removes refusals with a
|
direction; orthogonalizing weights against it removes refusals with a
|
||||||
rank-1 edit. Open implementations: `p-e-w/heretic` (automated, above),
|
rank-1 edit. Open implementations: `p-e-w/heretic` (automated, above)
|
||||||
Artio/abliteration-toolkits, `EleutherAI/repeng`-style rep-overlap
|
Artio/abliteration-toolkits, `EleutherAI/repeng`-style rep-overlap
|
||||||
variants **[verified (tool existence: heretic; rest survey)]**.
|
variants **[verified (tool existence: heretic; rest survey)]**.
|
||||||
- Hardware-agnostic fp-editing. On a MoE the edit touches attention/norm
|
- Hardware-agnostic fp-editing. On a MoE the edit touches attention/norm
|
||||||
weights, so it composes with any expert quantization.
|
weights, so it composes with any expert quantization.
|
||||||
- Fleet status: frank-dragoon runs an abliterated base **by Frank's choice**
|
- Fleet status: frank-dragoon runs an abliterated base **by Frank's choice**
|
||||||
(AGENTS: censored-base refusals were the accepted trade of the 27B era);
|
(AGENTS: censored-base refusals were the accepted trade of the 27B era)
|
||||||
frank-archon's ukisai Swift1.5 is an abliterated finetune of Qwen3.8.
|
frank-archon's ukisai Swift1.5 is an abliterated finetune of Qwen3.8.
|
||||||
|
|
||||||
### HauhauCS QX_K_P "Perfect" quants
|
### HauhauCS QX_K_P "Perfect" quants
|
||||||
- Our frank-zealot publisher. Method, from the publisher's own cards
|
- Our frank-zealot publisher. Method, from the publisher's own cards
|
||||||
**[verified]**: (1) abliterate + finetune ("Aggressive"/"Balanced"),
|
**[verified]**: (1) abliterate + finetune ("Aggressive"/"Balanced")
|
||||||
(2) quantize with **imatrix calibration on the ABLITERATED weights**,
|
(2) quantize with **imatrix calibration on the ABLITERATED weights**,
|
||||||
(3) K_P = a **per-model, per-tensor mixed-precision profile** —
|
(3) K_P = a **per-model, per-tensor mixed-precision profile** —
|
||||||
"model-specific analysis to selectively preserve quality where it
|
"model-specific analysis to selectively preserve quality where it
|
||||||
@@ -204,8 +204,8 @@ u2 draft — all recipe-expressible (see the w4dn8 runbook).
|
|||||||
|
|
||||||
#### Recipe portability: the w4dn8 shape onto other MoEs (2026-10-08 audit)
|
#### Recipe portability: the w4dn8 shape onto other MoEs (2026-10-08 audit)
|
||||||
The pin 1.0.13 container ships SIX arch plugins
|
The pin 1.0.13 container ships SIX arch plugins
|
||||||
(`/opt/radiance/share/radiance/architectures/`): `qwen4exp_fp8`,
|
(`/opt/radiance/share/radiance/architectures/`): `qwen4exp_fp8`
|
||||||
**`qwen35moe_fp8`**, `qwen35_fp8`, `qwen35_bf16`, `llama_fp8`,
|
**`qwen35moe_fp8`**, `qwen35_fp8`, `qwen35_bf16`, `llama_fp8`
|
||||||
`llama_dense_fp8` (census via `podman run --rm --entrypoint sh <image> -c`
|
`llama_dense_fp8` (census via `podman run --rm --entrypoint sh <image> -c`
|
||||||
— the image has no `ls`/`strings`; grep the `.so` files directly, and
|
— the image has no `ls`/`strings`; grep the `.so` files directly, and
|
||||||
the image lives in ROOTLESS storage as `docker.io/stilldeadcode/radiance`).
|
the image lives in ROOTLESS storage as `docker.io/stilldeadcode/radiance`).
|
||||||
@@ -214,7 +214,7 @@ the image lives in ROOTLESS storage as `docker.io/stilldeadcode/radiance`).
|
|||||||
calibration→GPTQ path exists for the qwen3.5-MoE family; GPTQ itself
|
calibration→GPTQ path exists for the qwen3.5-MoE family; GPTQ itself
|
||||||
lives in `rad-convert`, not the plugin (0 hits in both `.so`).
|
lives in `rad-convert`, not the plugin (0 hits in both `.so`).
|
||||||
|
|
||||||
Worked example — `Qwen/Qwen3.6-35B-A3B` (config: `qwen3_5_moe_text`,
|
Worked example — `Qwen/Qwen3.6-35B-A3B` (config: `qwen3_5_moe_text`
|
||||||
40 layers, `full_attention_interval=4` DeltaNet hybrid, **256 experts
|
40 layers, `full_attention_interval=4` DeltaNet hybrid, **256 experts
|
||||||
top-8**, moe_intermediate 512, hidden 2048, MTP ×1, 262k ctx, vocab
|
top-8**, moe_intermediate 512, hidden 2048, MTP ×1, 262k ctx, vocab
|
||||||
248320, multimodal wrapper with vision tower):
|
248320, multimodal wrapper with vision tower):
|
||||||
@@ -258,13 +258,13 @@ top-8**, moe_intermediate 512, hidden 2048, MTP ×1, 262k ctx, vocab
|
|||||||
- EXL3: github.com/turboderp-org/exllamav3 (QTIP lineage; CUDA; Ampere
|
- EXL3: github.com/turboderp-org/exllamav3 (QTIP lineage; CUDA; Ampere
|
||||||
decode incl. 3090 documented in v1.0 performance write-ups).
|
decode incl. 3090 documented in v1.0 performance write-ups).
|
||||||
- Ampere capability edges: vLLM FP8 docs ("only Hopper and Ada …
|
- Ampere capability edges: vLLM FP8 docs ("only Hopper and Ada …
|
||||||
W8A8; Ampere supported for W8A16 weight-only utilizing Marlin"),
|
W8A8; Ampere supported for W8A16 weight-only utilizing Marlin")
|
||||||
vLLM Supported-Hardware matrix (Marlin GPTQ/AWQ on SM 8.0/8.6),
|
vLLM Supported-Hardware matrix (Marlin GPTQ/AWQ on SM 8.0/8.6)
|
||||||
TensorRT-LLM Support Matrix (FP8 and INT4-AWQ-with-FP8-acts need
|
TensorRT-LLM Support Matrix (FP8 and INT4-AWQ-with-FP8-acts need
|
||||||
SM≥89; NVFP4 Blackwell), SGLang issue #12887 (Ampere FP8-via-Marlin).
|
SM≥89; NVFP4 Blackwell), SGLang issue #12887 (Ampere FP8-via-Marlin).
|
||||||
- Repo: `scripts/convert-swift15-w4dn8.sh`,
|
- Repo: `scripts/convert-swift15-w4dn8.sh`
|
||||||
`scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`,
|
`scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`
|
||||||
`scripts/kld-gate-swift15.sh`, `scripts/build-llama-rdna4.sh`,
|
`scripts/kld-gate-swift15.sh`, `scripts/build-llama-rdna4.sh`
|
||||||
`scripts/build-kairic-edge-gfx1151.sh`; endpoints [[frank-archon]],
|
`scripts/build-kairic-edge-gfx1151.sh`; endpoints [[frank-archon]],
|
||||||
[[frank-dragoon]], [[frank-zealot]]; method [[qwen3.8-strix-optimizations]]
|
, ; method
|
||||||
and [[GGUF Compatibility]].
|
and [[GGUF Compatibility]].
|
||||||
@@ -132,15 +132,12 @@ quantizer exists for it ([[Quantization Production]]).
|
|||||||
|---------|--------|-----|
|
|---------|--------|-----|
|
||||||
| 1 | `.hgn` (deployed) | the halogen engine's own format; production door is closed |
|
| 1 | `.hgn` (deployed) | the halogen engine's own format; production door is closed |
|
||||||
| 2 | GGUF K/I quants, K_P blends | the only self-produced option on this card ([[Quantization Production]]) |
|
| 2 | GGUF K/I quants, K_P blends | the only self-produced option on this card ([[Quantization Production]]) |
|
||||||
| 3 | INT4 W4A16 (Quark) / MXFP4 (Quark) | the retired Ling-3.0-flash vLLM era, measured under the firmware-VGM frame buffer: [[ling-3.0-flash-trial]] |
|
| 3 | INT4 W4A16 (Quark) / MXFP4 (Quark) | the retired Ling-3.0-flash vLLM era, measured under the firmware-VGM frame buffer: |
|
||||||
| 4 | FP8 (vLLM) | software emulation; memory savings only; no compute win |
|
| 4 | FP8 (vLLM) | software emulation; memory savings only; no compute win |
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[DFlash DSpark Speculative Decoding]]
|
- [[DFlash DSpark Speculative Decoding]]
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]]
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]]
|
|
||||||
- [[Dankclaw System Specs]]
|
- [[Dankclaw System Specs]]
|
||||||
- [[Dankserver System Specs]]
|
- [[Dankserver System Specs]]
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
- [[Shared Memory Architecture Dankclaw]]
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "vLLM uses CPU RAM and swap space to extend GPU memory capacity."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
- vllm
|
|
||||||
timestamp: 2026-08-28
|
|
||||||
---
|
|
||||||
|
|
||||||
# RAM CPU Offloading in vLLM
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Stock vLLM offloads **KV pages**, not weights: the scheduler moves KV cache between GPU
|
|
||||||
and CPU. That is the upstream behaviour and it is NOT the whole story on our hosts — the
|
|
||||||
tcclaviger/R4D fork ran a real **weight** offload (`--enable-expert-offload`, tiered
|
|
||||||
expert placement, a pinned host pool per rank), which is how a 147 GiB MoE served from a
|
|
||||||
246 GiB box: [[RDNA4 MoE Expert Offload VRAM Budget]] and the tcclaviger era row of
|
|
||||||
[[frank-archon]]. First-hand in-repo evidence outranks a generic upstream claim about our
|
|
||||||
own hosts ([[Model Swap and Rollback]] §10).
|
|
||||||
|
|
||||||
> **Verified 2026-10-19 against `vllm/config/cache.py` on `main`:** there is no
|
|
||||||
> `swap_space` field — `--swap-space` is gone from current engine args. CPU KV offload is
|
|
||||||
> `--kv-offloading-size`, and it activates **only** together with `--kv-offloading-backend`
|
|
||||||
> (default `native`); both fields live in that one file. This page is the fleet's owner of
|
|
||||||
> that knob contract — other pages link here instead of restating it.
|
|
||||||
|
|
||||||
## How CPU Offloading Works
|
|
||||||
|
|
||||||
VLLM manages KV cache as pages. Each page holds attention key and value tensors for a set of tokens. When GPU memory is full, VLLM can move pages to CPU memory or disk swap space.
|
|
||||||
|
|
||||||
The KV cache swap mechanism works as follows:
|
|
||||||
|
|
||||||
1. VLLM allocates GPU blocks for active KV cache pages
|
|
||||||
2. When GPU blocks are exhausted, VLLM swaps inactive pages to CPU
|
|
||||||
3. Swapped pages are stored in a CPU memory buffer
|
|
||||||
4. When a swapped page is needed, VLLM loads it back to GPU
|
|
||||||
5. If CPU buffer is full, pages are written to disk swap space
|
|
||||||
|
|
||||||
## Key Parameters
|
|
||||||
|
|
||||||
### gpu_memory_utilization
|
|
||||||
|
|
||||||
This parameter controls the fraction of GPU memory used for KV cache.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| 0.7 | Uses 70% of GPU memory for KV cache |
|
|
||||||
| 0.8 | Uses 80% of GPU memory for KV cache |
|
|
||||||
| 0.92 | Uses 92% of GPU memory for KV cache (default) |
|
|
||||||
| 0.95 | Uses 95% of GPU memory for KV cache |
|
|
||||||
|
|
||||||
Lower values leave more GPU memory for model weights. Higher values allow more concurrent requests but increase swap frequency.
|
|
||||||
|
|
||||||
### swap_space (removed) / kv-offloading-size + kv-offloading-backend (current)
|
|
||||||
|
|
||||||
`--swap-space` no longer exists in current engine args (verified above); the live pair is
|
|
||||||
`--kv-offloading-size` with `--kv-offloading-backend`, and offload stays off until the size
|
|
||||||
is set. The table below is the historical `--swap-space` sizing guidance, kept as a sizing
|
|
||||||
reference only.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| 0 | No CPU swap space |
|
|
||||||
| 4 | 4 GiB swap space (historical default) |
|
|
||||||
| 8 | 8 GiB swap space |
|
|
||||||
| 16 | 16 GiB swap space |
|
|
||||||
| 32 | 32 GiB swap space |
|
|
||||||
| 64 | 64 GiB swap space |
|
|
||||||
|
|
||||||
The swap space acts as an extension of GPU memory. When GPU blocks are full, VLLM moves KV cache pages to swap space.
|
|
||||||
|
|
||||||
### max_model_len
|
|
||||||
|
|
||||||
This parameter sets the maximum context length.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| auto | VLLM calculates optimal length |
|
|
||||||
| 2048 | 2048 tokens |
|
|
||||||
| 4096 | 4096 tokens |
|
|
||||||
| 8192 | 8192 tokens |
|
|
||||||
| 32768 | 32768 tokens |
|
|
||||||
|
|
||||||
Smaller values reduce memory usage but limit context length.
|
|
||||||
|
|
||||||
### max_num_batched_tokens
|
|
||||||
|
|
||||||
This parameter sets the maximum number of tokens per batch.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| 2048 | Default for online serving |
|
|
||||||
| 4096 | Moderate batch size |
|
|
||||||
| 8192 | Large batch size |
|
|
||||||
| 16384 | Very large batch size |
|
|
||||||
|
|
||||||
### max_num_seqs
|
|
||||||
|
|
||||||
This parameter sets the maximum number of sequences per batch.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| 128 | Default for online serving |
|
|
||||||
| 256 | Large batch |
|
|
||||||
|
|
||||||
### kv_cache_dtype
|
|
||||||
|
|
||||||
This parameter sets the KV cache data type.
|
|
||||||
|
|
||||||
| Value | Description |
|
|
||||||
|-------|-------------|
|
|
||||||
| auto | Uses model default dtype |
|
|
||||||
| fp8 | Uses FP8 for KV cache (reduces memory by 50%) |
|
|
||||||
| fp8_e5m2 | Uses FP8 E5M2 format for KV cache |
|
|
||||||
|
|
||||||
## Memory Configuration Examples
|
|
||||||
|
|
||||||
All three examples carry the retired `--swap-space` syntax and are kept as sizing
|
|
||||||
references; the live knob pair is named above.
|
|
||||||
|
|
||||||
### Small Model on Single GPU
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen2.5-1.5B-Instruct \
|
|
||||||
--gpu-memory-utilization 0.9 \
|
|
||||||
--swap-space 4 \
|
|
||||||
--max-num-batched-tokens 4096 \
|
|
||||||
--max-num-seqs 128
|
|
||||||
```
|
|
||||||
|
|
||||||
### Large Model with CPU Offload
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve meta-llama/Llama-3.1-70B-Instruct \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--gpu-memory-utilization 0.8 \
|
|
||||||
--swap-space 16 \
|
|
||||||
--max-model-len 8192
|
|
||||||
```
|
|
||||||
|
|
||||||
### Memory-Constrained GPU
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve meta-llama/Llama-3.1-70B-Instruct \
|
|
||||||
--tensor-parallel-size 4 \
|
|
||||||
--gpu-memory-utilization 0.7 \
|
|
||||||
--swap-space 32 \
|
|
||||||
--max-model-len 4096 \
|
|
||||||
--max-num-batched-tokens 2048
|
|
||||||
```
|
|
||||||
|
|
||||||
## Preemption Modes
|
|
||||||
|
|
||||||
VLLM uses two preemption modes when GPU memory is insufficient:
|
|
||||||
|
|
||||||
### Recompute Mode (Default in V1)
|
|
||||||
|
|
||||||
VLLM recomputes the prefill for preempted requests. This uses GPU compute instead of memory. This is the default mode in VLLM V1.
|
|
||||||
|
|
||||||
### Swap Mode
|
|
||||||
|
|
||||||
VLLM swaps KV cache pages to CPU and reloads them later. This uses CPU memory instead of compute.
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
Watch for preemption warnings:
|
|
||||||
|
|
||||||
```
|
|
||||||
WARNING scheduler.py: Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.
|
|
||||||
```
|
|
||||||
|
|
||||||
If you see frequent preemptions:
|
|
||||||
|
|
||||||
1. Increase `gpu_memory_utilization`
|
|
||||||
2. Raise the KV offload budget (`--kv-offloading-size`, with `--kv-offloading-backend`)
|
|
||||||
3. Decrease `max_num_seqs`
|
|
||||||
4. Increase `tensor_parallel_size`
|
|
||||||
5. Increase `max_model_len` to match actual usage
|
|
||||||
|
|
||||||
## Memory Conservation Tips
|
|
||||||
|
|
||||||
- Use quantization (FP8, INT8) to reduce model size
|
|
||||||
- Reduce `max_model_len` to match actual needs
|
|
||||||
- Reduce `max_num_seqs` to limit concurrent requests
|
|
||||||
- Disable CUDA graphs with `--enforce-eager` to save memory
|
|
||||||
- Set `compilation_config.cudagraph_capture_sizes` to smaller values
|
|
||||||
- Set `--kv-cache-memory-bytes` to pin the KV budget instead of letting the profiler guess
|
|
||||||
(the flag our own `scripts/patches/gpu_worker-kv-cap.patch` reads)
|
|
||||||
- Limit multi-modal inputs with `limit_mm_per_prompt`
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| vLLM_CPU_KVCACHE_SPACE | KV cache space in GiB for CPU backend | 4 |
|
|
||||||
| vLLM_CPU_OMP_THREADS_BIND | CPU thread binding for OpenMP | auto |
|
|
||||||
| vLLM_CPU_NUM_OF_RESERVED_CPU | Reserved CPU cores per rank | 0-1 |
|
|
||||||
| vLLM_CACHE_ROOT | torch.compile cache directory | ~/.cache/vllm |
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[VLLM Index]]
|
|
||||||
- [[Hybrid CPU GPU Processing Dankserver]]
|
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: RDNA4 Container Serving
|
|
||||||
description: "Bringing a model larger than VRAM onto the two-card box: quant gating, fetch verification, container GPU-init triage."
|
|
||||||
tags: [rdna4, gfx1201, r9700, vllm, expert-offload, podman, rocm]
|
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
|
||||||
---
|
|
||||||
|
|
||||||
# RDNA4 Container Serving
|
|
||||||
|
|
||||||
Bring-up path for Dankserver (2x Radeon AI PRO R9700, gfx1201 — card count,
|
|
||||||
per-card VRAM and host RAM are [[Dankserver System Specs]]'s) with
|
|
||||||
`docker.io/tcclaviger/vllm` (vLLM 0.29.0.dev, GLM/Qwen-specialised RDNA4 build
|
|
||||||
with native MoE quant kernels and a first-class expert offload planner). The
|
|
||||||
tuning history and current deployed shape live in
|
|
||||||
[[frank-archon]]; this page is the bring-up law.
|
|
||||||
|
|
||||||
## 0. Know the box — and its phantom third GPU
|
|
||||||
|
|
||||||
`torch.cuda.device_count()` returns **3**; summing `total_memory` overstates
|
|
||||||
the box as 186.7 GiB. The third device is the 7700X's integrated Vega
|
|
||||||
(gfx1036): torch reports ~123 GiB for it, `rocm-smi` reports 0.50 GiB,
|
|
||||||
allocating on it moves system `MemAvailable` 1:1 (it is GTT over DDR5, not
|
|
||||||
dedicated VRAM), and it dies with `hipErrorInvalidKernelFile` when the image
|
|
||||||
ships no code for that arch. **Never sum device `total_memory`; never count
|
|
||||||
the iGPU as headroom or a workload.**
|
|
||||||
|
|
||||||
## 1. Preflight: is the quant class EXECUTABLE, not merely present?
|
|
||||||
|
|
||||||
Expert offload is mandatory when weights exceed VRAM, and it requires the
|
|
||||||
checkpoint's experts class to have BOTH a compiled kernel AND an
|
|
||||||
`offload_recipe` ("a pinned expert has no host copy, so there is no
|
|
||||||
read-through fallback" — startup aborts otherwise). Check BEFORE downloading
|
|
||||||
anything:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
podman run --rm --device /dev/kfd --device /dev/dri --device /dev/mem \
|
|
||||||
-e HIP_VISIBLE_DEVICES=0,1 --entrypoint bash $IMAGE -c '
|
|
||||||
nm -D /app/r4dhip/r4d.so | grep -oE "gemm_moe_[a-z0-9_]+" | sort -u
|
|
||||||
grep -rl offload_recipe /opt/vllm/lib/python*/site-packages/vllm/model_executor/layers/fused_moe/'
|
|
||||||
```
|
|
||||||
|
|
||||||
| quant | kernel in `r4d.so` | experts class | verdict |
|
|
||||||
|---|---|---|---|
|
|
||||||
| FP8 | yes | `Fp8HipMoEExperts` | **serves** |
|
|
||||||
| MXFP4 / DSFP4 | yes (`gemm_moe_mxfp4a8_*` / `dsfp4a8_*`) | `R4dMxfp4MoEExperts` / `R4dDsfp4MoEExperts` | **serves** |
|
|
||||||
| W4A16 | **no** `gemm_moe_w4a16_*` | `R4dW4a16MoEExperts` exists | silently falls back to generic `TritonWNA16Experts`, no recipe → startup halts |
|
|
||||||
| RFA | none | no class at all | out |
|
|
||||||
|
|
||||||
Also confirm the architecture routes natively with NO overlay
|
|
||||||
(`ModelRegistry.get_supported_archs()` for `Qwen4Exp`/`Glm5`). The stock image
|
|
||||||
natively carries Qwen3.8/Qwen4Exp and GLM-5.x including MTP heads. **Do not
|
|
||||||
apply vendor overlays** against a fast-moving pre-release image — the breakage
|
|
||||||
misleads (a missing symbol in the overlay's OWN copy of a module masks the
|
|
||||||
real cause, then moves to another symbol each patch;
|
|
||||||
`cannot import name 'checkpoint_has_lm_head'` = overlay clobbered
|
|
||||||
`transformers_utils/config.py`). The image ships quantizers for
|
|
||||||
`rfa|rfi|mxfp4|mxfp4ocp|paro5` under `/app/tools/quantize.py`
|
|
||||||
(`--fp8-attn --quant-only --dry-run` first) if a re-quant is needed — note it
|
|
||||||
can run MXFP4/DSFP4/FP8 but only PRODUCE rfa/rfi/mxfp4/paro.
|
|
||||||
|
|
||||||
## 2. Fetch with verification, always
|
|
||||||
|
|
||||||
Gated repos: token never in argv (`pgrep -a curl` prints command lines).
|
|
||||||
Build a mode-600 curl config once:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
{ printf 'header = "Authorization: Bearer '; cat ~/.cache/huggingface/token; printf '"\n'; } \
|
|
||||||
> ~/.cache/huggingface/curl.conf && chmod 600 ~/.cache/huggingface/curl.conf
|
|
||||||
curl -K ~/.cache/huggingface/curl.conf -f -sS -L -C - -o "$out" "$url"
|
|
||||||
```
|
|
||||||
|
|
||||||
`--fail` is MANDATORY — `curl -o FILE URL` exits 0 on HTTP 401/403 and writes
|
|
||||||
the error body as a "successful artifact" (a gated repo once yielded three
|
|
||||||
153-byte shards with identical sha256s and a cheerful DONE banner). `-C -`
|
|
||||||
resumes torn shards; HTTP/2 cancels leave silently truncated files, so
|
|
||||||
re-verify EVERY shard's byte count against the Hub manifest (and the LFS
|
|
||||||
sha256) before serving. The AGENTS.md Secret Rules carry the token-hygiene
|
|
||||||
law; [[frank-medium-model-swap]] has the same detail for GGUFs and
|
|
||||||
`.partNofM` fragments.
|
|
||||||
|
|
||||||
## 3. Serve — the known-good invocation and its traps
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo sh -c 'sync; echo 3 > /proc/sys/vm/drop_caches'
|
|
||||||
podman run -d --name <ctr> --ipc=host --ulimit memlock=-1:-1 \
|
|
||||||
--cap-add SYS_PTRACE \
|
|
||||||
--device /dev/kfd --device /dev/dri --device /dev/mem \
|
|
||||||
-e HIP_VISIBLE_DEVICES=0,1 -e VLLM_ROCM_USE_AITER=0 \
|
|
||||||
-v /home/dank/models/<model>:/models:ro \
|
|
||||||
$IMAGE /models --served-model-name <name> --port 7770 \
|
|
||||||
--tensor-parallel-size 2 --max-model-len 65536 \
|
|
||||||
--kv-cache-memory-bytes 2147483648 \
|
|
||||||
--max-num-seqs 4 --gpu-memory-utilization 0.95 \
|
|
||||||
--enable-prefix-caching --trust-remote-code
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Entrypoint already execs `vllm serve "$@"`** — pass the model path as the
|
|
||||||
first argument and do NOT pass `serve` (podman then tries to pull `serve`
|
|
||||||
as an image name). `-e VAR=1` must come BEFORE the image name.
|
|
||||||
- **Mount the whole `/dev/dri`, never individual render nodes** (§4).
|
|
||||||
- `--ipc=host` and `--shm-size` are mutually exclusive in podman.
|
|
||||||
- `podman run -d` without `-a` sends container stdout nowhere — read
|
|
||||||
`podman logs <ctr>`; and vLLM writes `Application startup complete` to
|
|
||||||
**stderr**, so a `podman logs X 2>/dev/null | grep -q` readiness gate never
|
|
||||||
fires.
|
|
||||||
- Start at 65536, not the native ceiling; prove the plan, then scale.
|
|
||||||
- Load takes ~5 min with `/health` refusing the whole time — do not kill the
|
|
||||||
process (deploy.sh waits 20 min).
|
|
||||||
|
|
||||||
## 4. Container GPU-init triage (ordered)
|
|
||||||
|
|
||||||
1. **Enumerate-vs-context trap (most common):** individual
|
|
||||||
`renderD*` nodes give HIP the render nodes WITHOUT the primary `card*`
|
|
||||||
nodes — it enumerates agents (`device_count()` returns N) and then refuses
|
|
||||||
every context: `RuntimeError: No CUDA GPUs are available` on the first
|
|
||||||
real op. A/B-confirmed 2026-09-19. Rule: `--device /dev/kfd --device
|
|
||||||
/dev/dri --device /dev/mem`; restrict GPUs with `HIP_VISIBLE_DEVICES`,
|
|
||||||
never with device nodes.
|
|
||||||
2. **`device_count()` is not evidence.** Prove usability with a real op
|
|
||||||
(`(a@a).sum().item()` on a fresh `torch.randn`), then repeat via
|
|
||||||
`mp.spawn`, `mp.fork`, and `subprocess.run([sys.executable, ...])`, and
|
|
||||||
cross-check `rocm-smi --showmeminfo vram`.
|
|
||||||
3. **memlock rlimit → a MISLEADING CUDA OOM.** Host default 8192 KB; a pin
|
|
||||||
larger than the limit fails as
|
|
||||||
`torch.AcceleratorError: CUDA error: out of memory` raised from
|
|
||||||
`pin_memory()` — it names the accelerator but the fault is host pinned
|
|
||||||
memory. It cannot be raised from inside a container or even from root's
|
|
||||||
own shell (`Operation not permitted`); only PAM applies it at login, so
|
|
||||||
fixes only affect NEW sessions:
|
|
||||||
`/etc/security/limits.d/99-memlock.conf` with
|
|
||||||
`* - memlock unlimited` + `root - memlock unlimited`; the container needs
|
|
||||||
`--ulimit memlock=-1:-1` to inherit. Discriminating test after the fix:
|
|
||||||
the container reports `unlimited` AND small pins succeed; if pins still
|
|
||||||
fail, memlock was never the cause.
|
|
||||||
4. **`/proc/meminfo` TAB trap:** the key/value separator is a TAB, so
|
|
||||||
`grep MemAvailable /proc/meminfo | tr -s " " | cut -d" " -f2` returns
|
|
||||||
EMPTY and a following awk prints `0.0` — a false "host out of memory" on a
|
|
||||||
host with hundreds of GiB free, which invites aborting something healthy.
|
|
||||||
Tab-safe: `awk -v k='MemAvailable:' '$1==k {printf "%.1f", $2/1048576;
|
|
||||||
exit}' /proc/meminfo`, cross-check `<= MemTotal`, refuse to print a zero.
|
|
||||||
Use `scripts/dankstatus.sh` instead of hand-rolling.
|
|
||||||
5. **Silent-parse traps:** `grep -c PATTERN file || echo 0` yields TWO lines
|
|
||||||
(grep prints 0 AND exits 1) — `[: 0\n0: integer expected`; use
|
|
||||||
`| head -1` or `|| true`. `systemctl is-enabled` can emit multiple lines.
|
|
||||||
`rocm-smi --showmeminfo vram --csv` column order is `device, VRAM Total
|
|
||||||
Memory (B), VRAM Total Used Memory (B)` — reading $3 as total/$4 as used
|
|
||||||
yields 0.00; filter to `^card[0-9]+$`.
|
|
||||||
6. **Guard the box:** long loads with co-tenants can drive RAM to 0 — abort
|
|
||||||
before the OOM killer (`MemAvailable < 4 GiB → podman rm -f`, exit 1).
|
|
||||||
7. **Drop page cache before a large load** (`echo 3 > /proc/sys/vm/drop_caches`)
|
|
||||||
and confirm the drop was actually in effect for the run before treating a
|
|
||||||
failure as a capacity wall — a stale "it doesn't fit" from a contaminated
|
|
||||||
run is expensive to unwind.
|
|
||||||
|
|
||||||
## 5. Memory budgeting: the levers are not where they look
|
|
||||||
|
|
||||||
Every figure in this section is a tcclaviger-era boot on the port that is
|
|
||||||
[[frank-archon]]'s today; the laws they teach are general. The planner builds
|
|
||||||
`budget = total × util`, subtracts a KV reserve, and gives
|
|
||||||
the REMAINDER to the expert arena. Consequences, all measured:
|
|
||||||
|
|
||||||
- **`--gpu-memory-utilization` grows the ARENA, not the KV pool** (KV stayed
|
|
||||||
0.64 GiB at util 0.95 and 0.98). The arena is the only elastic term.
|
|
||||||
- **Lowering `--max-model-len` is the WRONG lever and backfires:** a smaller
|
|
||||||
context shrinks the reserve, handing the space to the arena — measured 65536
|
|
||||||
→ 0.64 GiB KV, 32768 → 0.34 GiB KV.
|
|
||||||
- **Hybrid-attention undercount:** the planner's per-token KV figure for
|
|
||||||
Qwen4Exp is an undercount — the printed figure and the true one are both on
|
|
||||||
[[frank-archon]] — because it misses the large fixed per-sequence Mamba state
|
|
||||||
(~2x), so the reserve is always short and the arena silently covers it.
|
|
||||||
Fix by pinning the pool: `--kv-cache-memory-bytes <BYTES>` (era reading: a
|
|
||||||
2 GiB pin = 113,595 tokens ≈ 3.47x concurrency at 32k — that pair is this
|
|
||||||
page's record of the tcclaviger boots, not [[frank-archon]]'s). For long
|
|
||||||
context,
|
|
||||||
`--kv-offloading-size` (GiB) spills KV to host.
|
|
||||||
- **`--expert-offload-mem` caps the HOST side, not the VRAM arena**
|
|
||||||
(`cap = int(x * GIB)` against `host_memory_available()`). Do not reach for
|
|
||||||
it to fix a VRAM problem; to grow the arena, shrink the KV pin or raise
|
|
||||||
util (see [[frank-archon]] for the
|
|
||||||
util=1.0 unbootability law).
|
|
||||||
|
|
||||||
**Read the plan — it is the real acceptance test:**
|
|
||||||
|
|
||||||
```
|
|
||||||
expert offload: AUTO-ENABLED, weights 62.54 GiB per rank exceed the 30.27 GiB budget (TP2)
|
|
||||||
budget (total x util) 30.27 GiB
|
|
||||||
non-expert weights 6.28 GiB
|
|
||||||
KV reserve 0.36 GiB <- sanity-check against the model's own per-token figure
|
|
||||||
CUDA graphs 0.66 GiB
|
|
||||||
runtime overhead 7.60 GiB
|
|
||||||
expert cache space 15.36 GiB 6712 slots of 2.344 MiB
|
|
||||||
host-backed experts/rank 52.74 GiB 23040 experts
|
|
||||||
host: 105.48 GiB across 2 ranks of 160.89 GiB usable
|
|
||||||
```
|
|
||||||
|
|
||||||
Host fits when `host_total < usable`. The PLE n-gram table rides memfd and is
|
|
||||||
SHARED across TP ranks.
|
|
||||||
|
|
||||||
**Loading mechanism** (for reasoning about peak RAM): weights never materialise
|
|
||||||
in VRAM — expert Parameters start as CPU staging tensors; each layer's
|
|
||||||
`weight_loader` counts shard events and, on layer completion,
|
|
||||||
`ExpertTopology.split_layer` moves that layer's experts into the VRAM arena
|
|
||||||
and compacts the rest into exact-size pinned host tensors behind UVA. Peak
|
|
||||||
host RAM = `host-backed set + layers still loading`, not the whole model. At
|
|
||||||
runtime `apply_hot_cold` sends arena-resident experts down a hot GEMM and the
|
|
||||||
rest straight over PCIe via UVA views — no staging copy in the critical path.
|
|
||||||
|
|
||||||
**Emulation check:** a dequantize-to-compute path doubles real host RAM — the
|
|
||||||
measured pair (offload request vs host footprint) is on [[frank-archon]] — and
|
|
||||||
the doubling is invisible in the weight count. Read the log for `Using
|
|
||||||
'EMULATION' Mxfp4 MoE backend` before concluding "doesn't fit".
|
|
||||||
|
|
||||||
## 6. Performance profile and honest measurement
|
|
||||||
|
|
||||||
Decode on this pair is **PCIe-gather-bound, context-independent**: the rate is
|
|
||||||
flat from a few hundred to sixteen thousand context tokens, so KV attention is
|
|
||||||
not the cost (the flat figure and its MTP/gather deltas are
|
|
||||||
[[Tcclaviger Fork Ops]]'s profile of the same shape). With ~29 % of experts
|
|
||||||
arena-resident, under a gigabyte crosses PCIe per token at the engine's
|
|
||||||
effective read rate; cold-vs-warm differing ~10 % confirms the LRU works;
|
|
||||||
growth is linear in hit rate, and only speculative decode cuts sequential STEPS
|
|
||||||
rather than making each cheaper.
|
|
||||||
|
|
||||||
**Benchmark traps that produced fake numbers here:**
|
|
||||||
|
|
||||||
- `max_tokens=1` is not TTFT (the server can satisfy it without doing the
|
|
||||||
work — prefill looked flat at 1.26 s for 16k AND 30k prompts). A 64-token
|
|
||||||
non-streamed reply is time-to-64-tokens (decode-bound, flat ~9.6 s at every
|
|
||||||
length). Correct TTFT: stream and time the FIRST content-bearing chunk.
|
|
||||||
- A small repeating word loop is prefix-cacheable AND n-gram-predictable — a
|
|
||||||
43-word cycle reported 22,897 tok/s prefill, almost entirely cache hit. Use
|
|
||||||
a large vocabulary in shuffled order and print `cached_tokens`.
|
|
||||||
- ~1.2 s fixed per-request overhead swamps prefill below ~16k tokens; compare
|
|
||||||
prefill at 30k+ only.
|
|
||||||
- Size prompts with `/v1/messages/count_tokens` (a chat call for counting
|
|
||||||
pollutes the metric); guessing words-per-token silently overran the window
|
|
||||||
(HTTP 400).
|
|
||||||
- Always run a canary: plant a unique token at the END of a long prompt and
|
|
||||||
ask the model to quote it — proves prompts reach the model at all.
|
|
||||||
- Measure warm AND long (`podman exec` + urllib at 127.0.0.1:7770): a 33-token
|
|
||||||
reply read 14.6 cold / 20.3 warm; a 768-token generation hit 25.7; two
|
|
||||||
concurrent streams scaled to 47 aggregate.
|
|
||||||
|
|
||||||
**Levers (all opt-in):** `--expert-prefetch N` takes an INT depth, not a
|
|
||||||
boolean (default 0; at depth 1 only ~10 % cold-vs-warm difference — the cache
|
|
||||||
already works); `CLAV_TUNABLEOP_SWEEP=1` autotunes GEMM shapes (silently
|
|
||||||
skipped under expert offload unless `CLAV_TUNABLEOP_WITH_OFFLOAD=1` too —
|
|
||||||
see [[frank-archon]] for why it was rejected);
|
|
||||||
`--expert-precise-memory` swaps the fitted overhead figure in.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [[frank-archon]] — deployed shape, plan law,
|
|
||||||
measured campaigns
|
|
||||||
- [[PCIe Link and Atomics]] — why decode is gather-bound and what cannot fix it
|
|
||||||
- [[RDNA4 MoE Expert Offload VRAM Budget]] — arena/plan arithmetic deep-dive
|
|
||||||
- [[frank-dragoon]] and [[frank-zealot]] — window discipline on live hosts
|
|
||||||
@@ -14,7 +14,6 @@ figure and rate below is from [[frank-archon]]'s tcclaviger era (2026-09-29 →
|
|||||||
to host RAM) — that page owns the endpoint's shape, tuning history and era
|
to host RAM) — that page owns the endpoint's shape, tuning history and era
|
||||||
record; this page owns only the plan arithmetic, the ladder method and the audit
|
record; this page owns only the plan arithmetic, the ladder method and the audit
|
||||||
procedure. Boot-failure capture lives in
|
procedure. Boot-failure capture lives in
|
||||||
[[vLLM Boot Forensics and Config Interrogation]].
|
|
||||||
|
|
||||||
## 1. The plan law
|
## 1. The plan law
|
||||||
|
|
||||||
@@ -164,7 +163,7 @@ spec maximum, never hand arithmetic.
|
|||||||
`Tried to allocate 10.28 GiB … 9.46 GiB free` died in KV ALLOCATION.
|
`Tried to allocate 10.28 GiB … 9.46 GiB free` died in KV ALLOCATION.
|
||||||
6. **Clean between rungs:** stop the transient unit, `podman rm -f`, kill
|
6. **Clean between rungs:** stop the transient unit, `podman rm -f`, kill
|
||||||
orphans by explicit PID
|
orphans by explicit PID
|
||||||
(`ps aux --sort=-rss | awk '/vllm\/bin\/python3/ && $6 > 50000000 {print $2}'`),
|
(`ps aux --sort=-rss | awk '/vllm\/bin\/python3/ && $6 > 50000000 {print $2}'`)
|
||||||
wait for `MemAvailable` to recover.
|
wait for `MemAvailable` to recover.
|
||||||
|
|
||||||
**The hazard that cost production:** a leftover ladder script whose teardown
|
**The hazard that cost production:** a leftover ladder script whose teardown
|
||||||
@@ -215,7 +214,7 @@ at graph capture; 6 seqs fp8 → 10.70 GiB ask with 9.18 GiB free (fails) —
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [[RDNA4 Container Serving]] — the bring-up path and container traps
|
- the bring-up path and container traps
|
||||||
- [[vLLM Boot Forensics and Config Interrogation]] — capture and contracts
|
- capture and contracts
|
||||||
- [[frank-archon]] — measured history
|
- [[frank-archon]] — measured history
|
||||||
- [[Llama Cpp Serving Sizing]] — the llama.cpp equivalent
|
- [[Llama Cpp Serving Sizing]] — the llama.cpp equivalent
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "Serving a dense FP8 vision-language checkpoint on the Strix Halo iGPU: the load path, the flags, the format gate."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
timestamp: 2026-09-03
|
|
||||||
---
|
|
||||||
|
|
||||||
# Running Qwen3.6 27B FP8 on Dankclaw
|
|
||||||
> **RETIRED - vLLM era (before the 2026-08-17 redo).** Nothing on this page serves
|
|
||||||
> today: no vLLM or ROCm process runs on Dankclaw, and the iGPU endpoint at
|
|
||||||
> `192.168.0.143:7770` is [[frank-dragoon]] (halogen flash-server). The page stays as
|
|
||||||
> the era record of the Qwen3.6-27B-FP8 vLLM bring-up and the Strix Halo ROCm
|
|
||||||
> recipes ([[frank-redo-2026-08-17]]). The hardware table below carries the era's
|
|
||||||
> host figures; live ones are [[Dankclaw System Specs]].
|
|
||||||
|
|
||||||
## Model Information
|
|
||||||
|
|
||||||
Qwen3.6-27B is a dense vision-language model from the Qwen3.6 series. It uses 27B parameters with gated DeltaNet and attention layers.
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| HuggingFace ID | `Qwen/Qwen3.6-27B-FP8` |
|
|
||||||
| Base Model | `Qwen/Qwen3.6-27B` |
|
|
||||||
| Architecture | Qwen3_5ForConditionalGeneration |
|
|
||||||
| Total Parameters | 27B |
|
|
||||||
| Hidden Size | 5120 |
|
|
||||||
| Layers | 64 (16 blocks of 3 DeltaNet + 1 Attention) |
|
|
||||||
| Attention Heads | 24 Q, 4 KV (head dim 256) |
|
|
||||||
| DeltaNet Heads | 48 V, 16 QK (head dim 128) |
|
|
||||||
| Intermediate Dim | 17,408 |
|
|
||||||
| Vocab Size | 248,320 (padded) |
|
|
||||||
| Context Length | 262,144 (natively extensible to 1,010,000) |
|
|
||||||
| Quantization | FP8 (block size 128) |
|
|
||||||
| Vision Encoder | Yes (image-text-to-text) |
|
|
||||||
|
|
||||||
## Hardware: Dankclaw
|
|
||||||
*Era figures (2026-08). The host has changed since - [[Dankclaw System Specs]].*
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| CPU | AMD RYZEN AI MAX+ 395 w/ Radeon 8060S (Strix Halo) |
|
|
||||||
| GPU | Integrated Radeon 8060S (unified memory) |
|
|
||||||
| Physical RAM | 64 GiB (61 GiB visible; corrected 2026-08-17) |
|
|
||||||
| GPU-Visible (ROCm) | 96 GiB (overcommitted) |
|
|
||||||
| Swap | 8 GiB |
|
|
||||||
| ROCm | 7.2.4 |
|
|
||||||
| Python | 3.12.13 (vLLM venv) |
|
|
||||||
|
|
||||||
## Memory Calculation
|
|
||||||
|
|
||||||
| Component | BF16 | FP8 |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| Model weights (27B) | 54 GB | 13.5 GB |
|
|
||||||
| Vision encoder | ~1 GB | ~0.5 GB |
|
|
||||||
| **Total weights** | ~55 GB | ~14 GB |
|
|
||||||
| KV cache (64K ctx, 1 seq) | ~1 GB | ~0.5 GB |
|
|
||||||
| Overhead | ~2 GB | ~2 GB |
|
|
||||||
| **Total (approximate)** | ~58 GB | ~16.5 GB |
|
|
||||||
|
|
||||||
The FP8 variant requires approximately 16-17 GB. ThinkingCap-FP8 requires ~29 GB. Both fit within the 61 GiB physical RAM, but ROCm reports 96 GiB to vLLM. Set `--gpu-memory-utilization` conservatively (0.80) to avoid OOM.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
> **Era note (2026-08, superseded twice):** at writing time Dankclaw's primary
|
|
||||||
> service was the vLLM-ROCm Ling-3.0-flash CIRU INT4 trial ([[ling-3.0-flash-trial]],
|
|
||||||
> retired 2026-08-17), and this page documented the alternative vLLM FP8 deployment
|
|
||||||
> of Qwen3.6-27B. Both paths are dead; `scripts/frank-dragoon.service` today is the
|
|
||||||
> halogen flash-server unit ([[frank-dragoon]]).
|
|
||||||
|
|
||||||
Install vLLM for ROCm. Version 0.24.0+ is required for Qwen3.6 with speculative decoding:
|
|
||||||
|
|
||||||
```
|
|
||||||
uv venv --python 3.12 --seed
|
|
||||||
source .venv/bin/activate
|
|
||||||
uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/
|
|
||||||
```
|
|
||||||
|
|
||||||
Installed: vLLM 0.26.0 in Python 3.12.13 venv at `/home/dank/vllm-rocm-venv`.
|
|
||||||
|
|
||||||
### MPI Dependency
|
|
||||||
|
|
||||||
Torch requires `libmpi_cxx.so.40`. If missing, install `openmpi-bin`:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo apt install -y openmpi-bin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
Set these for ROCm on Strix Halo:
|
|
||||||
|
|
||||||
```
|
|
||||||
export HSA_OVERRIDE_GFX_VERSION=11.5.1
|
|
||||||
export HF_HOME=/home/dank/.cache/huggingface
|
|
||||||
```
|
|
||||||
|
|
||||||
## vLLM Serve Commands
|
|
||||||
|
|
||||||
### Text-Only (Recommended for Memory Savings)
|
|
||||||
|
|
||||||
Skip the vision encoder to reduce memory:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-27B-FP8 \
|
|
||||||
--port 8000 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.85 \
|
|
||||||
--swap-space 8 \
|
|
||||||
--max-model-len 131072 \
|
|
||||||
--max-num-batched-tokens 2048 \
|
|
||||||
--max-num-seqs 32 \
|
|
||||||
--reasoning-parser qwen3 \
|
|
||||||
--language-model-only \
|
|
||||||
--dtype auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Vision Encoder
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-27B-FP8 \
|
|
||||||
--port 8000 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.85 \
|
|
||||||
--swap-space 8 \
|
|
||||||
--max-model-len 131072 \
|
|
||||||
--max-num-batched-tokens 2048 \
|
|
||||||
--max-num-seqs 32 \
|
|
||||||
--reasoning-parser qwen3 \
|
|
||||||
--dtype auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters Explained
|
|
||||||
|
|
||||||
| Parameter | Value | Reason |
|
|
||||||
|-----------|-------|--------|
|
|
||||||
| --kv-cache-dtype fp8 | FP8 KV cache | Cuts KV cache memory by 50% |
|
|
||||||
| --gpu-memory-utilization 0.85 | 85% of unified memory | Leaves 15% for CPU/OS |
|
|
||||||
| --swap-space 8 | 8 GB CPU swap | Matches system swap size |
|
|
||||||
| --max-model-len 131072 | 128K context | Full 262K is large but possible with 61 GB |
|
|
||||||
| --language-model-only | Skip vision | Saves ~1 GB for KV cache |
|
|
||||||
| --reasoning-parser qwen3 | Qwen3 parser | Required for thinking mode |
|
|
||||||
|
|
||||||
## Sampling Parameters
|
|
||||||
|
|
||||||
| Mode | Temperature | Top P | Top K | Min P | Presence Penalty |
|
|
||||||
|------|-------------|-------|-------|-------|-----------------|
|
|
||||||
| Thinking (general) | 1.0 | 0.95 | 20 | 0.0 | 0.0 |
|
|
||||||
| Thinking (coding) | 0.6 | 0.95 | 20 | 0.0 | 0.0 |
|
|
||||||
| Non-thinking | 0.7 | 0.80 | 20 | 0.0 | 1.5 |
|
|
||||||
|
|
||||||
Set `max_tokens` to 32768 for general use. Set to 81920 for complex reasoning.
|
|
||||||
|
|
||||||
## API Examples
|
|
||||||
|
|
||||||
### Chat with Thinking
|
|
||||||
|
|
||||||
```
|
|
||||||
curl http://localhost:8000/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "Qwen/Qwen3.6-27B-FP8",
|
|
||||||
"messages": [{"role": "user", "content": "Solve: 2+2"}],
|
|
||||||
"max_tokens": 32768,
|
|
||||||
"temperature": 1.0,
|
|
||||||
"top_p": 0.95,
|
|
||||||
"top_k": 20
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Non-Thinking Mode
|
|
||||||
|
|
||||||
```
|
|
||||||
curl http://localhost:8000/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "Qwen/Qwen3.6-27B-FP8",
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}],
|
|
||||||
"max_tokens": 2048,
|
|
||||||
"temperature": 0.7,
|
|
||||||
"top_p": 0.8,
|
|
||||||
"top_k": 20,
|
|
||||||
"presence_penalty": 1.5,
|
|
||||||
"extra_body": {
|
|
||||||
"chat_template_kwargs": {"enable_thinking": false}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Calling
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-27B-FP8 \
|
|
||||||
--port 8000 \
|
|
||||||
--reasoning-parser qwen3 \
|
|
||||||
--enable-auto-tool-choice \
|
|
||||||
--tool-call-parser qwen3_coder
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ultra-Long Context (YaRN)
|
|
||||||
|
|
||||||
Extend context beyond 262,144 tokens:
|
|
||||||
|
|
||||||
```
|
|
||||||
VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 vllm serve Qwen/Qwen3.6-27B-FP8 \
|
|
||||||
--hf-overrides '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}' \
|
|
||||||
--max-model-len 524288
|
|
||||||
```
|
|
||||||
|
|
||||||
## ROCm Notes
|
|
||||||
|
|
||||||
- Strix Halo (gfx1151) is listed as a supported AMD GPU for vLLM
|
|
||||||
- FP8 uses **software emulation** on RDNA architecture (gfx1151 has no native FP8 tensor cores). This gives memory savings, not full compute speedup. Compare against a BF16 test first
|
|
||||||
- Attention backends: try `ROCM_AITER_UNIFIED_ATTN` if the default backend underperforms
|
|
||||||
- Context length on ROCm may be limited for some models. Start with 131072 and increase gradually
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Out of Memory
|
|
||||||
|
|
||||||
1. Use `--language-model-only` to skip vision encoder
|
|
||||||
2. Reduce `--gpu-memory-utilization` to 0.75
|
|
||||||
3. Reduce `--max-model-len` to 65536
|
|
||||||
4. Reduce `--max-num-batched-tokens` to 1024
|
|
||||||
|
|
||||||
### FP8 Errors
|
|
||||||
|
|
||||||
1. Try BF16 first: `vllm serve Qwen/Qwen3.6-27B --dtype bfloat16`
|
|
||||||
2. Verify ROCm version supports FP8
|
|
||||||
3. Set `HSA_OVERRIDE_GFX_VERSION=11.5.1`
|
|
||||||
4. Use `--kv-cache-dtype auto` instead of fp8
|
|
||||||
|
|
||||||
### Slow Inference
|
|
||||||
|
|
||||||
1. Check GPU utilization with `rocm-smi`
|
|
||||||
2. Verify attention backend is optimal
|
|
||||||
3. Increase `--max-num-batched-tokens` if memory allows
|
|
||||||
4. Ensure CPU cores are not throttled
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[Dankclaw System Specs]]
|
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
|
||||||
- [[RAM CPU Offloading in VLLM]]
|
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]]
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "Serving a mixture-of-experts FP8 checkpoint on the R9700 host: the load path, the format gate, the offload shape."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
timestamp: 2026-09-03
|
|
||||||
---
|
|
||||||
|
|
||||||
# Running Qwen3.6 35B A3B FP8 on Dankserver
|
|
||||||
|
|
||||||
> **RETIRED.** This page documents the FP8 vLLM path for Qwen3.6-35B-A3B on Dankserver. The llama.cpp 35B service is retired (2026-08-16), its unit file was removed (2026-08-17), and the FP8 vLLM unit (`vllm-qwen36`) never shipped and is disabled. The content is kept as research history. The RTX 3090 now runs the PP worker: see [[Qwen3.8 PP Deployment]].
|
|
||||||
|
|
||||||
## Model Information
|
|
||||||
|
|
||||||
Qwen3.6-35B-A3B is a Mixture-of-Experts vision-language model from the Qwen3.6 series. It uses 35B total parameters with only 3B activated per token.
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| HuggingFace ID | `Qwen/Qwen3.6-35B-A3B-FP8` |
|
|
||||||
| Base Model | `Qwen/Qwen3.6-35B-A3B` |
|
|
||||||
| Architecture | Qwen3_5MoeForConditionalGeneration |
|
|
||||||
| Total Parameters | 35B |
|
|
||||||
| Activated Parameters | 3B |
|
|
||||||
| Experts | 256 (8 routed + 1 shared per token) |
|
|
||||||
| Layers | 40 (10 blocks of 3 DeltaNet + 1 Attention) |
|
|
||||||
| Attention Heads | 16 Q, 2 KV (head dim 256) |
|
|
||||||
| DeltaNet Heads | 32 V, 16 QK (head dim 128) |
|
|
||||||
| Expert Intermediate Dim | 512 |
|
|
||||||
| Vocab Size | 248,320 (padded) |
|
|
||||||
| Context Length | 262,144 (natively extensible to 1,010,000) |
|
|
||||||
| Quantization | FP8 (block size 128) |
|
|
||||||
| Vision Encoder | Yes (image-text-to-text) |
|
|
||||||
|
|
||||||
## Hardware: Dankserver
|
|
||||||
|
|
||||||
| Property | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| GPU | NVIDIA GeForce RTX 3090 |
|
|
||||||
| VRAM | 24 GB |
|
|
||||||
| System RAM | 62 GB |
|
|
||||||
| Swap | 127 GB |
|
|
||||||
|
|
||||||
## Memory Calculation
|
|
||||||
|
|
||||||
| Component | BF16 | FP8 |
|
|
||||||
|-----------|------|-----|
|
|
||||||
| Activated weights (3B) | 6 GB | 1.5 GB |
|
|
||||||
| Inactive experts (32B) | 64 GB | 16 GB |
|
|
||||||
| Vision encoder | ~1 GB | ~0.5 GB |
|
|
||||||
| **Total weights** | ~71 GB | ~18 GB |
|
|
||||||
|
|
||||||
The FP8 variant requires approximately 18 GB for weights. This leaves approximately 6 GB of the RTX 3090's 24 GB VRAM for KV cache and overhead. Use aggressive memory settings.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Install vLLM. Version 0.19.0 or later is required for Qwen3.6:
|
|
||||||
|
|
||||||
```
|
|
||||||
uv pip install vllm --torch-backend=auto
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hardware Quantization Notes (RTX 3090 / Ampere)
|
|
||||||
|
|
||||||
The RTX 3090 is an Ampere GPU (compute capability 8.6). It has **no native FP8 tensor cores**. FP8 W8A8 compute runs through a fallback/dequantization path, so the FP8 model gives memory savings but not full compute speedup. vLLM loads these models and runs them; verify quality against a BF16 run. `--kv-cache-dtype fp8` is still valid on CUDA 12 for an FP8 KV cache.
|
|
||||||
|
|
||||||
> Do not run on production without a BF16 comparison. The FP8 precision path may differ from native-FP8 hardware.
|
|
||||||
|
|
||||||
## vLLM Serve Commands
|
|
||||||
|
|
||||||
### Text-Only (Recommended for Memory Savings)
|
|
||||||
|
|
||||||
Skip the vision encoder to free VRAM for KV cache:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
|
|
||||||
--port 8000 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.75 \
|
|
||||||
--swap-space 32 \
|
|
||||||
--max-model-len 131072 \
|
|
||||||
--max-num-batched-tokens 2048 \
|
|
||||||
--max-num-seqs 32 \
|
|
||||||
--reasoning-parser qwen3 \
|
|
||||||
--dtype auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Vision Encoder
|
|
||||||
|
|
||||||
If you need multimodal input, expect higher VRAM usage:
|
|
||||||
|
|
||||||
```
|
|
||||||
vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
|
|
||||||
--port 8000 \
|
|
||||||
--kv-cache-dtype fp8 \
|
|
||||||
--gpu-memory-utilization 0.7 \
|
|
||||||
--swap-space 32 \
|
|
||||||
--max-model-len 131072 \
|
|
||||||
--max-num-batched-tokens 1024 \
|
|
||||||
--max-num-seqs 16 \
|
|
||||||
--reasoning-parser qwen3 \
|
|
||||||
--dtype auto
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters Explained
|
|
||||||
|
|
||||||
| Parameter | Value | Reason |
|
|
||||||
|-----------|-------|--------|
|
|
||||||
| --kv-cache-dtype fp8 | FP8 KV cache | Cuts KV cache memory by 50% |
|
|
||||||
| --gpu-memory-utilization 0.75 | 75% of VRAM | Leaves room for all MoE weights |
|
|
||||||
| --swap-space 32 (removed) | 32 GB CPU overflow | Modern vLLM removed swap-space; use --kv-offloading-size |
|
|
||||||
| --max-model-len 131072 | 128K context | Full 262K context is too large for 24 GB |
|
|
||||||
| --max-num-batched-tokens 2048 | Conservative batch | Prevents OOM with MoE |
|
|
||||||
| --reasoning-parser qwen3 | Qwen3 parser | Required for thinking mode output |
|
|
||||||
|
|
||||||
### Model Storage
|
|
||||||
|
|
||||||
Root disk has only 20 GB free. Store models on /mnt/entropy:
|
|
||||||
|
|
||||||
```
|
|
||||||
export HF_HOME=/mnt/entropy/huggingface
|
|
||||||
```
|
|
||||||
|
|
||||||
### Active Service Conflict
|
|
||||||
|
|
||||||
The retired `frank-zealot` service (llama.cpp) used about 22 GB of the 24 GB VRAM. The current PP worker holds ~23.8 GiB. The `vllm-awq-qwen36` (Podman) and `vllm-qwen36` units are disabled. Before starting any research service, verify that the GPU is free:
|
|
||||||
|
|
||||||
```
|
|
||||||
nvidia-smi # verify GPU is free
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sampling Parameters
|
|
||||||
|
|
||||||
| Mode | Temperature | Top P | Top K | Min P | Presence Penalty |
|
|
||||||
|------|-------------|-------|-------|-------|-----------------|
|
|
||||||
| Thinking (general) | 1.0 | 0.95 | 20 | 0.0 | 0.0 |
|
|
||||||
| Thinking (coding) | 0.6 | 0.95 | 20 | 0.0 | 0.0 |
|
|
||||||
| Non-thinking | 0.7 | 0.80 | 20 | 0.0 | 1.5 |
|
|
||||||
|
|
||||||
Set `max_tokens` to 32768 for general use. Set to 81920 for complex reasoning.
|
|
||||||
|
|
||||||
## API Examples
|
|
||||||
|
|
||||||
### Chat with Thinking
|
|
||||||
|
|
||||||
```
|
|
||||||
curl http://localhost:8000/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "Qwen/Qwen3.6-35B-A3B-FP8",
|
|
||||||
"messages": [{"role": "user", "content": "Solve: 2+2"}],
|
|
||||||
"max_tokens": 32768,
|
|
||||||
"temperature": 1.0,
|
|
||||||
"top_p": 0.95,
|
|
||||||
"top_k": 20
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Non-Thinking Mode
|
|
||||||
|
|
||||||
```
|
|
||||||
curl http://localhost:8000/v1/chat/completions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"model": "Qwen/Qwen3.6-35B-A3B-FP8",
|
|
||||||
"messages": [{"role": "user", "content": "Hello"}],
|
|
||||||
"max_tokens": 2048,
|
|
||||||
"temperature": 0.7,
|
|
||||||
"top_p": 0.8,
|
|
||||||
"top_k": 20,
|
|
||||||
"presence_penalty": 1.5,
|
|
||||||
"extra_body": {
|
|
||||||
"chat_template_kwargs": {"enable_thinking": false}
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ultra-Long Context (YaRN)
|
|
||||||
|
|
||||||
Extend context beyond 262,144 tokens. Use this environment variable and override:
|
|
||||||
|
|
||||||
```
|
|
||||||
VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 vllm serve Qwen/Qwen3.6-35B-A3B-FP8 \
|
|
||||||
--hf-overrides '{"text_config": {"rope_parameters": {"mrope_interleaved": true, "mrope_section": [11, 11, 10], "rope_type": "yarn", "rope_theta": 10000000, "partial_rotary_factor": 0.25, "factor": 4.0, "original_max_position_embeddings": 262144}}}' \
|
|
||||||
--max-model-len 524288
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Out of Memory
|
|
||||||
|
|
||||||
1. Kill llama-server to free 20 GB VRAM
|
|
||||||
2. Use `--language-model-only` to skip vision encoder
|
|
||||||
3. Reduce `--max-model-len` to 65536
|
|
||||||
4. Reduce `--max-num-batched-tokens` to 512
|
|
||||||
5. Reduce `--max-num-seqs` to 8
|
|
||||||
|
|
||||||
### Slow Inference
|
|
||||||
|
|
||||||
1. Verify GPU is exclusive to vLLM
|
|
||||||
2. Check temperature with `nvidia-smi`
|
|
||||||
3. Ensure `--kv-cache-dtype fp8` is set
|
|
||||||
4. Increase `--max-num-batched-tokens` if memory allows
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[Dankserver System Specs]]
|
|
||||||
- [[Hybrid CPU GPU Processing Dankserver]]
|
|
||||||
- [[RAM CPU Offloading in VLLM]]
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]]
|
|
||||||
@@ -85,7 +85,7 @@ Available backends for ROCm:
|
|||||||
|
|
||||||
All seven names exist on current vLLM `main` (`AttentionBackendEnum` in
|
All seven names exist on current vLLM `main` (`AttentionBackendEnum` in
|
||||||
`vllm/v1/attention/backends/registry.py` —
|
`vllm/v1/attention/backends/registry.py` —
|
||||||
[source](https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/v1/attention/backends/registry.py),
|
[source](https://raw.githubusercontent.com/vllm-project/vllm/main/vllm/v1/attention/backends/registry.py)
|
||||||
read 2026-10-18), and `--attention-backend`, `--numa-bind`, `--numa-bind-nodes`
|
read 2026-10-18), and `--attention-backend`, `--numa-bind`, `--numa-bind-nodes`
|
||||||
are real `vllm serve` flags (`vllm/engine/arg_utils.py:1098,1171-1176`).
|
are real `vllm serve` flags (`vllm/engine/arg_utils.py:1098,1171-1176`).
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ vllm serve model \
|
|||||||
|
|
||||||
- `gpu_memory_utilization`: Set lower (0.8-0.85) to leave room for CPU operations
|
- `gpu_memory_utilization`: Set lower (0.8-0.85) to leave room for CPU operations
|
||||||
- `max_model_len`: Match actual context needs
|
- `max_model_len`: Match actual context needs
|
||||||
- `--swap-space` is retired from current vLLM; the CPU KV-offload knob pair, with the upstream `CacheConfig` verification, is owned by [[RAM CPU Offloading in VLLM]]
|
- `--swap-space` is retired from current vLLM; the CPU KV-offload knob pair, with the upstream `CacheConfig` verification, is owned by
|
||||||
|
|
||||||
### NUMA Binding
|
### NUMA Binding
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ Unified memory limits concurrent requests:
|
|||||||
|
|
||||||
The two "No" rows (Async Output, Multi-step Decoding) were transcribed from a
|
The two "No" rows (Async Output, Multi-step Decoding) were transcribed from a
|
||||||
vLLM feature-support snapshot and not re-checked against current upstream
|
vLLM feature-support snapshot and not re-checked against current upstream
|
||||||
*(unverified 2026-10-18)*.
|
- (unverified 2026-10-18)*.
|
||||||
|
|
||||||
### Context Length
|
### Context Length
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ FP8 quantization depends on GPU architecture:
|
|||||||
- MI300 series supports native FP8
|
- MI300 series supports native FP8
|
||||||
- Radeon RX 7900 supports FP8 via software emulation
|
- Radeon RX 7900 supports FP8 via software emulation
|
||||||
- Strix Halo (gfx1151) uses **software emulation** for FP8 matmuls — RDNA 3.5 has
|
- Strix Halo (gfx1151) uses **software emulation** for FP8 matmuls — RDNA 3.5 has
|
||||||
no FP8 tensor instructions ([FP8-ROCM](https://github.com/wabibito/FP8-ROCM));
|
no FP8 tensor instructions ([FP8-ROCM](https://github.com/wabibito/FP8-ROCM))
|
||||||
expect memory savings, not full compute speedup. (Conflicting vendor-side
|
expect memory savings, not full compute speedup. (Conflicting vendor-side
|
||||||
evidence exists for an FP8/INT8 WMMA path used by FSR4 on RDNA 3.5, so treat
|
evidence exists for an FP8/INT8 WMMA path used by FSR4 on RDNA 3.5, so treat
|
||||||
the matmul claim as the settled half.)
|
the matmul claim as the settled half.)
|
||||||
@@ -294,8 +294,3 @@ rocm-smi --showtemperature
|
|||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[VLLM Index]]
|
|
||||||
- [[RAM CPU Offloading in VLLM]]
|
|
||||||
- [[Hybrid CPU GPU Processing Dankserver]]
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]]
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
title: Tcclaviger Fork Ops
|
|
||||||
description: "Operating the RDNA4 vLLM fork: the sweep harness, measured knob verdicts, sweep hygiene, compile-cache persistence."
|
|
||||||
tags: [tcclaviger, vllm, dankserver, expert-offload, sweeps, cache]
|
|
||||||
timestamp: 2026-10-05T00:00:00-07:00
|
|
||||||
---
|
|
||||||
|
|
||||||
# Tcclaviger Fork Ops
|
|
||||||
|
|
||||||
The tcclaviger RDNA4 vLLM fork (`docker.io/tcclaviger/vllm@sha256:da4029666…`,
|
|
||||||
vLLM 0.29.0.dev, GLM-5.3/Qwen-specialised with native MoE quant kernels and a
|
|
||||||
first-class expert-offload planner) served `frank-archon` at shape G until
|
|
||||||
the 2026-10-05 FreeToken cutover; it remains the rollback target
|
|
||||||
(`git tag frank-archon-vllm-shapeG-20261003`). Full plan law and VRAM
|
|
||||||
arithmetic: [[RDNA4 MoE Expert Offload VRAM Budget]]; boot forensics:
|
|
||||||
[[vLLM Boot Forensics and Config Interrogation]]; window discipline:
|
|
||||||
[[LLM Bench Windows and Measurement]].
|
|
||||||
|
|
||||||
## The ad-hoc sweep harness (dankserver)
|
|
||||||
|
|
||||||
Serve script `/home/dank/serve_qwen_fp8.sh` with env hooks: `SPEC` (MTP
|
|
||||||
depth), `TUNABLE` (TunableOp), `PREFETCH`, `UTIL`, `MAXLEN`, `KV_BYTES`,
|
|
||||||
`MNBT`, `UVA_TOK`, `GATHER_C/GATHER_L` (R4D_LRU gather), `TELEM`
|
|
||||||
(R4D_LRU_TELEMETRY), `PROFDIR` (torch profiler bind-mount + --profiler-config
|
|
||||||
JSON). The script re-copies the harness (`mtp_measure.py`, `bench_iter.py`)
|
|
||||||
into every fresh container — `podman cp` files die on container recreate.
|
|
||||||
|
|
||||||
- Measure ONLY via usage counters: `mtp_measure.py` (3 depths) or
|
|
||||||
`mtp_measure_short.py` (166 ctx). `decode = completion_tokens/(wall −
|
|
||||||
ttft)`; chunk counting is INVALID under spec decode.
|
|
||||||
- Jobs: `bash /home/dank/job.sh start NAME CMD…` / `wait NAME SECS 40` /
|
|
||||||
`out NAME` ([[Remote Job Runner]]).
|
|
||||||
- **Validity check on ANY speed result:** `completion_tokens == max_tokens`
|
|
||||||
(else repetition-halt or early EOS), finish_reason, and inspect the text —
|
|
||||||
`bench_iter` prompts are repeated word-lists + `!` padding, loop-prone. A
|
|
||||||
loop makes LRU hits ~100% → PCIe traffic vanishes → fake 3–4× decode.
|
|
||||||
Acceptance swings 48–61% run-to-run — noise; decode is the metric.
|
|
||||||
- Each arm = full reload: ~8.5 min cold, ~12 min with measurement.
|
|
||||||
- Readiness gate: `podman logs <c> 2>&1` (stdout+stderr merged) for
|
|
||||||
`Application startup complete` — it is on STDERR; an unmerged grep can
|
|
||||||
never match and burns the whole wait timeout.
|
|
||||||
- GPU util ~0% during load/compile is NORMAL (weights + CUDA-graph capture
|
|
||||||
are CPU-side); VRAM climbing is the load indicator.
|
|
||||||
- Config edit discipline: the serve script is patched by scp'd python
|
|
||||||
scripts with asserts (anchor must exist, addition must not); `bash -n`
|
|
||||||
after every patch.
|
|
||||||
|
|
||||||
## Measured knob verdicts (2026-09-29 campaign)
|
|
||||||
|
|
||||||
Incumbent: `SPEC=4 PREFETCH=1 UTIL=0.98 MAXLEN=32768 KV_BYTES=2147483648
|
|
||||||
TUNABLE=0`, stock gather 8×16 → 26.80/25.15/26.81 tok/s @ 166/1087/4064 ctx.
|
|
||||||
|
|
||||||
| Knob | Verdict | Evidence |
|
|
||||||
|---|---|---|
|
|
||||||
| Gather geometry `R4D_LRU_GATHER_{CHUNKS,LANES}` | **RULED OUT** | 16×32 worse (~24); 32×64 tie (26.6). The 9.5 vs 22.7 GB/s read-path gap is per-transfer structure (~105 µs/slab vs 78 ideal), not a tunable knob |
|
|
||||||
| MTP depth | **RULED OUT** | SPEC=3 ~9% worse; SPEC=6 collapses (13.5–19 tok/s, acceptance 17.4%) — also proves UVA read-through (crosses at step tokens > `R4D_LRU_UVA_TOKENS=5`) is SLOWER than copy-in |
|
|
||||||
| TunableOp | **REJECTED** | `CLAV_TUNABLEOP_SWEEP=1` silently SKIPPED under expert offload unless `CLAV_TUNABLEOP_WITH_OFFLOAD=1`; adds ~7 min/load, persists nothing (container-local), and CORRUPTS NUMERICS (loops, repetition-halt, fake 79.8 tok/s @ 96.5% acceptance on degenerate completions). Any "3× win" with early-EOS + absurd acceptance = the loop artifact |
|
|
||||||
| MNBT | neutral at 8192 vs 2048 | small values re-chunk prefill only (hurts TTFT, never decode — decode step = max-num-seqs × (spec+1) tokens). No ubatch concept in vLLM |
|
|
||||||
|
|
||||||
Decode on this stack is PCIe-gather-bound (~30% GPU duty cycle), so
|
|
||||||
batch-shaped compute knobs move ~nothing.
|
|
||||||
Measured with no spec: **12.11 tok/s FLAT from 304 to 16,095 context tokens** — KV
|
|
||||||
attention is not the cost, so a context-slope hunt on this stack is null by construction.
|
|
||||||
|
|
||||||
The `R4D_*/CLAV_*` knob family (QSA_*, MHC, ROUTE, ACT_QUANT, AR_QUANT,
|
|
||||||
SILU_QUANT) are kill-switches defaulting ON, not perf dials; real levers are
|
|
||||||
the R4D_LRU_* family, TunableOp (rejected), R4D_LRU_TELEMETRY, and spec
|
|
||||||
depth.
|
|
||||||
|
|
||||||
Telemetry gotchas: `--profiler-config` takes a JSON blob, not separate
|
|
||||||
flags; `torch_profiler_with_stack` defaults TRUE — a full measure inside a
|
|
||||||
profile window OOM-kills the engine worker (~40 GB trace buffer; kernel
|
|
||||||
kills python3 at ~95 GB anon-rss). Keep windows to ~15 s / one depth
|
|
||||||
(`mtp_measure_short.py`). `telemetry_report` (`expert_cache_rank<N>.json`)
|
|
||||||
fires ONLY from the worker's profiler-stop path; worker-side stop can hang
|
|
||||||
>5 min on ROCm/kineto. No alternative trigger exists. The LRU hit rate
|
|
||||||
measured 82.45% — NOT the ~27% the arena-fraction arithmetic implies;
|
|
||||||
temporal reuse dominates, so never infer miss rates from resident-slot
|
|
||||||
fractions.
|
|
||||||
|
|
||||||
## Sweep hygiene (fork-specific preflight)
|
|
||||||
|
|
||||||
1. **Knob landed:** for each `-e` env knob, confirm in-container
|
|
||||||
`podman exec … env` (or the engine log's confirmed-values line). A knob
|
|
||||||
the fork silently ignores produces a "parity" arm that is really the
|
|
||||||
default — the v1 gather sweep lost all three arms to exactly this
|
|
||||||
(GATHER_C/GATHER_L silently dropped, all arms ran stock 8×16).
|
|
||||||
2. **Prefetch uniformity:** every arm in a sweep MUST carry the same
|
|
||||||
expert-prefetch setting. The 2026-09-29 campaign measured losing arms
|
|
||||||
with prefetch ON and incumbents OFF; untaxed, its verdicts inverted
|
|
||||||
(no-spec 12.11→41.47; gather 32×64 parity→+18.7%). Never re-read a taxed
|
|
||||||
verdict.
|
|
||||||
3. **Spec arms must prove engagement:** engine log
|
|
||||||
`SpecDecoding metrics: Mean acceptance length …` and
|
|
||||||
`curl :7770/metrics | grep vllm:spec_decode_`. Zero such lines = MTP off.
|
|
||||||
4. Boot-fail fast-detection: poll `podman ps` INSIDE the health loop; a
|
|
||||||
refused boot dies in one poll instead of 20 min of health waits. Capture
|
|
||||||
>8 lines of `podman logs` on failure — the root cause (e.g. a SEQS init
|
|
||||||
refusal) lives higher up.
|
|
||||||
5. Report per-depth tables, never one number: g32×64's win grows with depth
|
|
||||||
(54@1k, 39.5@short) — a single 4064 median hides the short-ctx cost. TTFT
|
|
||||||
rows are noisy ±0.15 s and non-monotonic across depths; compare same-ctx
|
|
||||||
rows across arms only.
|
|
||||||
6. Generate ≥256 tokens per sample (at 8 tokens inter_token is first-token
|
|
||||||
noise).
|
|
||||||
|
|
||||||
Boot-log facts survive measure failures: KV pool size (`GPU KV cache size`),
|
|
||||||
arena slots (`expert cache: … -> N slots`), and the spec-reservation ladder
|
|
||||||
(MTP3 766,666 / MTP4 729,545 / MTP6 665,263 tokens) print at BOOT — a
|
|
||||||
measure-stage bug costs decode tables but not boot-log findings.
|
|
||||||
|
|
||||||
**MTP depth-brittleness law (2026-10-02, all arms rc=0):** MTP3 on this fork
|
|
||||||
is depth-brittle — 47.01 t/s at 166 tokens (47.88 at temp 0.7) but 23.09 at
|
|
||||||
4064, vs no-spec baseline 37.02 / 33.99. MTP3 + SEQS 16 + MNBT 4096 + util
|
|
||||||
0.95 + ladder [4..32] = best overall 37.04 / 38.61 t0.7 and keeps 33.15 at
|
|
||||||
4064. `--expert-precise-memory` is load-bearing for MTP (same arm with it
|
|
||||||
OFF = worst arm, 19.81 at 4064). Every spec arm costs KV (398,658 → 313,002
|
|
||||||
tokens). Acceptance is real: mean length 2.4–2.8, per-position
|
|
||||||
0.76/0.55/0.33, avg 52–57%. Next lever named by the engine: MNBT 4096 is
|
|
||||||
exactly SEQS×(n+1) (`suboptimal` warning) → try 8192.
|
|
||||||
|
|
||||||
## Serving a model larger than VRAM (dankserver recipe)
|
|
||||||
|
|
||||||
Dankserver's card count, per-card VRAM and host RAM are in [[Dankserver System Specs]];
|
|
||||||
`torch.cuda.device_count()` returns 3 on this box and summing `total_memory` overstates it
|
|
||||||
badly (the Raphael iGPU is in the list — reading discipline: [[Llama Cpp Serving Sizing]]).
|
|
||||||
|
|
||||||
Everything else about running this image — the quant-kernel gate that decides
|
|
||||||
which checkpoints can serve at all, the entrypoint contract, the known-good
|
|
||||||
invocation, the `/dev/dri` and memlock traps, the util and max-model-len arena
|
|
||||||
traps, the `--ipc=host` / `-d` stdout traps and the gated-fetch discipline — is
|
|
||||||
written once on [[RDNA4 Container Serving]], and the plan arithmetic behind the
|
|
||||||
arena/KV traps is [[RDNA4 MoE Expert Offload VRAM Budget]]'s. Three findings
|
|
||||||
stay here because this harness produced them:
|
|
||||||
|
|
||||||
- **The KV reserve undercounts hybrid-attention models ~2×** (the planner's
|
|
||||||
B/token figure against ~18,000 real: the Mamba state is a fixed per-sequence
|
|
||||||
cost). Fix by pinning the pool: `--kv-cache-memory-bytes`.
|
|
||||||
- **`--expert-offload-mem` caps the HOST side**, not the VRAM arena —
|
|
||||||
structurally orthogonal to `--expert-precise-memory` (verified in-container
|
|
||||||
2026-10-05: precise-memory only swaps the GPU overhead term,
|
|
||||||
expert_offload_mem only sets the host bound; they rejoin only inside
|
|
||||||
`place_experts`, and the plan rewrites `expert_offload_mem` back to the cap
|
|
||||||
the plan met, so plan and allocation cannot drift).
|
|
||||||
- **Check whether the stack supports the model natively before porting an
|
|
||||||
overlay.** The image already registered `Glm5NextForConditionalGeneration`,
|
|
||||||
`Qwen4ExpForCausalLM` and both MTP variants — chasing four missing symbols
|
|
||||||
through two image tags to bring an overlay up was entirely wasted work.
|
|
||||||
|
|
||||||
The checkpoint this recipe was verified on is
|
|
||||||
`orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8` (gated; 172.8 GiB, 131 shards,
|
|
||||||
`Qwen4ExpForConditionalGeneration`, 48 layers, 512 experts top-10, multimodal,
|
|
||||||
native 262,144) — an [[frank-archon]] era payload.
|
|
||||||
|
|
||||||
## Compile-cache persistence (survives `podman rm -f`)
|
|
||||||
|
|
||||||
Serve scripts that recreate the container discard the writable layer, so
|
|
||||||
every boot re-pays compile/JIT (frank-archon: `torch.compile took 181.20 s`
|
|
||||||
cold, of which 173 s was one graph).
|
|
||||||
|
|
||||||
1. **Prove it and locate it:** fresh container vs live
|
|
||||||
(`podman run --rm --entrypoint bash IMG -c "ls -la /root/.cache"` vs
|
|
||||||
`podman exec LIVE bash -c "du -sb /root/.cache/*; find /root/.cache -type
|
|
||||||
f | wc -l"`). vLLM cache root: `VLLM_CACHE_ROOT` (default
|
|
||||||
`/root/.cache/vllm`, `torch_compile_cache/`); a HIP runtime adds
|
|
||||||
`/root/.cache/comgr`.
|
|
||||||
2. **Mount the PARENT, not the leaf** — one bind on `/root/.cache` persists
|
|
||||||
both trees, safe whenever the image ships no such directory (step 1
|
|
||||||
proves it; otherwise you would be hiding baked state). Wire as an opt-in
|
|
||||||
tunable: `mkdir -p "$CACHE_DIR"` BEFORE the mount, then
|
|
||||||
`${CACHE_DIR:+-v "$CACHE_DIR":/root/.cache}`. Host path outside the
|
|
||||||
checkout.
|
|
||||||
3. **Verify with two consecutive boots** — cold `Compiling a graph … takes
|
|
||||||
N s`, warm `Directly load the compiled graph(s) for compile range (…)
|
|
||||||
from the cache` with `torch.compile took` in single-digit seconds.
|
|
||||||
4. Traps: the cache key includes the compile RANGE — an MNBT change costs
|
|
||||||
ONE recompile, not a permanent tax; do not claim a proportional boot cut
|
|
||||||
(time to `/health` is dominated by weight loading: 490 s warm vs 188 s
|
|
||||||
cold compile); stale entries are misses, not wrong answers (version +
|
|
||||||
config hashed); if the serve script is seam-gated, assert the mkdir and
|
|
||||||
the exact mount target.
|
|
||||||
|
|
||||||
## Plan-line verification checklist (after any fork deploy)
|
|
||||||
|
|
||||||
The journal one-liner that reads the plan lines (digit-anchored `grep -oE`,
|
|
||||||
because plain grep truncates those columns to `17.`) and the post-deploy
|
|
||||||
verification order — plan vs captured graph memory, `GPU KV cache size` vs the
|
|
||||||
ask, per-card `rocm-smi` high-water, `NRestarts` = 0, `VLLM_CUDAGRAPH_*` present
|
|
||||||
in the container env, then a real completion from a PEER because
|
|
||||||
`curl localhost:7770/health` inside an ssh session on the host can return `000`
|
|
||||||
and look like an outage — are step 3 of
|
|
||||||
[[RDNA4 MoE Expert Offload VRAM Budget]] §7. Run that; do not re-derive it
|
|
||||||
here.
|
|
||||||
|
|
||||||
## Related
|
|
||||||
|
|
||||||
- [[RDNA4 MoE Expert Offload VRAM Budget]] — the plan law this harness tunes
|
|
||||||
- [[vLLM Boot Forensics and Config Interrogation]] — journal forensics for these boots
|
|
||||||
- [[LLM Bench Windows and Measurement]] — window discipline and bash laws
|
|
||||||
- [[Model Swap and Rollback]] — where the fork sits in the swap history
|
|
||||||
- [[Remote Job Runner]] — the job.sh harness referenced above
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
---
|
|
||||||
type: reference
|
|
||||||
description: "Locator for every vLLM page in this vault."
|
|
||||||
tags:
|
|
||||||
- llm
|
|
||||||
- vllm
|
|
||||||
timestamp: 2026-08-28
|
|
||||||
---
|
|
||||||
|
|
||||||
# vLLM Index
|
|
||||||
|
|
||||||
> **RETIRED (2026-08-18).** The vLLM service on Dankclaw was replaced by the CIRU ROCmFPX llama.cpp runtime (v3). These pages are historical reference. See [[frank-dragoon]].
|
|
||||||
|
|
||||||
## Documentation Overview
|
|
||||||
|
|
||||||
This index contains all documentation for vLLM deployment on Dankserver and Dankclaw.
|
|
||||||
|
|
||||||
## Hardware Guides
|
|
||||||
|
|
||||||
- [[Dankserver System Specs]] — NVIDIA GPU server specifications
|
|
||||||
- [[Dankclaw System Specs]] — AMD GPU server specifications
|
|
||||||
|
|
||||||
## vLLM Architecture
|
|
||||||
|
|
||||||
- [[VLLM Overview]] — Complete vLLM documentation including installation, CLI, API, and key arguments
|
|
||||||
- [[RAM CPU Offloading in VLLM]] — CPU RAM offloading mechanisms, swap space, and memory configuration
|
|
||||||
- [[Hybrid CPU GPU Processing Dankserver]] — NVIDIA GPU with system RAM hybrid processing
|
|
||||||
- [[Shared Memory Architecture Dankclaw]] — AMD ROCm unified memory and APU configuration
|
|
||||||
|
|
||||||
## Model Deployment Guides
|
|
||||||
|
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]] — Qwen3.6-35B-A3B FP8 deployment on NVIDIA GPU
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]] — Qwen3.6-27B FP8 deployment on AMD GPU
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
### Installation Commands
|
|
||||||
|
|
||||||
```
|
|
||||||
# NVIDIA GPU
|
|
||||||
uv pip install vllm --torch-backend=auto
|
|
||||||
|
|
||||||
# AMD ROCm
|
|
||||||
uv pip install vllm --extra-index-url https://wheels.vllm.ai/rocm/
|
|
||||||
|
|
||||||
# CPU
|
|
||||||
uv pip install vllm --extra-index-url https://wheels.vllm.ai/nightly/cpu/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Start Commands
|
|
||||||
|
|
||||||
```
|
|
||||||
# Basic server
|
|
||||||
vllm serve Qwen/Qwen2.5-1.5B-Instruct
|
|
||||||
|
|
||||||
# With tensor parallelism
|
|
||||||
vllm serve meta-llama/Llama-3.1-70B-Instruct --tensor-parallel-size 4
|
|
||||||
|
|
||||||
# With quantization
|
|
||||||
vllm serve Qwen/Qwen2.5-1.5B-Instruct --quantization fp8
|
|
||||||
|
|
||||||
# With GPU memory control
|
|
||||||
vllm serve Qwen/Qwen2.5-1.5B-Instruct --gpu-memory-utilization 0.8 --kv-offloading-size 8 --kv-offloading-backend native
|
|
||||||
```
|
|
||||||
|
|
||||||
### Common API Endpoints
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|----------|--------|-------------|
|
|
||||||
| /v1/models | GET | List available models |
|
|
||||||
| /v1/completions | POST | Text completion |
|
|
||||||
| /v1/chat/completions | POST | Chat completion |
|
|
||||||
| /health | GET | Health check |
|
|
||||||
| /metrics | GET | Prometheus metrics |
|
|
||||||
|
|
||||||
### Key Parameters
|
|
||||||
|
|
||||||
| Parameter | Description | Typical Value |
|
|
||||||
|-----------|-------------|---------------|
|
|
||||||
| tensor_parallel_size | Number of GPUs for model sharding | 1-8 |
|
|
||||||
| gpu_memory_utilization | Fraction of GPU memory for KV cache (default 0.92) | 0.7-0.95 |
|
|
||||||
| swap_space | Retired; live knob pair in [[RAM CPU Offloading in VLLM]] | n/a |
|
|
||||||
| max_model_len | Maximum context length | auto |
|
|
||||||
| max_num_batched_tokens | Max tokens per batch | 2048-8192 |
|
|
||||||
| max_num_seqs | Max sequences per batch | 128-512 |
|
|
||||||
|
|
||||||
### Quantization Methods
|
|
||||||
|
|
||||||
| Method | GPU Support | Description |
|
|
||||||
|--------|-------------|-------------|
|
|
||||||
| FP8 | Hopper, Ada (native); Ampere W8A16 via Marlin; RDNA via emulation | FP8 E4M3 inference |
|
|
||||||
| INT8 | Most GPUs | Weight-only INT8 |
|
|
||||||
| GPTQ | Most GPUs (NVIDIA; limited on ROCm) | Post-training GPTQ |
|
|
||||||
| AWQ | Turing+ (NVIDIA) | Activation-aware weight quantization |
|
|
||||||
| GGUF | CPU, GPU | General GPU unification format |
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [[VLLM Overview]]
|
|
||||||
- [[RAM CPU Offloading in VLLM]]
|
|
||||||
- [[Hybrid CPU GPU Processing Dankserver]]
|
|
||||||
- [[Shared Memory Architecture Dankclaw]]
|
|
||||||
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]]
|
|
||||||
- [[Running Qwen3.6 27B FP8 on Dankclaw]]
|
|
||||||
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user