The NPU path is a dead end for this payload (verdict: report/tts-npu-spike-verdict-2026-10-19.md); the iGPU is the accelerator that exists on this box today, and it is already shared with frank-dragoon. - Containerfile.engine: -DGGML_VULKAN=ON, plus the three packages the door needs to configure — libvulkan-dev, spirv-headers, and glslc, which Ubuntu's glslang-tools does NOT ship (find_package(Vulkan COMPONENTS glslc REQUIRED) fails without it) — and libvulkan1 + mesa-vulkan-drivers (the RADV ICD) at runtime. No NVIDIA ICD on purpose: one Vulkan device in the enumeration is what makes the NAME Vulkan0 safe to write down. - tts-engine-start.sh: QWENTTS_DEVICE -> --device, default Vulkan0. A value that is set but EMPTY passes no flag at all, which is the CPU rollback; that is why the default is written as an -v test and not with :=, because := also fires on an empty value and would silently undo the rollback. - quadlet: AddDevice=/dev/dri + GroupAdd=keep-groups (a rootless container that drops its supplementary groups enumerates no render device). The node is not named: this host is hybrid, renderD128 is the NVIDIA card and renderD129 the amdgpu iGPU. - env: QWENTTS_DEVICE=Vulkan0 plus the device law in the header, including the rollback line. - tests: the two CPU-only assertions now assert the Vulkan shape, and a new one pins the rollback (empty device => no --device in argv). 451/451 green. Measured before writing any of it: the configure in the digest-pinned base answers "Found Vulkan: ... found components: glslc" and "Including Vulkan backend"; gate 1 (RADV GFX1151 visible to a rootless container off this base) is recorded in the verdict report.
179 lines
9.9 KiB
Docker
179 lines
9.9 KiB
Docker
# frank-tts-engine — the arm A TTS engine: qwentts.cpp's resident `tts-server`
|
|
# (the CPU binary that frank-tts on :7772 forwards to). The runbook is
|
|
# [[Frank TTS Engine]]; the fleet-facing endpoint is [[Frank TTS]].
|
|
#
|
|
# WHAT THIS IS. The upstream source is NOT vendored here: the image clones
|
|
# ServeurpersoCom/qwentts.cpp at one pinned commit and builds it. Rebuilding is
|
|
# therefore a network operation (github.com plus objects.githubusercontent.com
|
|
# for the `ggml` submodule), and build_compose_image in scripts/deploy-remote.sh
|
|
# skips it whenever this Containerfile's sha256 label already matches the built
|
|
# image — the same offline-safe idiom as loki/alloy/comfyui, so a re-deploy of
|
|
# an unchanged engine needs no network.
|
|
#
|
|
# THE PINNED COMMIT IS THE CONTRACT. 5916bf7c79dbb4e66fc059cdc507a79a23a28bb4
|
|
# is the commit the 2026-10-18 CPU trial on Dankclaw measured (server-side TTFA
|
|
# 94-112 ms, warm RTF 0.55-0.67, run spread +-0.01 — the numbers that won the
|
|
# engine decision). Moving it means re-measuring, not just editing this line.
|
|
# `ggml` is a submodule of the repo (the only one; vendor/cpp-httplib and
|
|
# vendor/yyjson are ordinary directories in the tree), and the submodule commit
|
|
# is recorded in the parent tree, so cloning the parent commit recursively is
|
|
# reproducible without a second pin.
|
|
#
|
|
# 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 \
|
|
# -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
|
|
#
|
|
# Every flag is load-bearing, so do not "tidy" it:
|
|
# GGML_BLAS=OFF is FORCED, not a preference — Dankclaw has libopenblas.so.0
|
|
# but no BLAS dev headers, so the upstream default (BLAS on) fails to
|
|
# configure. Upstream's own Dockerfile and buildcpu.sh both pass BLAS on,
|
|
# which is why this file does not copy them.
|
|
# GGML_CCACHE=OFF because a container build layer has no persistent ccache
|
|
# directory to hit; on, it only writes a useless one.
|
|
# GGML_OPENMP=ON is what makes the runtime need libgomp1 (installed below).
|
|
# GGML_BACKEND_DL=OFF / GGML_CPU_ALL_VARIANTS=OFF: upstream's images build
|
|
# every x86 ISA as a loadable backend so one image runs anywhere. This one
|
|
# is built on the machine that runs it, so the single native CPU backend is
|
|
# what was measured. That is also why no -DGGML_NATIVE is passed: ggml
|
|
# 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
|
|
# this image elsewhere, which is why the fleet builds it on Dankclaw.
|
|
# CUDA/HIP/METAL/SYCL/RPC stay OFF, VULKAN is ON: the endpoint computes on the
|
|
# 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
|
|
# tts-server links libggml*.so rather than swallowing them; the runtime stage
|
|
# copies the binary and its shared objects out of the build tree and points
|
|
# LD_LIBRARY_PATH at them, exactly as upstream's own Dockerfile does (the RPATH
|
|
# baked at link time names the build directory, which does not exist here).
|
|
#
|
|
# BASE PINNED BY DIGEST, like every other image this fleet builds: the digest
|
|
# is the multi-arch INDEX of ubuntu:24.04 as resolved from Docker Hub on
|
|
# 2026-10-18; Dankclaw takes the amd64 entry. One base for both stages so there
|
|
# is exactly one thing to re-resolve.
|
|
#
|
|
# NO HEALTHCHECK IN THIS FILE. The probe lives in the Quadlet unit
|
|
# (HealthCmd=), which is the fleet pattern for anything with a shell — see the
|
|
# alertmanager/prometheus units, and the opposite case in dankclaw/loki, whose
|
|
# distroless image has no /bin/sh for the generator's CMD-SHELL wrapping.
|
|
# `curl` is therefore installed in the runtime stage, for that probe only.
|
|
|
|
ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:68ee22004a9109b44973313b02ff7b1de250f1de0f5ecb628d73f579aaff33ac
|
|
|
|
# ------------------------------------------------------------------ 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 \
|
|
&& apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates cmake g++ git make \
|
|
libvulkan-dev glslc spirv-headers \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Checked out detached at a full SHA, then the recorded submodule. No branch
|
|
# name anywhere: a branch moves, this commit does not.
|
|
ARG QWENTTS_COMMIT=5916bf7c79dbb4e66fc059cdc507a79a23a28bb4
|
|
RUN git clone --no-checkout https://github.com/ServeurpersoCom/qwentts.cpp /src \
|
|
&& git -C /src checkout --detach "${QWENTTS_COMMIT}" \
|
|
&& git -C /src submodule update --init --depth 1 \
|
|
&& git -C /src rev-parse HEAD \
|
|
&& git -C /src submodule status
|
|
|
|
WORKDIR /src
|
|
RUN 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
|
|
|
|
# ---------------------------------------------------------------- runtime
|
|
FROM ${BASE_IMAGE}
|
|
|
|
# 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
|
|
# 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 \
|
|
&& apt-get install -y -qq --no-install-recommends \
|
|
ca-certificates curl libgomp1 libvulkan1 mesa-vulkan-drivers \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Same unprivileged uid as the frank-tts façade (10000): the voice packs and the
|
|
# model directory are host state mounted read-only, and one uid across the pair
|
|
# means one thing to remember about what is world-readable.
|
|
#
|
|
# `useradd`, NOT `adduser`: this trimmed ubuntu:24.04 base ships no `adduser` at
|
|
# all (verified 2026-10-18 against this digest — `adduser` is absent from the
|
|
# base and is not pulled in by ca-certificates/curl/libgomp1 either), so an
|
|
# `adduser` line dies at image-build time with crun's "executable file `adduser`
|
|
# not found in $PATH". `useradd --system --uid 10000` succeeds and prints one
|
|
# cosmetic warning (`uid 10000 is greater than SYS_UID_MAX 999`); the uid is the
|
|
# point, and it is what matches the façade. The alpine façade Containerfile keeps
|
|
# its own busybox `adduser -D -u 10000` — different base, different tool, same uid.
|
|
RUN useradd --system --uid 10000 --home-dir /app --shell /usr/sbin/nologin tts
|
|
|
|
WORKDIR /app
|
|
COPY --from=build /src/build/tts-server /src/build/*.so* /app/
|
|
COPY dankclaw/voiceservice/tts-engine-start.sh /app/start.sh
|
|
# The boot warmer lives INSIDE the image because the engine publishes no port:
|
|
# the unit reaches it with `podman exec`, and the script needs the same curl,
|
|
# base64 and /voices the engine's own namespace has. It is a sibling of
|
|
# start.sh, not a second entrypoint — CMD stays the server.
|
|
COPY dankclaw/voiceservice/tts-engine-warm.sh /app/warm-voices.sh
|
|
RUN chmod 755 /app/start.sh /app/warm-voices.sh
|
|
|
|
# The binary's RPATH points inside the build tree; this is what makes it find
|
|
# the libggml*.so copied beside it.
|
|
ENV LD_LIBRARY_PATH=/app
|
|
|
|
USER tts
|
|
EXPOSE 8880
|
|
CMD ["/app/start.sh"]
|