Add Dockerfile (cpu/cuda/vulkan), entrypoint, docs, and a GHCR build/release workflow (#15)

* Add Dockerfile (cpu/cuda), entrypoint, docs, and a GHCR release workflow

Multi-stage Dockerfile with cpu and cuda targets, built from the
existing build scripts' cmake invocations. The CUDA target documents
and applies the two docker-build-specific gotchas we hit running this
in production: CMAKE_CUDA_ARCHITECTURES needs an explicit override for
GPUs older than this project's own default arch list when building
without GPU device access, and the CUDA driver stub library needs an
explicit -L/-lcuda at link time since ggml's VMM pool allocator needs
driver-API symbols that aren't present without a real driver. Also
installs make/pkg-config/libopenblas-dev (CPU build) and libgomp1
(CUDA runtime), and copies the ggml shared libraries alongside the
binaries with LD_LIBRARY_PATH set, since the binaries' baked-in RPATH
points at the build-tree location that doesn't exist in the final
stage.

Adds a GitHub Actions workflow that builds both variants on every
push to master and version tag, publishing to
ghcr.io/serveurpersocom/qwentts.cpp, and validates the build (without
pushing) on PRs that touch the Docker files.

Verified end-to-end on a real GTX 1070 (Pascal): both cpu and cuda
targets build clean, run, and produce valid synthesized WAV output
through tts-server's HTTP API.

* Dockerfile: add a vulkan build target (AMD/Intel GPUs)

Uses the LunarG Vulkan SDK apt repo for glslc (ggml-vulkan's shader
compiler), which Ubuntu 22.04's own repos don't package. Runtime image
ships Mesa's Vulkan drivers; NVIDIA users should prefer :cuda instead,
since NVIDIA's Vulkan ICD isn't bundled.

* docker workflow: bump actions to latest majors (checkout v7, buildx v4, login v4, build-push v7)

* Support Pascal (sm_61) in the default CUDA architecture list

Adds 61-real to CMAKE_CUDA_ARCHITECTURES' default so Pascal cards
(GTX 10-series etc.) work without an explicit override -- including
in the Docker cuda target, where docker build has no GPU device to
autodetect against in the first place. Real-only (no virtual/PTX):
Pascal is now the oldest supported card, so it doesn't need to seed
forward JIT compatibility for anything older the way the 75-virtual
entry does for 7.5+.

Verified end-to-end on a real GTX 1070 (sm_61): built the cuda Docker
target with no --build-arg override, ran it with --gpus all, and
confirmed via container logs that the GPU loaded the model and served
a real synthesis request producing valid WAV output.

Adjusts the Dockerfile comments and docs/DOCKER.md accordingly.

* docker: pass through --max-prefill-tokens, add ref_text voice cloning

- entrypoint.sh: forward MAX_PREFILL_TOKENS to --max-prefill-tokens,
  matching the existing optional-flag passthrough pattern
- entrypoint.sh: a same-stem .txt next to a voice's .wav now supplies
  ref_text, enabling ICL clone mode instead of the x_vector_only
  fallback used when no transcript is given
- switch voice registration's JSON construction from raw printf to jq
  for safe escaping of arbitrary transcript text; base64 payloads go
  through jq's --rawfile (not --arg) since large files blow past
  ARG_MAX as a command-line argument
- add jq to all three runtime image stages (cpu/cuda/vulkan) for the
  above
- docs/DOCKER.md: document both additions

Verified end-to-end on a CPU build: both the ref_text and no-ref_text
registration paths log correctly (ref_text=yes / ref_text=no) and
/health responds after voice registration completes.

* docker: opt-in fatal worst-case warmup synthesis

ggml_backend_sched grows its compute buffer to fit the largest graph
it has ever built and never shrinks it back, so VRAM use can ratchet
up the first time a long reply arrives on live traffic. This adds an
opt-in startup warmup: when WARMUP_VOICE is set, entrypoint.sh runs
one real synthesis capped at WARMUP_MAX_NEW_TOKENS (default 750)
frames after voice registration, forcing that worst-case decode/
codec-decode buffer growth to happen at startup instead of mid-request.
Complements --max-prefill-tokens, which only covers the input side.

Off by default (WARMUP_VOICE unset skips it entirely, matching every
other optional flag in this entrypoint), and fatal on failure: if the
warmup synthesis fails (most likely OOM), the container kills the
server and exits non-zero rather than come up healthy and fail
unpredictably later -- a deployment that can't afford its own
configured worst case should know that at startup, not on a live
request.

Verified on a CPU build: warmup runs after voice registration, hits
the configured frame cap exactly, and the container stays healthy.

---------

Co-authored-by: Gary <gitea@gerasch.dev>
This commit is contained in:
Stefan Gerasch
2026-08-07 07:52:22 +02:00
committed by GitHub
co-authored by Gary
parent 7b6ed4f6db
commit 978fc08f5a
7 changed files with 413 additions and 3 deletions
+108
View File
@@ -0,0 +1,108 @@
# syntax=docker/dockerfile:1
#
# Build context must have the `ggml` submodule checked out already
# (`git clone --recurse-submodules`, or `actions/checkout` with
# `submodules: recursive` in CI) -- this Dockerfile does not fetch it.
#
# Usage:
# docker build --target cpu -t qwentts.cpp:cpu .
# docker build --target cuda -t qwentts.cpp:cuda .
# docker build --target vulkan -t qwentts.cpp:vulkan .
#
# This project's default distributed arch list covers Pascal (sm_61) to
# Blackwell. GPUs older than that need an explicit override, since
# `docker build` has no GPU device to auto-detect against:
# docker build --target cuda -t qwentts.cpp:cuda \
# --build-arg CMAKE_CUDA_ARCHITECTURES=50 . # Maxwell
# See docs/DOCKER.md for details.
ARG CUDA_BUILD_IMAGE=nvidia/cuda:12.4.1-devel-ubuntu22.04
ARG CUDA_RUNTIME_IMAGE=nvidia/cuda:12.4.1-runtime-ubuntu22.04
# ---------------------------------------------------------------- CPU build
FROM ubuntu:22.04 AS build-cpu
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
git ca-certificates cmake g++ make pkg-config libopenblas-dev \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . .
RUN cmake -B build -DGGML_BLAS=ON -DCMAKE_BUILD_TYPE=Release && \
cmake --build build --config Release -j"$(nproc)"
FROM ubuntu:22.04 AS cpu
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
libgomp1 libopenblas0 curl ca-certificates jq \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build-cpu /build/build/tts-server /build/build/qwen-tts /build/build/qwen-codec /build/build/*.so* ./
COPY docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Binaries are copied out of the build tree their RPATH points at, so the
# ggml shared libraries (copied alongside, above) need an explicit search path.
ENV LD_LIBRARY_PATH=/app
ENTRYPOINT ["./entrypoint.sh"]
# --------------------------------------------------------------- CUDA build
FROM ${CUDA_BUILD_IMAGE} AS build-cuda
ARG CMAKE_CUDA_ARCHITECTURES
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
git ca-certificates cmake g++ make \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . .
# `docker build` never has GPU device access, unlike `docker run --gpus`, so:
# - CMAKE_CUDA_ARCHITECTURES must be set explicitly when targeting a GPU
# generation outside this project's own default arch list (see
# docs/DOCKER.md); when unset here, CMake's own project default
# (Pascal and newer) is used unchanged.
# - ggml's CUDA VMM pool allocator needs driver-API symbols (cuMemCreate,
# cuMemMap, ...) at link time. The real libcuda.so isn't present without
# a GPU, but the devel image ships a link-time-only stub at
# lib64/stubs/libcuda.so for exactly this case; it isn't on the default
# linker search path so both -L and -lcuda are needed explicitly.
RUN cmake -B build -DGGML_CUDA=ON \
-DCMAKE_CUDA_COMPILER=/usr/local/cuda/bin/nvcc \
${CMAKE_CUDA_ARCHITECTURES:+-DCMAKE_CUDA_ARCHITECTURES=${CMAKE_CUDA_ARCHITECTURES}} \
-DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
-DCMAKE_SHARED_LINKER_FLAGS="-L/usr/local/cuda/lib64/stubs -lcuda" \
-DCMAKE_BUILD_TYPE=Release && \
cmake --build build --config Release -j"$(nproc)"
FROM ${CUDA_RUNTIME_IMAGE} AS cuda
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
libgomp1 curl ca-certificates jq \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build-cuda /build/build/tts-server /build/build/qwen-tts /build/build/qwen-codec /build/build/*.so* ./
COPY docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
ENV LD_LIBRARY_PATH=/app
ENTRYPOINT ["./entrypoint.sh"]
# ------------------------------------------------------------- Vulkan build
# AMD/Intel GPUs (and NVIDIA via its Vulkan ICD). glslc (shader compiler) is
# only packaged by the LunarG SDK repo on Ubuntu 22.04, not apt's universe.
FROM ubuntu:22.04 AS build-vulkan
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
git ca-certificates cmake g++ make wget gnupg \
> /dev/null && rm -rf /var/lib/apt/lists/*
RUN wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc | gpg --dearmor -o /usr/share/keyrings/lunarg.gpg && \
echo "deb [signed-by=/usr/share/keyrings/lunarg.gpg] https://packages.lunarg.com/vulkan/1.3.296 jammy main" \
> /etc/apt/sources.list.d/lunarg-vulkan.list && \
apt-get update -qq && apt-get install -y -qq --no-install-recommends vulkan-sdk \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . .
RUN cmake -B build -DGGML_VULKAN=ON -DCMAKE_BUILD_TYPE=Release && \
cmake --build build --config Release -j"$(nproc)"
FROM ubuntu:22.04 AS vulkan
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \
libgomp1 libvulkan1 mesa-vulkan-drivers curl ca-certificates jq \
> /dev/null && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build-vulkan /build/build/tts-server /build/build/qwen-tts /build/build/qwen-codec /build/build/*.so* ./
COPY docker/entrypoint.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
ENV LD_LIBRARY_PATH=/app
ENTRYPOINT ["./entrypoint.sh"]