Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6d8610b3 | ||
|
|
933420fd29 | ||
|
|
9ffa99902d | ||
|
|
b8f564f3bd | ||
|
|
d42d053abe | ||
|
|
7eed636bd1 | ||
|
|
d9b82b372e | ||
|
|
3d10a4ca4f | ||
|
|
e5b731216c | ||
|
|
ce8d82d4a7 | ||
|
|
02d04bc1f6 | ||
|
|
428ac22736 | ||
|
|
5d5cac4da3 | ||
|
|
3eaba57925 | ||
|
|
9b9c32005f | ||
|
|
ea2fc6440b | ||
|
|
6248f02ca2 | ||
|
|
25df9d05a2 | ||
|
|
7601edfe28 |
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`
|
||||||
@@ -440,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,
|
||||||
@@ -458,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
|
||||||
|
|||||||
@@ -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[@]}"
|
||||||
@@ -136,25 +136,36 @@ TTS engine today|3.84 GiB RSS, CPU|its device-resident replacement lands in that
|
|||||||
ICD**, so Vulkan device 0 is the iGPU here. Do not trust that ordering — select by
|
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
|
device *name*, or an image that grows an `nvidia_icd.json` silently points TTS at the
|
||||||
3090 slot.
|
3090 slot.
|
||||||
2. **Contention gate.** `frank-dragoon` is the endpoint the operator session runs on, so the
|
2. **Contention gate — PASSED, measured 2026-10-19 after the cutover.** With a 16.9 s
|
||||||
honest measurement with both loaded is dragoon's own TTFT and decode with a synthesis in
|
synthesis occupying the device, `frank-dragoon` answered a streamed 124-token request at
|
||||||
flight — one request each, no ladder. Expect compute-unit sharing rather than memory
|
**17 ms to first token and ~35 tok/s decode**, against 17 ms and ~33 tok/s for the same
|
||||||
pressure: the aperture has room, the shaders do not.
|
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** — the whole edit, so nobody re-derives it:
|
**Change surface — as landed** (PR #34, `6248f02`; the CPU shape stays reachable as the
|
||||||
|
rollback):
|
||||||
|
|
||||||
1. `Containerfile.engine` build stage: add `libvulkan-dev glslang-dev`. It installs only
|
1. `Containerfile.engine` build stage: add **`libvulkan-dev glslc spirv-headers`**. The
|
||||||
`ca-certificates cmake g++ git make` today, and ggml's Vulkan backend compiles its
|
prediction above (`libvulkan-dev glslang-dev`) is wrong on two counts and the configure
|
||||||
shaders at build time — the flag alone fails at configure.
|
says so: ggml looks for `glslc`, which Ubuntu's `glslang-tools` does **not** ship (it
|
||||||
2. `-DGGML_VULKAN=OFF` → `ON` in the same cmake block; the flag list is written out
|
ships `glslangValidator`), and `find_package(SPIRV-Headers CONFIG REQUIRED)` needs the
|
||||||
explicitly, so this is a one-word diff.
|
distro's `spirv-headers`, which does carry the CMake config. `glslc` is its own source
|
||||||
3. Runtime stage: add `mesa-vulkan-drivers`. Without a Vulkan **ICD inside the image** the
|
package on noble, at 2023.8 — old enough that only `GL_KHR_cooperative_matrix` compiles,
|
||||||
device node buys nothing — there is no `radeon_icd.json` in the container today.
|
so the coopmat2 / integer-dot / bf16 / micro-float shader families are built out. Suspect
|
||||||
4. `frank-tts-engine.container`: `AddDevice=/dev/dri` + `GroupAdd=keep-groups` — **not**
|
that compiler before the device if a Vulkan prefill ever reads as slow.
|
||||||
`renderD128`. On this hybrid box that node is the NVIDIA card; the iGPU is
|
2. `-DGGML_VULKAN=OFF` → `ON`: one word in the written-out flag list.
|
||||||
`renderD129` (`readlink -f /sys/class/drm/renderD129/device/driver` → `amdgpu`).
|
3. Runtime stage: `mesa-vulkan-drivers` **and** `libvulkan1` — the ICD and its loader. An
|
||||||
5. Engine argv: `--device Vulkan0` — qwentts.cpp's tools take `--device <name>` at the
|
image with a loader and no ICD enumerates zero devices.
|
||||||
pinned commit.
|
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
|
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
|
throwaway container off the same base, `--device /dev/dri`, then `vulkaninfo --text` and read
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+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"
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -45,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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 , 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 |
|
||||||
@@ -131,3 +146,5 @@ still open.
|
|||||||
| `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 |
|
| `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 |
|
||||||
@@ -113,17 +113,18 @@ shows up as a failed synthesis, never as a down probe.
|
|||||||
|
|
||||||
## Where the engine computes
|
## Where the engine computes
|
||||||
|
|
||||||
CPU, and only CPU is compiled in: `Containerfile.engine` builds `qwentts.cpp` with
|
The **gfx1151 iGPU**, as a co-tenant beside `frank-dragoon`: the Vulkan build of
|
||||||
`-DGGML_VULKAN=OFF` written out explicitly, so the container carries `libggml-cpu` and no
|
`qwentts.cpp` with `--device Vulkan0`, and `/dev/dri` handed to the engine container. That is
|
||||||
other backend. The realistic accelerator is the gfx1151 iGPU behind that flag — plus
|
the shape since 2026-10-19. The flag, the device name, the render-node trap on this hybrid box
|
||||||
`/dev/dri` passthrough, the ComfyUI pattern — on the same payload and the same pinned
|
and the one-line CPU rollback are owned by [[Frank TTS Engine]] — read that page before
|
||||||
commit, and the intended shape is **co-tenancy**: the engine resident on the card beside
|
touching either tenant, because a change to one is a change to both. Co-tenancy was gated with
|
||||||
`frank-dragoon`, not a handover. Memory is not what decides that; shader contention is, and
|
`frank-dragoon` live on the card (its first token and decode rates unchanged with a synthesis
|
||||||
the census plus both gates are in `report/tts-npu-spike-verdict-2026-10-19.md` §5.
|
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
|
**The Strix Halo NPU is not a door at all**: XDNA2 executes only precompiled AIE partition
|
||||||
ioctls, then declines this model family's graph without emitting a single log line. The
|
programs, and AMD's own Linux runtime opens `/dev/accel/accel0`, succeeds at the ioctls, then
|
||||||
measured record and the model-side contract live in `report/` at the repo root
|
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`).
|
(`tts-npu-spike-verdict-2026-10-19.md`).
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|||||||
@@ -251,21 +251,20 @@ Knobs, in the order worth trying them (radiance GUIDE §4.5):
|
|||||||
the prefill stager's smallest pass — the startup line names it, and below it every
|
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
|
routed layer reads its pooled experts synchronously, which costs the prefill far
|
||||||
more than sharing saves. Counts as `engine_prefill_chunks_capped_total`.
|
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
|
- `--max-num-batched-tokens` — the blunt rung, and the only one older pins have. The vendor's own
|
||||||
vendor's curve on a 48-layer MoE at 200k on two cards: chunk 512 → 1750 t/s, 1024
|
curve prices **halving the shared chunk at roughly a fifth of prefill throughput** (a 48-layer MoE
|
||||||
→ 2200, 2048 → 2706 (a 185k prompt: 138 s against 91 s), so halving the stall
|
at 200k on two cards — knob pricing, not this endpoint's number, so treat it as the order of
|
||||||
costs ~19 % of prefill. Raising it past the interval buys nothing and loses expert
|
magnitude a chunk decision costs). Raising it past the checkpoint interval buys nothing and loses
|
||||||
residency to the arena.
|
expert residency to the arena.
|
||||||
- `--prefill-span N` — the inverse trade: a *lone* prefill carries N intervals a step
|
- `--prefill-span N` — the inverse trade: a *lone* prefill carries N intervals a step while every
|
||||||
while every shared step is planned at a smaller shared budget, which on an
|
shared step is planned at a smaller shared budget, which on an expert-tiered box lends that arena
|
||||||
expert-tiered box lends that arena back to the expert slab. Vendor TP2
|
back to the expert slab. The vendor's own measurement says the win is **long-prompt-only**: nothing
|
||||||
expert-tiered TTFT deltas vs a plain 2048 budget: −6.1 % at 2k, +2.3 % at 18k,
|
at short prompts, a large TTFT gain at the far end of the window.
|
||||||
+49.1 % at 250k — long prompts only.
|
|
||||||
|
|
||||||
Two traps. **The arena/slab coupling makes a shared step doubly expensive on an
|
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
|
expert-tiered shape**: the full-plan arena takes VRAM the resident expert slab would have had, so the
|
||||||
have had (320 MiB/card at TP2 for a 4096 budget), so the decode rows inside that step
|
decode rows inside that step also read experts over the link — a smaller shared budget helps twice.
|
||||||
also read experts over the link — a smaller shared budget helps twice. And **re-check
|
And **re-check
|
||||||
`prefix_hit_rate` / `prefix_evictions` after any chunk change**: the arena and the KV
|
`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
|
pool are the same memory, and the failure looks like turn two taking as long as turn
|
||||||
one.
|
one.
|
||||||
@@ -276,8 +275,9 @@ Availability is a version question, not a config question: `--busy-prefill-chunk
|
|||||||
tree, and check the source clone's depth before dating a flag from `git log -S`
|
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).
|
(a `--depth 1` clone reports its own tip for every search).
|
||||||
|
|
||||||
Run on: [[frank-archon]] (radiance 1.0.13, TP2 `expert_tiered`, budget 2048 = interval
|
Run on: [[frank-archon]] — the census was taken on its 3-slot radiance 1.0.13 era (budget 2048 =
|
||||||
2048) — the measured census and the verdict for this fleet's shape are that page's.
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: reference
|
type: reference
|
||||||
title: "frank-archon on Dankserver — radiance (StillDeadcode) 1.0.13, publisher Qwen3.8-Flash-Next iq4r .rad, 2× R9700 TP=2 (ACTIVE)"
|
title: "frank-archon on Dankserver — radiance (StillDeadcode) 1.8.1, publisher Qwen3.8-Flash-Next fp8-iq4r-moe .rad, 2× R9700 TP=2 (ACTIVE)"
|
||||||
description: "The radiance endpoint: live shape and harness contract, every era with its rollback path, and the laws it taught."
|
description: "The radiance endpoint: live shape and harness contract, every era with its rollback path, and the laws it taught."
|
||||||
tags:
|
tags:
|
||||||
- llm
|
- llm
|
||||||
@@ -12,7 +12,7 @@ tags:
|
|||||||
- tp2
|
- tp2
|
||||||
- expert-offload
|
- expert-offload
|
||||||
- endpoint
|
- endpoint
|
||||||
timestamp: 2026-10-18
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# frank-archon — radiance (StillDeadcode), 2× R9700 TP=2, Dankserver :7770
|
# frank-archon — radiance (StillDeadcode), 2× R9700 TP=2, Dankserver :7770
|
||||||
@@ -20,8 +20,9 @@ timestamp: 2026-10-18
|
|||||||
The radiance engine (C++/HIP, `libr4d` kernels built for gfx1201, no Python in the serving
|
The radiance engine (C++/HIP, `libr4d` kernels built for gfx1201, no Python in the serving
|
||||||
path) driving `podman run` straight from the digest-pinned image under a **root** systemd
|
path) driving `podman run` straight from the digest-pinned image under a **root** systemd
|
||||||
unit; container name = unit name. Host Dankserver (192.168.0.235), port 7770, both Radeon AI
|
unit; container name = unit name. Host Dankserver (192.168.0.235), port 7770, both Radeon AI
|
||||||
PRO R9700s. Capacity contract **3 conversations × 200,000 tokens at fp8 KV**; a payload swap
|
PRO R9700s. Capacity contract **2 conversations × 200,000 tokens at fp8 KV** — at 1.8.1 that pair
|
||||||
is a `MODEL_FILE` line plus a re-download — see Eras.
|
is the **demand signal** the budget resolver sizes the cache against, not a KV allocation (Laws).
|
||||||
|
A payload swap is a `MODEL_FILE` line plus a re-download — see Eras.
|
||||||
|
|
||||||
## Current shape
|
## Current shape
|
||||||
|
|
||||||
@@ -44,14 +45,20 @@ declared size + sha256) as the transient unit `iq4r-fetch` (log
|
|||||||
`/home/dank/models/StillDeadcode/…` keeps card, LICENSE and recipe as provenance, and only
|
`/home/dank/models/StillDeadcode/…` keeps card, LICENSE and recipe as provenance, and only
|
||||||
`.rad` files are mounted.
|
`.rad` files are mounted.
|
||||||
|
|
||||||
**Runtime** — `stilldeadcode/radiance@sha256:4c3f484564bb2872784e46b0a59cbd71becc9c1e746e301a3596ffae253c322e`
|
**Runtime** — `stilldeadcode/radiance@sha256:d590e300234bf6d2f942a0b8d456d183153fd808f28820e6c2fa69c9389178ed`
|
||||||
= pin **1.0.13** (Codeberg `v1.0.13`, commit `53282fa`): a 287 MiB image in dank's
|
= pin **1.8.1** (adopted 2026-10-18). The version oracle is the boot's own `STARTUP REPORT --
|
||||||
**rootless** storage, host needs nothing but the amdgpu driver. Pin the INDEX manifest digest,
|
radiance 1.8.1` line, because `podman inspect` reports only the digest. The image lives in dank's
|
||||||
never the image ID (`69d05724…` is the config digest and sends `podman run` to the network).
|
**rootless** storage and the host needs nothing but the amdgpu driver. Pin the INDEX manifest
|
||||||
The serve script's gate is that digest ref itself — `podman image exists name@sha256:…` in one
|
digest, never the image ID (`69d05724…` is the config digest and sends `podman run` to the
|
||||||
command (the 2026-09-30 no-pipe/grep-race lesson) — so a retagged look-alike or a newer
|
network). The serve script's gate is that digest ref itself — `podman image exists name@sha256:…`
|
||||||
`latest` cannot satisfy it. 1.2.0 is a rejected serve pin (Laws); the
|
in one command (the 2026-09-30 no-pipe/grep-race lesson) — so a retagged look-alike or a newer
|
||||||
`--vram-weights-mib`/`--vram-kv-mib` pair is accepted by both pins, which is the rollback symmetry.
|
`latest` cannot satisfy it. 1.2.0 is a rejected serve pin (Laws). **Two CLI breaks ride this pin,
|
||||||
|
and both are boot refusals, never silently-ignored flags:** the `--vram-weights-mib`/`--vram-kv-mib`
|
||||||
|
pair died at 1.3.0 (the resolver derives the split) and `--prefix-cache-dir` /
|
||||||
|
`--prefix-cache-disk-mib` died with the disk tier. A rollback to 1.0.13 (`sha256:4c3f4845…`) is
|
||||||
|
therefore a trio rewrite, not a digest swap — the Eras row carries it, and
|
||||||
|
`tests/frank_archon_unit.bats` asserts the retired knobs are ABSENT from env and argv so a stale
|
||||||
|
line cannot reach a boot.
|
||||||
|
|
||||||
**Trio** — `scripts/frank-archon.env` (every number, with its arithmetic in the header)
|
**Trio** — `scripts/frank-archon.env` (every number, with its arithmetic in the header)
|
||||||
`scripts/serve-frank-archon.sh`, `scripts/frank-archon.service`. Gates run in order: image
|
`scripts/serve-frank-archon.sh`, `scripts/frank-archon.service`. Gates run in order: image
|
||||||
@@ -64,8 +71,10 @@ the name registered, and every one of those identities holds this GPU. Container
|
|||||||
(publisher-vetted, `docs/DOCKER.md`): `--network host`, `--init` (so the engine releases the
|
(publisher-vetted, `docs/DOCKER.md`): `--network host`, `--init` (so the engine releases the
|
||||||
cards on SIGTERM), `--security-opt seccomp=unconfined` (the default profile refuses io_uring —
|
cards on SIGTERM), `--security-opt seccomp=unconfined` (the default profile refuses io_uring —
|
||||||
the PLE table's reads — and the NUMA calls the ROCm runtime makes for host memory), `--ulimit
|
the PLE table's reads — and the NUMA calls the ROCm runtime makes for host memory), `--ulimit
|
||||||
memlock=-1:-1`, `/models` ro + `/data` rw + the tune cache at `/opt/radiance/share/radiance/tune`
|
memlock=-1:-1`, `/models` ro + the rad-tune cache at
|
||||||
template as a single-file mount, then `exec podman wait`: the script's lifetime IS the
|
`/opt/radiance/share/radiance/tune` + the template as a single-file mount (there is **no `/data`**
|
||||||
|
any more: it left with the prefix disk tier, and `FRANK_ARCHON_STATE_DIR` is now only the host-side
|
||||||
|
directory that holds `tune/`), then `exec podman wait`: the script's lifetime IS the
|
||||||
container's, so a crash is a nonzero `ExecStart` and the restart policy is real.
|
container's, so a crash is a nonzero `ExecStart` and the restart policy is real.
|
||||||
Unit: `After=network-online.target`, `WantedBy=multi-user.target`, `User=dank`
|
Unit: `After=network-online.target`, `WantedBy=multi-user.target`, `User=dank`
|
||||||
`LimitMEMLOCK=infinity`, `StartLimitIntervalSec=0`, `OOMScoreAdjust=-500` (the last process the
|
`LimitMEMLOCK=infinity`, `StartLimitIntervalSec=0`, `OOMScoreAdjust=-500` (the last process the
|
||||||
@@ -79,27 +88,33 @@ on stop. Boot budget 1800 s (the publisher's compose allows a 15-minute `start_p
|
|||||||
| Knob (env name, `FRANK_ARCHON_` dropped) | Live value |
|
| Knob (env name, `FRANK_ARCHON_` dropped) | Live value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| TP / wire | `TP=2`, `TP_WIRE=wht6` — libr4d one-shot all-reduce, not RCCL; `min-kb` stays the engine default |
|
| TP / wire | `TP=2`, `TP_WIRE=wht6` — libr4d one-shot all-reduce, not RCCL; `min-kb` stays the engine default |
|
||||||
| Slots / window | `MAX_SEQS=3`, `MAX_MODEL_LEN=200000` (native 262,144; longer is refused, never truncated) |
|
| Slots / window | `MAX_SEQS=2`, `MAX_MODEL_LEN=200000` (native 262,144; longer is refused, never truncated) — this pair is the **cache bound**, not a KV allocation |
|
||||||
| KV dtype | `KV_DTYPE=fp8` — the only dtype that funds 3 slots (Laws) |
|
| KV dtype | `KV_DTYPE=fp8` — the only dtype that funds the slot count (Laws) |
|
||||||
| VRAM pair | `VRAM_WEIGHTS_MIB=25600` + `VRAM_KV_MIB=5120` = 30,720 of the 30,969 ceiling; `GPU_HEADROOM_MIB=96` |
|
| VRAM split | **derived at boot** from `GPU_HEADROOM_MIB=96` + the cache bound + `HOST_POOL_MIB`; naming `VRAM_WEIGHTS_MIB`/`VRAM_KV_MIB` is a boot refusal (removed at 1.3.0) |
|
||||||
| Expert tier | `PLACEMENT=expert_tiered`, `HOST_POOL_MIB=44928` **per rank** |
|
| Expert tier | `PLACEMENT=expert_tiered`, `HOST_POOL_MIB=44928` **per rank** |
|
||||||
| PLE | `NGRAM_PLACEMENT=ram` (47.7 GiB pinned; refuses boot on a short memlock) |
|
| PLE | `NGRAM_PLACEMENT=ram` (47.7 GiB pinned; refuses boot on a short memlock) |
|
||||||
| Spec / batch | — |
|
| Spec / batch | `SPEC_TOKENS=2` — MTP depth 2, the `.rad` carries the model's own `mtp.*` head (`mtp.fc_hidden`/`fc_embedding`/`draft_head`); `MAX_BATCHED_TOKENS=2048` (MNBT — 2560 collapses decode, Laws) |
|
||||||
| Prefix cache | `PREFIX_HOST_MIB=8192` + `PREFIX_DISK_MIB=65536` under `STATE_DIR=/home/dank/radiance-state` (mounted rw at `/data`) |
|
| Prefix cache | `PREFIX_CACHE_MIB=8192`, pinned **host tier only** — the 64 GiB disk tier is gone, so an evicted prefix is re-prefilled; `STATE_DIR=/home/dank/radiance-state` now exists only to hold the rad-tune cache |
|
||||||
| Template | `TEMPLATE=scripts/qwen38-froggeric-v22.5.jinja` (28,234 B) via `--override-chat-template` — froggeric's fixed Qwen3.8 template (the repo root's `chat_template.jinja`, vendored); the checkpoint's own template is baked into the `.rad` by rad-convert and this overrides it, and radiance derives the reasoning and tool-call reply format from it |
|
| Template | `TEMPLATE=scripts/qwen38-froggeric-v22.5.jinja` (28,234 B) via `--override-chat-template` — froggeric's fixed Qwen3.8 template (the repo root's `chat_template.jinja`, vendored); the checkpoint's own template is baked into the `.rad` by rad-convert and this overrides it, and radiance derives the reasoning and tool-call reply format from it |
|
||||||
|
| Step sharing | `BUSY_PREFILL_CHUNK=auto` (a shared chunk reserves a row plus the draft window per waiting decoder) · `PREFILL_INTERLEAVE=0` — held at zero until the decode-vs-prefill buckets ask for it |
|
||||||
| TTM gate | `TTM_MIN_MIB=150000`, `TTM_TARGET_MIB=204800` |
|
| TTM gate | `TTM_MIN_MIB=150000`, `TTM_TARGET_MIB=204800` |
|
||||||
| Boot | `RAD_MIN_MIB=110000` deployer floor; the listener binds only after the full load (health ~100 s, budget 1800 s) |
|
| Boot | `RAD_MIN_MIB=110000` deployer floor; the listener binds only after the full load (health ~100 s, budget 1800 s) |
|
||||||
|
|
||||||
Boot ledger for this shape (2026-10-15, pin 1.0.13): pool 5.00 GiB backs **607,740 tokens**
|
Boot ledger for the live shape (2026-10-19 11:10, pin 1.8.1, `MAX_SEQS=2`, `HOST_POOL_MIB=44928`):
|
||||||
(`kv_attn` fp8 page 26.00 KiB, `kv_qsa_bkey` 3.25 KiB), `kv_gdn_state` 54 MiB × 6 slots, slab
|
the resolver prints `elastic 26.73 GiB` → **experts 23.73 GiB (89 %) / kv cache 2.76 GiB (10 %)**,
|
||||||
**17,548 slots** with 4.22 GiB of KV lent to it, sampler **9 rows**, template 28,234 B, vision
|
`vram_weights 27.31 GiB`, `host_pool 44.47 GiB`. `kv_attn` (13 layers, 4-token blocks) pages
|
||||||
live (≤ 4,194,304 px, 16,384 patches/pass).
|
**26.00 KiB** → 91,301 blocks = **365,204 tokens = 1.8 full-length sessions = 91 % of the
|
||||||
|
2 × 200,000 worst case** (the resolver never funds 100 %, see Laws); `mover` lends 2,146 slots
|
||||||
|
(2.54 GiB) out of the cache to the expert slab; sampler **9 rows**, template 28,234 B, vision live
|
||||||
|
(≤ 4,194,304 px, 16,384 patches/pass).
|
||||||
|
|
||||||
The boot **ledger**, not this arithmetic, is the referee: radiance states every budget before
|
The boot **ledger**, not this arithmetic, is the referee: radiance states every budget before
|
||||||
anything is allocated (weights vs KV, the host pool's coverage of the worst case, the PLE pin,
|
anything is allocated (weights vs KV, the host pool's coverage of the worst case, the PLE pin,
|
||||||
the P2P answer per pair) — read it after every resize, and unlike every predecessor there is no
|
the P2P answer per pair) — read it after every resize, and unlike every predecessor there is no
|
||||||
per-boot VRAM planner to second-guess. For comparison, the card's own suggested run line on 32
|
per-boot VRAM planner to second-guess. For comparison, the publisher's own suggested run line on 32
|
||||||
GB cards is `--host-pool-mib 12288 --expert-vs-cache-ratio 0.82` with seqs 8 and spec 3; the
|
GB cards was `--host-pool-mib 12288 --expert-vs-cache-ratio 0.82` with seqs 8 and spec 3 — a
|
||||||
|
1.0.x-era line, and `--expert-vs-cache-ratio` was removed at 1.2.0, so today it is a capacity floor
|
||||||
|
quoted from the card, not a run line this pin accepts; the
|
||||||
shape above is the fleet's measured one. `FRANK_ARCHON_EXTRA_ARGS` is the boot-ladder escape
|
shape above is the fleet's measured one. `FRANK_ARCHON_EXTRA_ARGS` is the boot-ladder escape
|
||||||
hatch (unset → no extra argv; it is expanded unquoted on purpose, so keep multi-flag values as
|
hatch (unset → no extra argv; it is expanded unquoted on purpose, so keep multi-flag values as
|
||||||
one string).
|
one string).
|
||||||
@@ -130,6 +145,7 @@ marked "re-fetch" is gone from `/home/dank/models/`, which today holds only `Sti
|
|||||||
|
|
||||||
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-10-15 → 10-19 | same `qwen3.8-next-flash-fp8-iq4r-moe.rad` (payload unchanged) | radiance **1.0.13**, `sha256:4c3f4845…` | 3 × 200,000 fp8 KV bought with the **explicit** pair `--vram-weights-mib 25600 --vram-kv-mib 5120`; host pool 44,928 → 45,568 MiB a rank; prefix cache 8 GiB host + 64 GiB disk | decode turned to sludge beside any long prompt — step contention, 18.7 ms decode-only steps against 317 ms steps carrying a prefill chunk (Laws) — and the only two knobs that fix it, `--busy-prefill-chunk` and `--prefill-interleave`, exist only after this pin | `FRANK_ARCHON_IMAGE` back to `sha256:4c3f4845…` **plus a trio rewrite**: restore the pair and the host+disk prefix knobs, delete `BUSY_PREFILL_CHUNK`/`PREFILL_INTERLEAVE`. A digest swap alone is a boot refusal (Runtime) |
|
||||||
| 2026-10-08 → 10-15 | in-house `qwen4exp-swift15-w4dn8-i8-hc8m.rad`, 116,416 MiB (Swift1.5 abliterated BF16, GPTQ w4 experts + 24 bf16 protected, KLD 0.0131 / top-1 97.92 %) | radiance 1.0.13, same digest | 3 × 200,000 fp8 KV, slab 25,600 / pool 5,120 — the shape the live file still carries | the engine author's own container is the supported path and his quality ceiling; the finetune's refusal-bending left the endpoint | `.rad` purged 2026-10-17 (trash-staged then emptied, Frank's approval). Re-download `frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700` → `qwen4exp-w4nl64-i8-hc8m.rad`, 122,071,973,888 B, LFS sha256 `09dd9306…`, via the iq4r fetch script's hfget/verify pattern retargeted at that repo; point `MODEL_FILE` at it, redeploy. Re-**convert** is dead: the BF16 source was `rm`'d 2026-10-15 (`kld-ref`, grams, corpora and `/home/dank/logs/swift15_*.log` survive) |
|
| 2026-10-08 → 10-15 | in-house `qwen4exp-swift15-w4dn8-i8-hc8m.rad`, 116,416 MiB (Swift1.5 abliterated BF16, GPTQ w4 experts + 24 bf16 protected, KLD 0.0131 / top-1 97.92 %) | radiance 1.0.13, same digest | 3 × 200,000 fp8 KV, slab 25,600 / pool 5,120 — the shape the live file still carries | the engine author's own container is the supported path and his quality ceiling; the finetune's refusal-bending left the endpoint | `.rad` purged 2026-10-17 (trash-staged then emptied, Frank's approval). Re-download `frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700` → `qwen4exp-w4nl64-i8-hc8m.rad`, 122,071,973,888 B, LFS sha256 `09dd9306…`, via the iq4r fetch script's hfget/verify pattern retargeted at that repo; point `MODEL_FILE` at it, redeploy. Re-**convert** is dead: the BF16 source was `rm`'d 2026-10-15 (`kld-ref`, grams, corpora and `/home/dank/logs/swift15_*.log` survive) |
|
||||||
| 2026-10-05 → 10-08 | `qwen4exp-fp8-native.rad`, 177,109 MiB (173.0 GiB) — native modelopt block-FP8, no requantisation | radiance 1.0.7 line, same digest | — | native FP8 experts (56.4 GiB/rank) ate a 40 GiB pool and left KV at 3,543 MiB = 386,904 tokens, **1.93 sessions** — context economics, not quality; and the host pool could not be trimmed to pay for it (36,864 refused the boot) | rebuild-only: `scripts/download-frank-archon.sh` (official `Qwen/Qwen3.8-Flash-Next-FP8`, 131 shards, 172.8 GB, ungated) + `scripts/build-frank-archon-rad.sh` (no-recipe convert under the descriptor law); raw **and** `.rad` were `rm`'d 2026-10-15 (Frank's call, to free the volume for the iq4r download) |
|
| 2026-10-05 → 10-08 | `qwen4exp-fp8-native.rad`, 177,109 MiB (173.0 GiB) — native modelopt block-FP8, no requantisation | radiance 1.0.7 line, same digest | — | native FP8 experts (56.4 GiB/rank) ate a 40 GiB pool and left KV at 3,543 MiB = 386,904 tokens, **1.93 sessions** — context economics, not quality; and the host pool could not be trimmed to pay for it (36,864 refused the boot) | rebuild-only: `scripts/download-frank-archon.sh` (official `Qwen/Qwen3.8-Flash-Next-FP8`, 131 shards, 172.8 GB, ungated) + `scripts/build-frank-archon-rad.sh` (no-recipe convert under the descriptor law); raw **and** `.rad` were `rm`'d 2026-10-15 (Frank's call, to free the volume for the iq4r download) |
|
||||||
| 2026-10-05 (same day) | `orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8` safetensors, 131 shards | FreeToken PR #587 head `77a7201` (`rocm714_torch214`), image `localhost/frank-primary-freetoken:pr587-77a7201` | — | TP=2 cannot boot on ROCm: the per-rank `pynccl.cu` JIT cannot resolve `cuda_runtime.h` for gfx1201, so the second card idled | git tag `frank-archon-freetoken-pr587-20261005` (trio + `scripts/docker/frank-primary-freetoken.Dockerfile`, which exists only there); image still in rootless storage; payload needs a re-fetch, and that repo 403s until the gate is accepted on the account |
|
| 2026-10-05 (same day) | `orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8` safetensors, 131 shards | FreeToken PR #587 head `77a7201` (`rocm714_torch214`), image `localhost/frank-primary-freetoken:pr587-77a7201` | — | TP=2 cannot boot on ROCm: the per-rank `pynccl.cu` JIT cannot resolve `cuda_runtime.h` for gfx1201, so the second card idled | git tag `frank-archon-freetoken-pr587-20261005` (trio + `scripts/docker/frank-primary-freetoken.Dockerfile`, which exists only there); image still in rootless storage; payload needs a re-fetch, and that repo 403s until the gate is accepted on the account |
|
||||||
@@ -161,7 +177,8 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
2×**, precisely because the bkey plane is dtype-fixed. Secondary saving: a prefix-cache entry
|
2×**, precisely because the bkey plane is dtype-fixed. Secondary saving: a prefix-cache entry
|
||||||
tracks the attention plane (110.50 KiB at bf16 → 58.50 at fp8, per the `kv tier transfer`
|
tracks the attention plane (110.50 KiB at bf16 → 58.50 at fp8, per the `kv tier transfer`
|
||||||
lines), so the same 8 GiB host tier parks ~5.8 sessions of 200k at fp8 where it parked ~2.9 at bf16.
|
lines), so the same 8 GiB host tier parks ~5.8 sessions of 200k at fp8 where it parked ~2.9 at bf16.
|
||||||
- **Pool sizing law.** `pool_mib ≥ tokens × 7.3125 KiB + 110 × seqs + 448` — the dtype-
|
- **Pool sizing law (the explicit-pair era — at 1.8.1 the cache is derived, so the resolver law below
|
||||||
|
owns it).** `pool_mib ≥ tokens × 7.3125 KiB + 110 × seqs + 448` — the dtype-
|
||||||
independent terms are also in the pool: `kv_gdn_state` 54 MiB × 2 copies per sequence (+conv/
|
independent terms are also in the pool: `kv_gdn_state` 54 MiB × 2 copies per sequence (+conv/
|
||||||
tail/ple ≈ 110 MiB a sequence) and the prefix checkpoint region 8 × 55.99 MiB. Deployed: 3 ×
|
tail/ple ≈ 110 MiB a sequence) and the prefix checkpoint region 8 × 55.99 MiB. Deployed: 3 ×
|
||||||
200,000 at fp8 = 4,285 + 330 + 448 = **5,063 MiB**, pool 5,120, which predicts (5120 − 330 −
|
200,000 at fp8 = 4,285 + 330 + 448 = **5,063 MiB**, pool 5,120, which predicts (5120 − 330 −
|
||||||
@@ -174,13 +191,15 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
pool of 3,543 gave 386,904 (1.93 sessions), and the four-slot fp8 boot of 2026-10-13 19:21
|
pool of 3,543 gave 386,904 (1.93 sessions), and the four-slot fp8 boot of 2026-10-13 19:21
|
||||||
backed 843,276. The ledger's `backs N sequences all at 200000 tokens at once` line is the
|
backed 843,276. The ledger's `backs N sequences all at 200000 tokens at once` line is the
|
||||||
referee — a pair this tight is read off the boot, never assumed, and the concurrent probe is
|
referee — a pair this tight is read off the boot, never assumed, and the concurrent probe is
|
||||||
its regression test. Fourth full-height conversation needs 6,601 MiB of pool: refusing it is
|
its regression test. A fourth full-height conversation needs 6,601 MiB of pool: refusing it is
|
||||||
Frank's explicit call, and serving it is a `--vram-kv-mib` change, not a code change.
|
Frank's explicit call, and under that pin serving it was a `--vram-kv-mib` change, not a code change.
|
||||||
|
At 1.8.1 the same decision is `MAX_SEQS` — the resolver buys the cache the demand signal implies.
|
||||||
- **The hard limit is per request.** The engine treats `kv group tokens >= max-model-len` as a
|
- **The hard limit is per request.** The engine treats `kv group tokens >= max-model-len` as a
|
||||||
HARD limit — "one request longer than N tokens is accepted by the API and then refused as too
|
HARD limit — "one request longer than N tokens is accepted by the API and then refused as too
|
||||||
large to ever fit — this is a hard limit, not a backing ratio" — so the pool is sized for
|
large to ever fit — this is a hard limit, not a backing ratio" — so the pool is sized for
|
||||||
`seqs × max-model-len`, not for one sequence. Radiance never truncates, at any size.
|
`seqs × max-model-len`, not for one sequence. Radiance never truncates, at any size.
|
||||||
- **One VRAM budget per card:** `weights + kv ≤ 30,969 MiB`, read off the engine's own budget
|
- **One VRAM budget per card** (the arithmetic below is the pair era's; the ceiling survives, the knobs
|
||||||
|
do not): `weights + kv ≤ 30,969 MiB`, read off the engine's own budget
|
||||||
table (boot 2026-10-13 at pair 24000/5120): card 32,624 MiB, budgets claimed 29,120, and
|
table (boot 2026-10-13 at pair 24000/5120): card 32,624 MiB, budgets claimed 29,120, and
|
||||||
"*3.42 GiB is neither budgeted nor accounted — the driver's context, the activation arena and
|
"*3.42 GiB is neither budgeted nor accounted — the driver's context, the activation arena and
|
||||||
`--gpu-headroom-mib` come out of it; anything past those is throughput on the floor*", with
|
`--gpu-headroom-mib` come out of it; anything past those is throughput on the floor*", with
|
||||||
@@ -188,12 +207,34 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
static region). Reserve above the engine's floor = 2,058 − 209 = 1,849, ceiling 29,120 + 1,849
|
static region). Reserve above the engine's floor = 2,058 − 209 = 1,849, ceiling 29,120 + 1,849
|
||||||
= 30,969; the deployed pair lands 458 MiB above that floor. The 2026-10-11 bound of 29,120
|
= 30,969; the deployed pair lands 458 MiB above that floor. The 2026-10-11 bound of 29,120
|
||||||
("31,856 − 1,710 outside both budgets − keep 1 GiB free") was a hand-picked reserve, not the
|
("31,856 − 1,710 outside both budgets − keep 1 GiB free") was a hand-picked reserve, not the
|
||||||
engine's wall. Boot with the explicit `--vram-weights-mib`/`--vram-kv-mib` pair, never
|
engine's wall. Under that pin, boot with the explicit `--vram-weights-mib`/`--vram-kv-mib` pair,
|
||||||
`--kv-cache-size-mib`; with the disk tier engaged the engine also forces ~9.6 GiB of weight
|
never `--kv-cache-size-mib`; with the disk tier engaged the engine also forced ~9.6 GiB of weight
|
||||||
buffers + arena onto the weights side (~29.6 GiB/card effective).
|
buffers + arena onto the weights side (~29.6 GiB/card effective). **Neither flag exists at 1.8.1:**
|
||||||
|
the same ceiling is enforced by the resolver, and passing the pair is a boot refusal.
|
||||||
|
- **At 1.8.1 the split belongs to the resolver, and it funds ~90 % of the worst case, never 100 %.**
|
||||||
|
The engine takes `--gpu-headroom-mib`, the cache bound (`--max-model-len` × `--max-num-seqs`) and
|
||||||
|
`--host-pool-mib`, prints `claimable → elastic → experts / kv cache` before allocating anything, and
|
||||||
|
sizes the expert plane from the model's own weight footprint plus slack — not from a number typed by
|
||||||
|
hand. Three boots of evidence:
|
||||||
|
|
||||||
|
| shape | demand | cache given | backing |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 3 × 200,000, host pool 44,928 | 600,000 tok | 4.20 GiB | 93 % (2.8 sessions) |
|
||||||
|
| 3 × 200,000, host pool 45,568 | 600,000 tok | **4.20 GiB — byte-identical** | 93 % |
|
||||||
|
| 2 × 200,000, host pool 44,928 (live) | 400,000 tok | 2.76 GiB = 365,204 tok | 91 % (1.8 sessions) |
|
||||||
|
|
||||||
|
So `--max-num-seqs` is a **demand signal** for the resolver and an admission cap for the scheduler —
|
||||||
|
never a KV allocation — and no slot count fully funds the worst case. Growing host RAM by less than a
|
||||||
|
slab step (640 MiB) moved nothing: the expert plane's claim decides what is left for KV, so cache is
|
||||||
|
grown through slots × window or the dtype, and experts are grown by not leaving elastic pool idle.
|
||||||
|
`--host-pool-mib` is still load-bearing the other way — it is where the expert rows the cards cannot
|
||||||
|
hold get pinned, and under-sizing it has always been a boot failure (36,864 refused a boot in the fp8
|
||||||
|
era). The two-slot boot's own behavioural check: three concurrent conversations all answer `200` —
|
||||||
|
the third **queues** (`queue_cap: 16`), it is not refused; the 400 class is a single request over
|
||||||
|
`--max-model-len`, or three concurrent full-height conversations against a 1.8-session cache.
|
||||||
- **Idle KV VRAM is the expert cache's spillover — the price is paid twice.** `mover: 2275
|
- **Idle KV VRAM is the expert cache's spillover — the price is paid twice.** `mover: 2275
|
||||||
slots can be lent out of the KV cache (2.69 GiB)`, `arena: lends 740 MiB while steps are <=
|
slots can be lent out of the KV cache (2.69 GiB)`, `arena: lends 740 MiB while steps are <=
|
||||||
256 tokens`; the lend grows with the pool (2.69 GiB from 3.46, 6.08 from 6.912, 4.22 today)
|
256 tokens`; the lend grows with the pool (2.69 GiB from 3.46, 6.08 from 6.912, 4.22 in the pair era)
|
||||||
so a bigger KV pool steals weight residency from the slab AND from the lendable cache. Priors
|
so a bigger KV pool steals weight residency from the slab AND from the lendable cache. Priors
|
||||||
on this card/model: stealing 2.14 GiB of weights to buy `MAX_MODEL_LEN 262144` cost 2.5 %
|
on this card/model: stealing 2.14 GiB of weights to buy `MAX_MODEL_LEN 262144` cost 2.5 %
|
||||||
decode; the other direction (`--expert-vs-cache-ratio` 0.82 → 0.84, +2.5 GiB of weights) cost
|
decode; the other direction (`--expert-vs-cache-ratio` 0.82 → 0.84, +2.5 GiB of weights) cost
|
||||||
@@ -239,8 +280,8 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
- **MNBT 2048.** 2560 collapses decode to 2.6-7.7 t/s (MTP resonance); the w4 file tier streams
|
- **MNBT 2048.** 2560 collapses decode to 2.6-7.7 t/s (MTP resonance); the w4 file tier streams
|
||||||
in place only at MNBT ≤ 2048 and the activation arena scales with the chunk (32768 → 33.3
|
in place only at MNBT ≤ 2048 and the activation arena scales with the chunk (32768 → 33.3
|
||||||
GiB, refused).
|
GiB, refused).
|
||||||
- **Decode pays for the chunk it shares a step with.** Census of the live engine
|
- **Decode pays for the chunk it shares a step with.** Census of the live engine taken at the
|
||||||
(radiance 1.0.13, `/stats` + `/metrics`, cumulative over the 2026-10-15 boot):
|
3-slot shape on 1.0.13 (`/stats` + `/metrics`, cumulative over the 2026-10-15 boot):
|
||||||
1,435,585 decode-only steps at **18.7 ms** each against 127,991 prefill+mixed steps
|
1,435,585 decode-only steps at **18.7 ms** each against 127,991 prefill+mixed steps
|
||||||
at **317 ms** — 60 % of engine wall-clock spent inside 8 % of the steps, because the
|
at **317 ms** — 60 % of engine wall-clock spent inside 8 % of the steps, because the
|
||||||
chunk is `min(remaining, checkpoint interval 2048, MNBT 2048)` = 2048 rows and every
|
chunk is `min(remaining, checkpoint interval 2048, MNBT 2048)` = 2048 rows and every
|
||||||
@@ -252,28 +293,33 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
|
|||||||
busy-prefill window sits at the idle baseline (58-64 vs 77-85 t/s, the residual
|
busy-prefill window sits at the idle baseline (58-64 vs 77-85 t/s, the residual
|
||||||
confounded by co-scheduling), so no lingering expert-residency penalty at 2-minute
|
confounded by co-scheduling), so no lingering expert-residency penalty at 2-minute
|
||||||
resolution, and `preemptions: 0` rules out preempt-and-re-prefill. Method:
|
resolution, and `preemptions: 0` rules out preempt-and-re-prefill. Method:
|
||||||
[[LLM Performance Diagnosis]] §9.
|
[[LLM Performance Diagnosis]] §9. The law is shape-independent; the fix lives at 1.8.1 (next law),
|
||||||
- **The knobs for it are not in this pin.** `--busy-prefill-chunk` and
|
and nobody has re-run the census since the migration — so the fix is ordered, **not verified**.
|
||||||
`--prefill-interleave` (radiance GUIDE §4.5 — a shared chunk's cap, and N decode-only
|
- **The knobs for it arrived with 1.8.1, and only one of them is on.** `--busy-prefill-chunk` caps the
|
||||||
steps after each chunk) are absent from 1.0.13: proven on the running container
|
shared chunk so a step with waiting decoders takes a smaller piece (live: `auto`, the engine picks;
|
||||||
(`podman exec frank-archon radiance --help` prints no `prefill`/`chunk` flag), and the
|
the explicit value is 512), and `--prefill-interleave N` inserts N decode-only steps after a chunk
|
||||||
2026-10-18 source tree carries both. The only crank this pin has is MNBT, and that is
|
step taken beside decoders. `interleave` is deliberately **0** — the env comment holds it there until
|
||||||
an arm and not an edit: the vendor prices a 1024 chunk at −19 % prefill to halve the
|
the decode-vs-prefill buckets ask for it, and those buckets have not been read since the migration.
|
||||||
stall, and this page's own MNBT law already knows 2560 collapses decode (MTP
|
The vendor prices a 1024 chunk at about a fifth off prefill throughput to halve the stall; that is a
|
||||||
resonance). An image bump is gated by the 1.2.0 numerics dead end below, so any try
|
knob for choosing tomorrow's chunk, not a measurement of this shape. MNBT remains an arm and not an
|
||||||
re-scores draft acceptance before it is trusted.
|
edit: 2560 collapses decode through MTP resonance. Any chunk change re-scores draft acceptance before
|
||||||
- **Slots cost KV and rows, never speed.** `--max-num-seqs` carries no radiance constraint: it
|
it is trusted.
|
||||||
adds 110 MiB of recurrent state per slot and declares the fused TP all-reduce over `seqs ×
|
- **Slots cost KV, rows, and window — never speed.** `--max-num-seqs` carries no radiance constraint: it
|
||||||
(1 + spec)` rows, and `wht6` is bit-exact up to 25 rows (single-sequence decode and any step
|
adds ≈110 MiB of recurrent state per slot (the live `kv_full_backing` arithmetic prices them exactly),
|
||||||
of ≤ 25 rows is exact; only busier steps are lossy) — 3 × 3 = 9 today. Measured 2026-10-06:
|
declares the fused TP all-reduce over `seqs × (1 + spec)` rows, and `wht6` is bit-exact up to 25 rows
|
||||||
seqs 2 and seqs 3 are statistically tied (30.9-33.1 t/s, inside the ±3 % floor). The "seqs 2
|
(single-sequence decode and any step of ≤ 25 rows is exact; only busier steps are lossy) — 2 × 3 = 6
|
||||||
cannot boot" law belonged to the retired tcclaviger fork's `torch.compile` shape
|
today. Measured 2026-10-06: seqs 2 and seqs 3 are statistically tied (30.9-33.1 t/s, inside the ±3 %
|
||||||
specialization, and radiance booted seqs 2 on its first fp8-native start.
|
floor). Measured 2026-10-19 at 1.8.1: the third slot's real price is **KV, not rows** — dropping it
|
||||||
**Same era, separate law: a dequantize-to-compute path bills host RAM twice.** On the
|
moved the resolver's cache from 4.20 GiB to 2.76 GiB and the backing from 2.8 to 1.8 sessions, while
|
||||||
EMULATION backend an offload request of **118 GiB** landed as **~238 GiB** of real host RAM —
|
the ≈4 GiB of host RAM it freed came back unbudgeted, landing in elastic pool that turned into a bigger
|
||||||
the dequantized copy rides on top of the pinned weights, and the doubling is invisible in the
|
mover lend instead of cache (resolver law above). The old "seqs 2 cannot boot" law belonged to the
|
||||||
weight count. Read the engine log for `Using 'EMULATION' Mxfp4 MoE backend` before concluding
|
retired tcclaviger fork's `torch.compile` shape specialization, and radiance booted seqs 2 on its first
|
||||||
"doesn't fit"; the container-side procedure and the kernel gate are .
|
fp8-native start.
|
||||||
|
- **A dequantize-to-compute path bills host RAM twice.** On the EMULATION backend an offload request
|
||||||
|
of **118 GiB** landed as **~238 GiB** of real host RAM — the dequantized copy rides on top of the
|
||||||
|
pinned weights, and the doubling is invisible in the weight count. Read the engine log for
|
||||||
|
`Using 'EMULATION' Mxfp4 MoE backend` before concluding "doesn't fit"; a page that says a payload
|
||||||
|
"does not fit" without that line has not read the runtime.
|
||||||
- **One encoding per expert table** (`rad_arch_declare`): a whole expert kept bf16 is the only
|
- **One encoding per expert table** (`rad_arch_declare`): a whole expert kept bf16 is the only
|
||||||
thing legal beside the table, so a split tier (down=fp8, gate_up=w4) is unservable.
|
thing legal beside the table, so a split tier (down=fp8, gate_up=w4) is unservable.
|
||||||
- **Descriptor-selection law.** The plugin is picked by the exact `(arch_id, quant descriptor)`
|
- **Descriptor-selection law.** The plugin is picked by the exact `(arch_id, quant descriptor)`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
type: reference
|
type: reference
|
||||||
title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 10777), Ternary-Bonsai-2-9B PQ2_0-MTP, 3 × 200704, f16 K + q8_0 V, MTP armed"
|
title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 10777), Ternary-Bonsai-2-9B PQ2_0-MTP + refusal adapter, 3 × 200704, f16 K + q8_0 V, MTP armed"
|
||||||
description: "The RTX 3090 endpoint: live shape exactly as the env ships it, every era with its rollback path, and its card laws."
|
description: "The RTX 3090 endpoint: live shape exactly as the env ships it, every era with its rollback path, and its card laws."
|
||||||
tags:
|
tags:
|
||||||
- llm
|
- llm
|
||||||
@@ -10,10 +10,10 @@ tags:
|
|||||||
- gguf
|
- gguf
|
||||||
- rtx3090
|
- rtx3090
|
||||||
- endpoint
|
- endpoint
|
||||||
timestamp: 2026-10-18
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# frank-zealot — llama.cpp (PrismML) Bonsai-2-9B PQ2_0-MTP, 3 × 200,704, f16 K + q8_0 V, MTP armed
|
# frank-zealot — llama.cpp (PrismML) Bonsai-2-9B PQ2_0-MTP + refusal adapter, 3 × 200,704, f16 K + q8_0 V, MTP armed
|
||||||
|
|
||||||
Dankclaw's RTX 3090 endpoint — `http://192.168.0.143:7771`, wire id `frank-zealot`, the fleet's **compaction hub**
|
Dankclaw's RTX 3090 endpoint — `http://192.168.0.143:7771`, wire id `frank-zealot`, the fleet's **compaction hub**
|
||||||
(three conversations at 200,704 each, the window the other two endpoints also cap at). One page
|
(three conversations at 200,704 each, the window the other two endpoints also cap at). One page
|
||||||
@@ -29,11 +29,12 @@ is the bug.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Checkpoint | `/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.gguf` — 2,675,410,720 B, LFS sha256 `c79fcdfaf6b7bf47…` |
|
| Checkpoint | `/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.gguf` — 2,675,410,720 B, LFS sha256 `c79fcdfaf6b7bf47…` |
|
||||||
| Projector | `…/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf` — 624,229,664 B, sha256 `e0cb6f6988cea323…` — **Q8_0 is mandatory**, not a preference (see Contract) |
|
| Projector | `…/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf` — 624,229,664 B, sha256 `e0cb6f6988cea323…` — **Q8_0 is mandatory**, not a preference (see Contract) |
|
||||||
| Fetch | `scripts/download-frank-zealot-bonsai9.sh [DEST]` (`--verify-only`; `-f`, byte count, LFS sha256 per file) |
|
| Adapter | `…/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf` — **3,155,424 B**, sha256 `861b9c099fb5cd81…` — a rank-1 refusal-direction LoRA (`adapter.type lora`, 128 tensors, every transformer layer, embedding untouched), applied at load by `--lora-scaled <file>:1`. Its metadata names `Ternary-Bonsai-2-9B-PTQ1_0.gguf` (`3561df7a…`) as the pack it was fitted against — **not this packing, and not on this host**; see Laws |
|
||||||
|
| Fetch | `scripts/download-frank-zealot-bonsai9.sh [DEST]` (payload + projector) · `scripts/download-frank-zealot-uncensored-lora.sh [DEST]` (the adapter) — both `--verify-only`-able, `-f`, byte count and sha256 per file |
|
||||||
| Runtime | `/home/dank/GitPackages/llama-prism-b10777/llama-server` — PrismML **fork**, `0.2.0-dev (build 10777, commit e3de71522)`; the only loader for this payload |
|
| Runtime | `/home/dank/GitPackages/llama-prism-b10777/llama-server` — PrismML **fork**, `0.2.0-dev (build 10777, commit e3de71522)`; the only loader for this payload |
|
||||||
| Trio | `scripts/frank-zealot.service` (root, relinked, `User=dank`, `Restart=on-failure`, `RestartSec=10`, `LimitMEMLOCK=infinity`, `TimeoutStopSec=180`) · `scripts/frank-zealot.env` · `scripts/serve-frank-zealot.sh` |
|
| Trio | `scripts/frank-zealot.service` (root, relinked, `User=dank`, `Restart=on-failure`, `RestartSec=10`, `LimitMEMLOCK=infinity`, `TimeoutStopSec=180`) · `scripts/frank-zealot.env` · `scripts/serve-frank-zealot.sh` |
|
||||||
| Address / alias | `BIND_ADDR=0.0.0.0`, `LISTEN_PORT=7771`, `ALIAS=frank-zealot` (a flag, not a fact) |
|
| Address / alias | `BIND_ADDR=0.0.0.0`, `LISTEN_PORT=7771`, `ALIAS=frank-zealot` (a flag, not a fact) |
|
||||||
| Flags | `--ctx-size 602112 --parallel 3` ⇒ 200,704 per slot · `--cache-type-k f16 --cache-type-v q8_0` (asymmetric, **14,406.00 MiB**) · `--flash-attn on` · `--batch-size 6144 --ubatch-size 1024` · `--n-gpu-layers 99` · `--jinja --reasoning auto --reasoning-format deepseek` · `--metrics` · `--mmproj` · `--log-verbosity 4` · `--presence-penalty 1.0` · **`--spec-type draft-mtp`** with `--spec-draft-n-max 2 --spec-draft-n-min 0 --spec-draft-p-min 0` |
|
| Flags | `--ctx-size 602112 --parallel 3` ⇒ 200,704 per slot · `--cache-type-k f16 --cache-type-v q8_0` (asymmetric, **14,406.00 MiB**) · `--flash-attn on` · `--batch-size 6144 --ubatch-size 1024` · `--n-gpu-layers 99` · `--jinja --reasoning auto --reasoning-format deepseek` · `--metrics` · `--mmproj` · **`--lora-scaled Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf:1`** · `--log-verbosity 4` · `--presence-penalty 1.0` · **`--spec-type draft-mtp`** with `--spec-draft-n-max 2 --spec-draft-n-min 0 --spec-draft-p-min 0` |
|
||||||
|
|
||||||
**Why this shape.** `general.architecture qwen35`: 33 blocks (32 trunk + `blk.32` = the MTP block)
|
**Why this shape.** `general.architecture qwen35`: 33 blocks (32 trunk + `blk.32` = the MTP block)
|
||||||
`full_attention_interval 4` ⇒ 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, `nextn_predict_layers 1`
|
`full_attention_interval 4` ⇒ 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, `nextn_predict_layers 1`
|
||||||
@@ -57,6 +58,28 @@ and `UBATCH_SIZE=2048` beside an armed drafter is still the shape that halved de
|
|||||||
`--spec-draft-model` — the draft rides the main model's weights, so a draft file would load the trunk twice.
|
`--spec-draft-model` — the draft rides the main model's weights, so a draft file would load the trunk twice.
|
||||||
`SPEC_TYPE=none` is the rollback and strips the whole set.
|
`SPEC_TYPE=none` is the rollback and strips the whole set.
|
||||||
|
|
||||||
|
**The adapter is behaviour, not shape.** It folds 3.2 MB of rank-1 tensors into the trunk at
|
||||||
|
load, so it moves nothing in the budget equation above — no KV, no slot, no window. What it
|
||||||
|
changes is what the endpoint answers, and the only instrument for that is a refusal probe:
|
||||||
|
the same two prompts the plain pack answered `I cannot provide…` (captured on the live
|
||||||
|
endpoint before the swap, `temperature 0.1`), re-asked after it. Budget **≥ 2 048 tokens** for
|
||||||
|
that probe on this payload — the thinking channel swallows a 600-token request whole and
|
||||||
|
`content` comes back empty with `finish_reason: length`, which reads as a refusal that never
|
||||||
|
happened. The adapter's own card table is **not** evidence here: it was measured on the PTQ1_0
|
||||||
|
packing. Rollback is `LORA_FILE=` (empty) in the env and one redeploy — no rebuild, no payload
|
||||||
|
move; `LORA_SCALE=0` is the same result through the flag.
|
||||||
|
|
||||||
|
**Measured on the cutover boot (2026-10-19, `02d04bc`).** Before: both probe prompts
|
||||||
|
opened `I cannot provide…`. After: neither carries a refusal marker — one ran to its
|
||||||
|
2 048-token cap, the other finished at 1 930 — so the direction is out. It still
|
||||||
|
volunteers a `⚠️ Important Disclaimer` heading before answering, which is style, not the
|
||||||
|
circuit: the marker sentence is gone, and a leading hedge is not an escape hatch. Chat,
|
||||||
|
tools (`tool_calls` with valid JSON) and vision ("Red", 281 prompt tokens) answered from
|
||||||
|
the same boot, and `DropInPaths` came back empty. Read the ledger, not this paragraph,
|
||||||
|
for provenance: the fork prints the adapter's `prism.abliterate.*` keys — including the
|
||||||
|
`base_gguf = Ternary-Bonsai-2-9B-PTQ1_0.gguf` line — at every start, so the packing
|
||||||
|
mismatch is visible in the journal to anyone who boots it.
|
||||||
|
|
||||||
**Harness contract** (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw-3090)`, id `frank-zealot`):
|
**Harness contract** (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw-3090)`, id `frank-zealot`):
|
||||||
`contextWindow: 200704` — **the per-slot window**, because llama.cpp divides `--ctx-size` across slots and answers a
|
`contextWindow: 200704` — **the per-slot window**, because llama.cpp divides `--ctx-size` across slots and answers a
|
||||||
longer prompt with a 400, never a truncation — plus `maxTokens: 16384`, `input: [text, image]`
|
longer prompt with a 400, never a truncation — plus `maxTokens: 16384`, `input: [text, image]`
|
||||||
@@ -113,6 +136,7 @@ with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY c
|
|||||||
|
|
||||||
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-10-19 → | the same 9B MTP file and projector, **plus** `Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf` (3,155,424 B `861b9c09…`) applied by `--lora-scaled …:1` | PrismML fork 10777 (its verified build) | — (shape unchanged; the adapter is behaviour) | Frank ordered the refusal direction removed on this endpoint | one env line — `LORA_FILE=` (or `LORA_SCALE=0`) — and redeploy; the plain pack is the same file it already loads |
|
||||||
| 2026-10-18 21:06 → 2026-10-19 | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered the third conversation back AND the drafter armed, and exact V funds neither: it bills 18,816 MiB at the pool that pays for the third slot, leaving nothing for the draft's 2,352 | one line each — `CTX_SIZE=524288 PARALLEL=2 CACHE_TYPE_V=f16 BATCH_SIZE=4096 UBATCH_SIZE=2048 SPEC_TYPE=none` — and redeploy; no payload move |
|
| 2026-10-18 21:06 → 2026-10-19 | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered the third conversation back AND the drafter armed, and exact V funds neither: it bills 18,816 MiB at the pool that pays for the third slot, leaving nothing for the draft's 2,352 | one line each — `CTX_SIZE=524288 PARALLEL=2 CACHE_TYPE_V=f16 BATCH_SIZE=4096 UBATCH_SIZE=2048 SPEC_TYPE=none` — and redeploy; no payload move |
|
||||||
| 2026-10-18 13:19 → same evening (the boot `68afc6d` replaced) | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered unquantized KV, and that pool cannot survive it: 24,576 MiB of f16 cache > 24,123 addressable | one env line — `CTX_SIZE=786432 PARALLEL=3 CACHE_TYPE_K/V=q8_0` — and redeploy; no payload move |
|
| 2026-10-18 13:19 → same evening (the boot `68afc6d` replaced) | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered unquantized KV, and that pool cannot survive it: 24,576 MiB of f16 cache > 24,123 addressable | one env line — `CTX_SIZE=786432 PARALLEL=3 CACHE_TYPE_K/V=q8_0` — and redeploy; no payload move |
|
||||||
| 2026-10-18 03:14 → 13:19 (arm under the unit, then the leftover drop-in) | `Bonsai2-PTQ1_0/Ternary-Bonsai-2-27B-PTQ1_0.gguf` 5,946,648,928 B + hardlinked Q8 mmproj | PrismML fork 10777 | arm: 2 × 200,704, batch/ubatch 3072/1536. The leftover `frank-zealot.service.d/ptq1.conf` then made the 13:16 cutover deploy boot THIS payload at 3 × 133,888 (`new slot, n_ctx = 133888` x3, journal read 2026-10-19) until the 13:19 boot | — | `/home/dank/models/Bonsai2-PTQ1_0/` + `scripts/download-frank-zealot-bonsai2-ptq1.sh` |
|
| 2026-10-18 03:14 → 13:19 (arm under the unit, then the leftover drop-in) | `Bonsai2-PTQ1_0/Ternary-Bonsai-2-27B-PTQ1_0.gguf` 5,946,648,928 B + hardlinked Q8 mmproj | PrismML fork 10777 | arm: 2 × 200,704, batch/ubatch 3072/1536. The leftover `frank-zealot.service.d/ptq1.conf` then made the 13:16 cutover deploy boot THIS payload at 3 × 133,888 (`new slot, n_ctx = 133888` x3, journal read 2026-10-19) until the 13:19 boot | — | `/home/dank/models/Bonsai2-PTQ1_0/` + `scripts/download-frank-zealot-bonsai2-ptq1.sh` |
|
||||||
@@ -169,13 +193,15 @@ with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY c
|
|||||||
- MTP is a property of the **file**: the plain PQ2_0 sibling could not draft at any flag value; only the `-MTP` export carries `blk.N.nextn.*`.
|
- MTP is a property of the **file**: the plain PQ2_0 sibling could not draft at any flag value; only the `-MTP` export carries `blk.N.nextn.*`.
|
||||||
- **The format is the runtime lock.** PQ2_0 is `general.file_type 141` over 202 tensors of custom GGML type **142** plus `prism.hadamard.*` metadata: stock llama.cpp refuses the type, and a loader ignoring the Hadamard transform would dequant **wrong, silently** (same lock class as ROCmFPX). Build ≥ b10776 is additionally the floor that pairs MTP with a quantized V cache.
|
- **The format is the runtime lock.** PQ2_0 is `general.file_type 141` over 202 tensors of custom GGML type **142** plus `prism.hadamard.*` metadata: stock llama.cpp refuses the type, and a loader ignoring the Hadamard transform would dequant **wrong, silently** (same lock class as ROCmFPX). Build ≥ b10776 is additionally the floor that pairs MTP with a quantized V cache.
|
||||||
- Check arch support in the source tree: `grep -c` on the packed binary returns 0 for every arch, including the one it serves.
|
- Check arch support in the source tree: `grep -c` on the packed binary returns 0 for every arch, including the one it serves.
|
||||||
- **Base alignment.** Bonsai-2 is the censored base lineage. `/home/dank/models/Bonsai2-CRACK/` matches the **27B** geometry (a one-line `MODEL_FILE` swap there, identical tensor census); no dealigned 9B exists, so for this payload the refusal escape hatch is a fetch, not a revert.
|
- **Base alignment.** Bonsai-2 is the censored base lineage. `/home/dank/models/Bonsai2-CRACK/` matches the **27B** geometry (a one-line `MODEL_FILE` swap there, identical tensor census); no dealigned 9B exists, so for this payload the refusal escape hatch is a fetch, not a revert — which is exactly what was taken on 2026-10-19: the rank-1 adapter in the Adapter row above, behind `LORA_FILE`.
|
||||||
- **A stale bench drop-in hijacks the production boot and no health check sees it.** An `Environment=FRANK_ZEALOT_ENV_FILE=/tmp/<arm>.env` in `frank-zealot.service.d/` booted a 27B PTQ1_0 payload while `/health` answered 200, `/v1/models` echoed the alias flag and the e2e gate passed 33/33 — none of them read the payload.
|
- **A stale bench drop-in hijacks the production boot and no health check sees it.** An `Environment=FRANK_ZEALOT_ENV_FILE=/tmp/<arm>.env` in `frank-zealot.service.d/` booted a 27B PTQ1_0 payload while `/health` answered 200, `/v1/models` echoed the alias flag and the e2e gate passed 33/33 — none of them read the payload.
|
||||||
- After any bench window: `systemctl show -p DropInPaths` → `tr '\0' ' ' </proc/$pid/cmdline` → ledger, in that order; drop-ins retire to `~/.AGENTTRASH/<date>-<topic>/`, and payload decisions land on this page the moment they are made, or an approved arm becomes indistinguishable from a rogue one a day later.
|
- After any bench window: `systemctl show -p DropInPaths` → `tr '\0' ' ' </proc/$pid/cmdline` → ledger, in that order; drop-ins retire to `~/.AGENTTRASH/<date>-<topic>/`, and payload decisions land on this page the moment they are made, or an approved arm becomes indistinguishable from a rogue one a day later.
|
||||||
- **An arm env must use the serve script's own variable names** (`CACHE_TYPE_K`/`CACHE_TYPE_V`, `BIND_ADDR`, `NGL`): an invented `KV_CACHE_TYPE` is swallowed by `: "${CACHE_TYPE_K:=f16}"` and the arm boots f16 — an OOM that reads like a model fault.
|
- **An arm env must use the serve script's own variable names** (`CACHE_TYPE_K`/`CACHE_TYPE_V`, `BIND_ADDR`, `NGL`): an invented `KV_CACHE_TYPE` is swallowed by `: "${CACHE_TYPE_K:=f16}"` and the arm boots f16 — an OOM that reads like a model fault.
|
||||||
- **Stop, shebang, GPU node.** llama.cpp rides out the default stop timeout (`TimeoutStopSec=180` is on the unit for that reason) and a hand-booted arm keeps answering 7771 behind a `failed` unit — kill it **by PID**, never `pkill -f llama-server`.
|
- **Stop, shebang, GPU node.** llama.cpp rides out the default stop timeout (`TimeoutStopSec=180` is on the unit for that reason) and a hand-booted arm keeps answering 7771 behind a `failed` unit — kill it **by PID**, never `pkill -f llama-server`.
|
||||||
- `serve-frank-zealot.sh` is a direct `ExecStart`, so a missing shebang is a `203/EXEC` boot loop at `Mem peak: 1.9M` with the GPU untouched: `head -1` is a deploy check.
|
- `serve-frank-zealot.sh` is a direct `ExecStart`, so a missing shebang is a `203/EXEC` boot loop at `Mem peak: 1.9M` with the GPU untouched: `head -1` is a deploy check.
|
||||||
- `No CUDA GPUs are available` ⇒ `nvidia-modprobe -u -c=0`; KV spilled into unified memory halves decode silently, so re-read `nvidia-smi` after any shape change.
|
- `No CUDA GPUs are available` ⇒ `nvidia-modprobe -u -c=0`; KV spilled into unified memory halves decode silently, so re-read `nvidia-smi` after any shape change.
|
||||||
|
- **An adapter's provenance is metadata, not a gate.** Its GGUF carries `prism.abliterate.base_gguf = Ternary-Bonsai-2-9B-PTQ1_0.gguf` and that pack's sha256; the endpoint applies the file to the PQ2_0+MTP packing anyway, because the loader matches **tensor names, not hashes**. So an adapter's effect is only ever proven behaviourally (a refusal probe against the live endpoint), never by the card, and an adapter whose rank landed on `token_embd` would be a crash rather than a degradation — the publisher leaves the embedding untouched for exactly that reason.
|
||||||
|
- **The fork pin now has a third reason.** The payload's custom type 142, the b10776 floor that pairs MTP with a quantized V, and now this: older PrismML builds (e.g. `prism-b10709`) refuse the adapter outright with `unsupported prism.hadamard.version: 2`, so a runtime that can load the pack can still be unable to carry the adapter.
|
||||||
|
|
||||||
## Contract
|
## Contract
|
||||||
|
|
||||||
@@ -210,12 +236,12 @@ with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY c
|
|||||||
boot ledger and the KV derivation all live on this page now; the header used to hold them, and two of its
|
boot ledger and the KV derivation all live on this page now; the header used to hold them, and two of its
|
||||||
rung figures were wrong (`CACHE_TYPE_V=q8_0` and `CTX_SIZE=401408` both free **3,840 MiB** at the 524,288 pool,
|
rung figures were wrong (`CACHE_TYPE_V=q8_0` and `CTX_SIZE=401408` both free **3,840 MiB** at the 524,288 pool,
|
||||||
not 8,192 and not ~6.7 GiB — the Fallback rungs table above is the corrected statement).
|
not 8,192 and not ~6.7 GiB — the Fallback rungs table above is the corrected statement).
|
||||||
- `scripts/serve-frank-zealot.sh` — the exec line and its rationale comments; honors `FRANK_ZEALOT_ENV_FILE` for arm boots; exits 1 naming a missing binary, GGUF or projector rather than half-loading (a restart policy would turn an unfinished download into a crash loop that reads like a runtime fault).
|
- `scripts/serve-frank-zealot.sh` — the exec line and its rationale comments; honors `FRANK_ZEALOT_ENV_FILE` for arm boots; exits 1 naming a missing binary, GGUF, projector or adapter rather than half-loading (a restart policy would turn an unfinished download into a crash loop that reads like a runtime fault).
|
||||||
- `scripts/frank-zealot.service` — root unit, relinked into `/etc/systemd/system/`.
|
- `scripts/frank-zealot.service` — root unit, relinked into `/etc/systemd/system/`.
|
||||||
- `scripts/download-frank-zealot-bonsai9.sh` (active) · `…-bonsai2.sh` · `…-bonsai2-ptq1.sh` ·
|
- `scripts/download-frank-zealot-bonsai9.sh` (active payload + projector) · `scripts/download-frank-zealot-uncensored-lora.sh` (the refusal adapter; its header is the provenance record) · `…-bonsai2.sh` · `…-bonsai2-ptq1.sh` ·
|
||||||
`scripts/download-frank-zealot.sh` (master-era GGUFs) · `old/download-frank-zealot-sglang-int8.sh` (SGLang).
|
`scripts/download-frank-zealot.sh` (master-era GGUFs) · `old/download-frank-zealot-sglang-int8.sh` (SGLang).
|
||||||
- `tests/frank_zealot_unit.bats` — stub-argv seam plus the VRAM fit gate.
|
- `tests/frank_zealot_unit.bats` — stub-argv seam plus the VRAM fit gate.
|
||||||
- `scripts/deploy-remote.sh` — `deploy_llm_zealot` (payload-path gates, MainPID-hardened health);
|
- `scripts/deploy-remote.sh` — `deploy_llm_zealot` (payload-path gates including the adapter, MainPID-hardened health);
|
||||||
- `shared/prometheus/prometheus.yml`, `shared/prometheus/rules/llm-fleet.yml`
|
- `shared/prometheus/prometheus.yml`, `shared/prometheus/rules/llm-fleet.yml`
|
||||||
`shared/grafana/provisioning/dashboards/llm-fleet.json` — the scrape, the alert, the dashboard.
|
`shared/grafana/provisioning/dashboards/llm-fleet.json` — the scrape, the alert, the dashboard.
|
||||||
- Live state, not repo files: the OpenWebUI picker row (back up `webui.db` before re-keying) and `~/.omp/agent/models.yml`.
|
- Live state, not repo files: the OpenWebUI picker row (back up `webui.db` before re-keying) and `~/.omp/agent/models.yml`.
|
||||||
@@ -23,7 +23,7 @@ is a locator: one row, one clause, no numbers.
|
|||||||
| [[ComfyUI Qwen-Image Workflows]] | Proven Qwen-Image text-to-image workflows on Dankclaw: the recipes, the API call shape, and which models to add next. |
|
| [[ComfyUI Qwen-Image Workflows]] | Proven Qwen-Image text-to-image workflows on Dankclaw: the recipes, the API call shape, and which models to add next. |
|
||||||
| [[Dankclaw Stack]] | Service inventory and routing overview for the Dankclaw AI, development, and observability host. |
|
| [[Dankclaw Stack]] | Service inventory and routing overview for the Dankclaw AI, development, and observability host. |
|
||||||
| [[Frank TTS]] | The fleet's cloned-voice speech endpoint and the one voice surface consumers address; wired as the harness speech role. |
|
| [[Frank TTS]] | The fleet's cloned-voice speech endpoint and the one voice surface consumers address; wired as the harness speech role. |
|
||||||
| [[Frank TTS Engine]] | The engine behind the fleet's speech endpoint: its runtime, private bridge, and the surface it publishes. |
|
| [[Frank TTS Engine]] | The engine behind the fleet's speech endpoint: its Vulkan build on the iGPU, its private bridge, and what it publishes. |
|
||||||
| [[Gitea]] | Gitea deployment on Dankclaw with its web UI reverse-proxied by the canonical Caddy service. |
|
| [[Gitea]] | Gitea deployment on Dankclaw with its web UI reverse-proxied by the canonical Caddy service. |
|
||||||
| [[Gluetun]] | Gluetun is a lightweight VPN client container. |
|
| [[Gluetun]] | Gluetun is a lightweight VPN client container. |
|
||||||
| [[Hermes]] | Hermes is the self-improving AI agent from Nous Research. |
|
| [[Hermes]] | Hermes is the self-improving AI agent from Nous Research. |
|
||||||
|
|||||||
@@ -331,13 +331,40 @@ FAKE
|
|||||||
[ "$(argv --port)" = "8880" ]
|
[ "$(argv --port)" = "8880" ]
|
||||||
[ "$(argv --lang)" = "auto" ]
|
[ "$(argv --lang)" = "auto" ]
|
||||||
[ "$(argv --max-batch)" = "1" ]
|
[ "$(argv --max-batch)" = "1" ]
|
||||||
# The two flags that must never appear: there is no thread count to set (it
|
# The device the env names has to reach argv BY NAME: the image ships the
|
||||||
# comes from hardware_concurrency()/2), and naming a device on an image with
|
# Mesa ICD and no NVIDIA one, so Vulkan0 is the gfx1151 and cannot drift onto
|
||||||
# every GPU backend compiled out is only a new way to be wrong.
|
# another card the way an index would.
|
||||||
! grep -qx -- '--device' "$ARGS_FILE"
|
[ "$(argv --device)" = "Vulkan0" ]
|
||||||
|
# The flag that must never appear: there is no thread count to set in this
|
||||||
|
# build (it comes from hardware_concurrency()/2), so naming one would only
|
||||||
|
# disagree with what the binary sized its pools from.
|
||||||
! grep -qE -- '^--?t$|^--threads$' "$ARGS_FILE"
|
! grep -qE -- '^--?t$|^--threads$' "$ARGS_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "an empty device value is the CPU rollback: no --device reaches argv" {
|
||||||
|
# The same entrypoint with one key cleared, which is the rollback the env
|
||||||
|
# header documents. A script-level default that survived an explicitly empty
|
||||||
|
# value would leave the endpoint on the iGPU with no way back except a
|
||||||
|
# rebuild.
|
||||||
|
cat > "$TEST_TMP/fake-tts-server" <<'FAKE'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
printf '%s\n' "$@" > "$ARGS_FILE"
|
||||||
|
FAKE
|
||||||
|
chmod +x "$TEST_TMP/fake-tts-server"
|
||||||
|
mkdir -p "$TEST_TMP/models"
|
||||||
|
: > "$TEST_TMP/models/$TALKER"
|
||||||
|
: > "$TEST_TMP/models/$CODEC"
|
||||||
|
export QWENTTS_MODEL="$TEST_TMP/models/$TALKER"
|
||||||
|
export QWENTTS_CODEC="$TEST_TMP/models/$CODEC"
|
||||||
|
export QWENTTS_BIN="$TEST_TMP/fake-tts-server"
|
||||||
|
export ARGS_FILE="$TEST_TMP/argv.txt"
|
||||||
|
export QWENTTS_DEVICE=""
|
||||||
|
|
||||||
|
run bash "$START"
|
||||||
|
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||||
|
! grep -qx -- '--device' "$ARGS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
@test "missing weights are a refusal that names the fetcher, before the port is bound" {
|
@test "missing weights are a refusal that names the fetcher, before the port is bound" {
|
||||||
cat > "$TEST_TMP/fake-tts-server" <<'FAKE'
|
cat > "$TEST_TMP/fake-tts-server" <<'FAKE'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -627,17 +654,23 @@ PY
|
|||||||
@test "the image build is the pinned commit with the proven cmake line" {
|
@test "the image build is the pinned commit with the proven cmake line" {
|
||||||
# GGML_BLAS=OFF is forced by the host (libopenblas.so.0 present, no dev
|
# GGML_BLAS=OFF is forced by the host (libopenblas.so.0 present, no dev
|
||||||
# headers) and upstream's own Dockerfile passes it ON, so this file is not a
|
# headers) and upstream's own Dockerfile passes it ON, so this file is not a
|
||||||
# copy of upstream on purpose. -j 8 and the OFF-for-every-GPU-backend list
|
# copy of upstream on purpose. -j 8 is what the 2026-10-18 trial ran;
|
||||||
# are what the 2026-10-18 trial ran.
|
# GGML_VULKAN=ON is the 2026-10-19 iGPU cutover, and the two greps below are
|
||||||
|
# the packages that door needs at build time and at run time.
|
||||||
grep -qE '^ARG QWENTTS_COMMIT=[0-9a-f]{40}$' "$CF"
|
grep -qE '^ARG QWENTTS_COMMIT=[0-9a-f]{40}$' "$CF"
|
||||||
grep -qF '5916bf7c79dbb4e66fc059cdc507a79a23a28bb4' "$CF"
|
grep -qF '5916bf7c79dbb4e66fc059cdc507a79a23a28bb4' "$CF"
|
||||||
grep -qE 'git -C /src checkout --detach' "$CF"
|
grep -qE 'git -C /src checkout --detach' "$CF"
|
||||||
grep -qE 'submodule update --init' "$CF" # ggml is a submodule; without it the build cannot configure
|
grep -qE 'submodule update --init' "$CF" # ggml is a submodule; without it the build cannot configure
|
||||||
for flag in -DGGML_CCACHE=OFF -DGGML_BACKEND_DL=OFF -DGGML_CPU_ALL_VARIANTS=OFF \
|
for flag in -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_OPENMP=ON -DGGML_BLAS=OFF -DGGML_CUDA=OFF -DGGML_HIP=OFF \
|
||||||
-DGGML_VULKAN=OFF -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF; do
|
-DGGML_VULKAN=ON -DGGML_METAL=OFF -DGGML_SYCL=OFF -DGGML_RPC=OFF; do
|
||||||
grep -qF -- "$flag" "$CF" || { echo "build line lost $flag"; false; }
|
grep -qF -- "$flag" "$CF" || { echo "build line lost $flag"; false; }
|
||||||
done
|
done
|
||||||
|
# glslc is not in glslang-tools on this distro, and the configure fails
|
||||||
|
# without it; an image with the loader and no RADV ICD enumerates zero
|
||||||
|
# devices and dies in device lookup after the weights load.
|
||||||
|
grep -qE 'libvulkan-dev glslc spirv-headers' "$CF"
|
||||||
|
grep -qE 'libvulkan1 mesa-vulkan-drivers' "$CF"
|
||||||
grep -qE 'cmake --build build -j 8' "$CF"
|
grep -qE 'cmake --build build -j 8' "$CF"
|
||||||
# OpenMP on means the runtime stage needs the OpenMP runtime, and the probe
|
# OpenMP on means the runtime stage needs the OpenMP runtime, and the probe
|
||||||
# runs as /bin/sh -c, so the runtime stage needs curl.
|
# runs as /bin/sh -c, so the runtime stage needs curl.
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
# pool (4,096 B/position, 2,352.00 MiB here), and the 786,432 pool this shape
|
# pool (4,096 B/position, 2,352.00 MiB here), and the 786,432 pool this shape
|
||||||
# replaced cannot carry a drafter at any dtype inside the 24,123 MiB the driver
|
# replaced cannot carry a drafter at any dtype inside the 24,123 MiB the driver
|
||||||
# leaves addressable. The publisher's own Q8_0 vision projector rides along (the
|
# leaves addressable. The publisher's own Q8_0 vision projector rides along (the
|
||||||
# BF16 one can crash on concurrent images). Log
|
# BF16 one can crash on concurrent images). The payload also carries a
|
||||||
|
# REFUSAL-DIRECTION ADAPTER: a rank-1 LoRA applied at load, so the seam pins the
|
||||||
|
# --lora-scaled token (path and scale together), the scale knob, and the
|
||||||
|
# empty-LORA_FILE opt-out.
|
||||||
# This seam runs the serve script against a stub engine and asserts on the
|
# This seam runs the serve script against a stub engine and asserts on the
|
||||||
# argv it actually execs, so what is pinned is the command the operator gets.
|
# argv it actually execs, so what is pinned is the command the operator gets.
|
||||||
|
|
||||||
@@ -39,8 +42,9 @@ STUB
|
|||||||
# (the env) instead of silently forking the test's filename constants.
|
# (the env) instead of silently forking the test's filename constants.
|
||||||
MODEL_STUB="$(basename "$(sed -nE 's/^MODEL_FILE=(.+)$/\1/p' "$ENV_FILE")")"
|
MODEL_STUB="$(basename "$(sed -nE 's/^MODEL_FILE=(.+)$/\1/p' "$ENV_FILE")")"
|
||||||
MM_STUB="$(basename "$(sed -nE 's/^MMPROJ_FILE=(.+)$/\1/p' "$ENV_FILE")")"
|
MM_STUB="$(basename "$(sed -nE 's/^MMPROJ_FILE=(.+)$/\1/p' "$ENV_FILE")")"
|
||||||
|
LORA_STUB="$(basename "$(sed -nE 's/^LORA_FILE=(.+)$/\1/p' "$ENV_FILE")")"
|
||||||
DRAFT_STUB="draft-assistant.gguf"
|
DRAFT_STUB="draft-assistant.gguf"
|
||||||
touch "$MODEL_DIR/$MODEL_STUB" "$MODEL_DIR/$MM_STUB" "$MODEL_DIR/$DRAFT_STUB"
|
touch "$MODEL_DIR/$MODEL_STUB" "$MODEL_DIR/$MM_STUB" "$MODEL_DIR/$LORA_STUB" "$MODEL_DIR/$DRAFT_STUB"
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
@@ -69,6 +73,8 @@ SPEC_DRAFT_N_MAX=$(sed -nE 's/^SPEC_DRAFT_N_MAX=([0-9]+)$/\1/p' "$ENV_FILE")
|
|||||||
VERBOSITY=$(sed -nE 's/^VERBOSITY=([0-9]+)$/\1/p' "$ENV_FILE")
|
VERBOSITY=$(sed -nE 's/^VERBOSITY=([0-9]+)$/\1/p' "$ENV_FILE")
|
||||||
METRICS=$(sed -nE 's/^METRICS=([0-9]+)$/\1/p' "$ENV_FILE")
|
METRICS=$(sed -nE 's/^METRICS=([0-9]+)$/\1/p' "$ENV_FILE")
|
||||||
NGL=$(sed -nE 's/^NGL=([0-9]+)$/\1/p' "$ENV_FILE")
|
NGL=$(sed -nE 's/^NGL=([0-9]+)$/\1/p' "$ENV_FILE")
|
||||||
|
LORA_FILE=$MODEL_DIR/$LORA_STUB
|
||||||
|
LORA_SCALE=$(sed -nE 's/^LORA_SCALE=(.+)$/\1/p' "$ENV_FILE")
|
||||||
EOF
|
EOF
|
||||||
run env FRANK_ZEALOT_ENV_FILE="$envf" nvidia-modprobe=true bash "$SERVE"
|
run env FRANK_ZEALOT_ENV_FILE="$envf" nvidia-modprobe=true bash "$SERVE"
|
||||||
}
|
}
|
||||||
@@ -117,7 +123,7 @@ run_stage() {
|
|||||||
grep -q 'FRANK_ZEALOT_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-zealot.env' "$UNIT"
|
grep -q 'FRANK_ZEALOT_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-zealot.env' "$UNIT"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the shipped env points at the fork runtime and the Bonsai-2 9B MTP pair" {
|
@test "the shipped env points at the fork runtime, the MTP pair and the refusal adapter" {
|
||||||
# The payload path IS the endpoint's identity, and the runtime is part of
|
# The payload path IS the endpoint's identity, and the runtime is part of
|
||||||
# it here: the file's custom tensor type 142 + prism.hadamard.* metadata
|
# it here: the file's custom tensor type 142 + prism.hadamard.* metadata
|
||||||
# make the PrismML build the only loader, and this build must be >= b10776
|
# make the PrismML build the only loader, and this build must be >= b10776
|
||||||
@@ -127,6 +133,7 @@ run_stage() {
|
|||||||
grep -qE '^LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server$' "$ENV_FILE"
|
grep -qE '^LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server$' "$ENV_FILE"
|
||||||
grep -qE '^MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0\.gguf$' "$ENV_FILE"
|
grep -qE '^MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0\.gguf$' "$ENV_FILE"
|
||||||
grep -qE '^MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0\.gguf$' "$ENV_FILE"
|
grep -qE '^MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0\.gguf$' "$ENV_FILE"
|
||||||
|
grep -qE '^LORA_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA\.gguf$' "$ENV_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the deployer gates on exactly the payload paths the env file ships" {
|
@test "the deployer gates on exactly the payload paths the env file ships" {
|
||||||
@@ -135,7 +142,7 @@ run_stage() {
|
|||||||
# would be gating on a file nobody reads — and a missing projector would
|
# would be gating on a file nobody reads — and a missing projector would
|
||||||
# ship a blind endpoint.
|
# ship a blind endpoint.
|
||||||
local env_v dep_v path pair
|
local env_v dep_v path pair
|
||||||
for pair in "MODEL_FILE LLM_ZEALOT_MODEL_FILE" "MMPROJ_FILE LLM_ZEALOT_MMPROJ"; do
|
for pair in "MODEL_FILE LLM_ZEALOT_MODEL_FILE" "MMPROJ_FILE LLM_ZEALOT_MMPROJ" "LORA_FILE LLM_ZEALOT_LORA"; do
|
||||||
env_v="${pair%% *}"
|
env_v="${pair%% *}"
|
||||||
dep_v="${pair##* }"
|
dep_v="${pair##* }"
|
||||||
path="$(sed -nE "s/^${env_v}=(.+)$/\1/p" "$ENV_FILE")"
|
path="$(sed -nE "s/^${env_v}=(.+)$/\1/p" "$ENV_FILE")"
|
||||||
@@ -367,6 +374,45 @@ run_stage() {
|
|||||||
! grep -qxF -- '--mmproj' <<<"$output"
|
! grep -qxF -- '--mmproj' <<<"$output"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "the refusal adapter reaches the engine as path and scale in ONE token" {
|
||||||
|
# --lora-scaled takes FNAME:SCALE as a single argument, so the seam pins the
|
||||||
|
# whole token. An adapter that reaches the engine without its scale — or a
|
||||||
|
# scale without its adapter — is a different payload than the one the env
|
||||||
|
# file describes, and nothing else would notice.
|
||||||
|
serve_argv
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
local scale
|
||||||
|
scale="$(sed -nE 's/^LORA_SCALE=(.+)$/\1/p' "$ENV_FILE")"
|
||||||
|
[ "$(argv_value --lora-scaled)" = "$MODEL_DIR/$LORA_STUB:$scale" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the scale knob rides inside the token, including the card's off value" {
|
||||||
|
# Distinctive values first: proof the flag is wired to the env and not
|
||||||
|
# merely to the script's default. Scale 0 is the card's second mode — the
|
||||||
|
# direction present but inert — so the endpoint can be neutered without
|
||||||
|
# deleting a file or rebuilding anything.
|
||||||
|
serve_argv
|
||||||
|
{ echo 'LORA_SCALE=0'; } >> "$STAGE/test.env"
|
||||||
|
run_stage
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$(argv_value --lora-scaled)" = "$MODEL_DIR/$LORA_STUB:0" ]
|
||||||
|
|
||||||
|
sed -i 's|^LORA_SCALE=.*|LORA_SCALE=0.75|' "$STAGE/test.env"
|
||||||
|
run_stage
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$(argv_value --lora-scaled)" = "$MODEL_DIR/$LORA_STUB:0.75" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "LORA_FILE empty opts the adapter out and leaves no trace in the argv" {
|
||||||
|
# The rollback is ABSENCE of the flag: an empty argument would have the
|
||||||
|
# engine open "" and die at load, which is the failure this seam forbids.
|
||||||
|
serve_argv
|
||||||
|
sed -i 's|^LORA_FILE=.*|LORA_FILE=|' "$STAGE/test.env"
|
||||||
|
run_stage
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
! grep -qxF -- '--lora-scaled' <<<"$output"
|
||||||
|
}
|
||||||
|
|
||||||
@test "the card sampler's only non-metadata term reaches the argv" {
|
@test "the card sampler's only non-metadata term reaches the argv" {
|
||||||
# general.sampling.* (temperature / top_p / top_k / min_p / repeat_penalty)
|
# general.sampling.* (temperature / top_p / top_k / min_p / repeat_penalty)
|
||||||
# rides in the checkpoint and the engine reads it itself; presence penalty
|
# rides in the checkpoint and the engine reads it itself; presence penalty
|
||||||
@@ -406,23 +452,33 @@ run_stage() {
|
|||||||
grep -qxF -- '--jinja' <<<"$output"
|
grep -qxF -- '--jinja' <<<"$output"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the serve script refuses a missing runtime, GGUF or projector" {
|
@test "the serve script refuses a missing runtime, GGUF, projector or adapter" {
|
||||||
rm -f "$MODEL_DIR/$MM_STUB"
|
rm -f "$MODEL_DIR/$MM_STUB"
|
||||||
serve_argv
|
serve_argv
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
[[ "$output" == *"frank-zealot: missing"* ]]
|
[[ "$output" == *"frank-zealot: missing"* ]]
|
||||||
|
|
||||||
|
# The adapter is payload: a fetch that stopped short must not boot a
|
||||||
|
# silently-plain endpoint, and Restart=always would loop on the missing
|
||||||
|
# file until somebody reads the name in the log.
|
||||||
|
touch "$MODEL_DIR/$MM_STUB"
|
||||||
|
rm -f "$MODEL_DIR/$LORA_STUB"
|
||||||
|
serve_argv
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"frank-zealot: missing $MODEL_DIR/$LORA_STUB"* ]]
|
||||||
|
|
||||||
rm -f "$RUNTIME_DIR/bin/llama-server"
|
rm -f "$RUNTIME_DIR/bin/llama-server"
|
||||||
serve_argv
|
serve_argv
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
[[ "$output" == *"frank-zealot: missing"* ]]
|
[[ "$output" == *"frank-zealot: missing"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the deploy handler gates on binary, GGUF and projector, then MainPID-hardens health" {
|
@test "the deploy handler gates on binary, GGUF, projector and adapter, then MainPID-hardens health" {
|
||||||
grep -q 'deploy_llm_zealot' "$REMOTE_DEPLOY"
|
grep -q 'deploy_llm_zealot' "$REMOTE_DEPLOY"
|
||||||
grep -q 'LLM_ZEALOT_RUNTIME' "$REMOTE_DEPLOY"
|
grep -q 'LLM_ZEALOT_RUNTIME' "$REMOTE_DEPLOY"
|
||||||
grep -q 'LLM_ZEALOT_MODEL_FILE' "$REMOTE_DEPLOY"
|
grep -q 'LLM_ZEALOT_MODEL_FILE' "$REMOTE_DEPLOY"
|
||||||
grep -q 'LLM_ZEALOT_MMPROJ' "$REMOTE_DEPLOY"
|
grep -q 'LLM_ZEALOT_MMPROJ' "$REMOTE_DEPLOY"
|
||||||
|
grep -q 'LLM_ZEALOT_LORA' "$REMOTE_DEPLOY"
|
||||||
grep -q 'relink_unit frank-zealot' "$REMOTE_DEPLOY"
|
grep -q 'relink_unit frank-zealot' "$REMOTE_DEPLOY"
|
||||||
grep -q 'systemctl enable frank-zealot.service' "$REMOTE_DEPLOY"
|
grep -q 'systemctl enable frank-zealot.service' "$REMOTE_DEPLOY"
|
||||||
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
|
grep -q '127.0.0.1:7771/health' "$REMOTE_DEPLOY"
|
||||||
|
|||||||
+12
-11
@@ -170,18 +170,19 @@ DB_PIN="sha256:e5d7387e3727bbe2f7cd6d869e48cd34777272258572ead37c9656b25f745723"
|
|||||||
! grep -qE '^Exec=.*--(pass|user)' "$DB"
|
! grep -qE '^Exec=.*--(pass|user)' "$DB"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "both health probes are argument-only and name real paths" {
|
@test "the database probe can run in a shell-less image and the API probe names the health route" {
|
||||||
for unit in "$APP" "$DB"; do
|
# podman wraps a BARE HealthCmd as CMD-SHELL (measured on this host's podman
|
||||||
cmd="$(sed -n 's/^HealthCmd=//p' "$unit")"
|
# 5.7), and the SurrealDB image is distroless — no /bin/sh — so a bare
|
||||||
[ -n "$cmd" ] || { echo "$(basename "$unit") has no HealthCmd"; false; }
|
# command there is a probe that can only ever fail: the database answers
|
||||||
# The quadlet generator splits HealthCmd on spaces, so anything needing
|
# perfectly while the health status stays `unhealthy` and the deploy gate
|
||||||
# a shell (quotes, pipes, &&) would be silently mis-sent.
|
# times out. Only the exec-form JSON array runs in that image.
|
||||||
! printf '%s' "$cmd" | grep -qE '["&;|`]'
|
db_cmd="$(sed -n 's/^HealthCmd=//p' "$DB")"
|
||||||
done
|
[ "$db_cmd" = '["/surreal","is-ready"]' ] || {
|
||||||
# The API's own route, exempt from its password middleware upstream.
|
echo "database probe is not exec-form JSON: $db_cmd"; false; }
|
||||||
|
# The API image ships curl and a shell, so a bare command is fine there; it
|
||||||
|
# must name the route that is exempt from the password middleware, on the
|
||||||
|
# CONTAINER port (the probe runs inside the netns).
|
||||||
grep -qxF 'HealthCmd=curl -fsS http://127.0.0.1:5055/health' "$APP"
|
grep -qxF 'HealthCmd=curl -fsS http://127.0.0.1:5055/health' "$APP"
|
||||||
# The distroless database image has no shell and no curl; it ships a probe.
|
|
||||||
grep -qxF 'HealthCmd=/surreal is-ready' "$DB"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the deployer gates open-notebook on the health path that actually answers" {
|
@test "the deployer gates open-notebook on the health path that actually answers" {
|
||||||
|
|||||||
Reference in new issue
Block a user