Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 56 additions & 19 deletions dgx/tts/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
# Thin TTS sidecar — reuses the v3 vLLM image's Python/torch/audio stack
# (aarch64-native, has librosa/soundfile already). Adds qwen-tts + FastAPI.
FROM ghcr.io/aeon-7/vllm-aeon-ultimate-dflash:qwen36-v3@sha256:6506ebcb79b1bd0d48f8afca127984791f32345333be1be0fef334eaa5a9e23a
ARG CUDA_DEVEL_IMAGE=nvidia/cuda:13.3.1-devel-ubuntu26.04@sha256:da3989b0ea8e8b4b241711edd5823bc1cc83d05a01882258bddad84d7394c37e
ARG CUDA_RUNTIME_IMAGE=nvidia/cuda:13.3.1-base-ubuntu26.04@sha256:f65b4f0b65bbf2e0a2520cebaec3120bf4ed110aecc3e7dcab3b11cb508a0484

# Qwen audio I/O and every package added to the pinned base image are locked
# to the exact versions verified on the GX10 ARM64 target.
COPY tts/apt-packages-arm64.lock /tmp/apt-packages-arm64.lock
FROM ${CUDA_DEVEL_IMAGE} AS builder

COPY tts/apt-builder-packages-arm64.lock /tmp/apt-builder-packages-arm64.lock
RUN apt-get update \
&& xargs -r apt-get install -y --no-install-recommends \
< /tmp/apt-packages-arm64.lock \
< /tmp/apt-builder-packages-arm64.lock \
&& rm -rf /var/lib/apt/lists/*

COPY tts/requirements-arm64.lock /tmp/requirements-arm64.lock

# flash-attn for max throughput. The qwen-asr README explicitly recommends this
# install style ("--no-build-isolation"). ARM64 compiles the hash-pinned source
# distribution (about 15 minutes on GX10) and failure must fail the image build.
# Cap MAX_JOBS to keep memory pressure down on Spark's unified RAM during the
# native build; 4 is conservative for a 20-core ARM with 128GB but keeps the
# build stable.
ENV MAX_JOBS=4 \
RUN python3 -m venv /opt/tts-venv
ENV PATH=/opt/tts-venv/bin:$PATH \
MAX_JOBS=4 \
FLASH_ATTN_CUDA_ARCHS=120
RUN pip install --no-cache-dir --break-system-packages --no-build-isolation \
--require-hashes -r /tmp/requirements-arm64.lock

COPY tts/requirements-slim-arm64.lock /tmp/requirements-slim-arm64.lock
RUN pip install --no-cache-dir --require-hashes \
-r /tmp/requirements-slim-arm64.lock

COPY tts/tts-packages-arm64.lock /tmp/tts-packages-arm64.lock
RUN pip install --no-cache-dir --require-hashes --no-deps \
-r /tmp/tts-packages-arm64.lock

COPY tts/flash-attn-arm64.lock /tmp/flash-attn-arm64.lock
RUN pip install --no-cache-dir --require-hashes --no-deps --no-build-isolation \
-r /tmp/flash-attn-arm64.lock

COPY tts/torchaudio-kaldi-compat-arm64.lock /tmp/torchaudio-kaldi-compat-arm64.lock
RUN pip download --no-cache-dir --require-hashes --no-deps \
-r /tmp/torchaudio-kaldi-compat-arm64.lock \
--dest /tmp/torchaudio-kaldi \
&& package_root="$(python3 -c \
'from pathlib import Path; import importlib.util; print(Path(importlib.util.find_spec('\''qwen_tts'\'').origin).resolve().parent)')" \
&& kaldi_wheel="$(find /tmp/torchaudio-kaldi -name 'torchaudio-*.whl' -print -quit)" \
&& python3 -c "from pathlib import Path; from zipfile import ZipFile; import sys; wheel, root, license_root = sys.argv[1:]; archive = ZipFile(wheel); source = archive.read('torchaudio/compliance/kaldi.py').decode(); license_text = archive.read('torchaudio-2.9.1.dist-info/LICENSE'); archive.close(); assert source.count('import torchaudio\\n') == 1; provenance = '# Derived from torchaudio 2.9.1, torchaudio/compliance/kaldi.py.\\n# SPDX-License-Identifier: BSD-2-Clause\\n# Full license: /usr/share/licenses/torchaudio-kaldi-compat/LICENSE\\n# Compatibility contract: only fbank is supported; mfcc is outside this runtime contract.\\n\\n'; Path(root, 'core/tokenizer_25hz/vq/kaldi_compat.py').write_text(provenance + source.replace('import torchaudio\\n', ''), encoding='utf-8'); license_path = Path(license_root) / 'LICENSE'; license_path.parent.mkdir(parents=True, exist_ok=True); license_path.write_bytes(license_text)" \
"$kaldi_wheel" "$package_root" /opt/tts-licenses/torchaudio-kaldi-compat \
&& python3 -c "from pathlib import Path; import sys; path = Path(sys.argv[1]) / 'core/tokenizer_25hz/vq/speech_vq.py'; source = path.read_text(encoding='utf-8'); updated = source.replace('import torchaudio.compliance.kaldi as kaldi', 'from . import kaldi_compat as kaldi'); assert updated != source; path.write_text(updated, encoding='utf-8')" \
Comment on lines +36 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The inlined wheel/kaldi-compat manipulation logic is quite dense and would benefit from being factored into a dedicated script for maintainability.

The python3 -c snippet that rewrites kaldi_compat.py tightly couples filesystem and archive manipulation into a single inline command, which makes it difficult to read, debug, and safely change (e.g., path updates or behavior tweaks). Moving this logic into a small, checked-in Python module that’s copied into the image and invoked from the Dockerfile would make the build more declarative and the transformation easier to test and evolve.

Suggested implementation:

COPY tts/torchaudio-kaldi-compat-arm64.lock /tmp/torchaudio-kaldi-compat-arm64.lock
COPY tts/extract_kaldi_compat.py /usr/local/bin/extract_kaldi_compat.py
RUN pip download --no-cache-dir --require-hashes --no-deps \

 && kaldi_wheel="$(find /tmp/torchaudio-kaldi -name 'torchaudio-*.whl' -print -quit)" \
 && python3 /usr/local/bin/extract_kaldi_compat.py \
      "$kaldi_wheel" "$package_root" /opt/tts-licenses/torchaudio-kaldi-compat \

To fully implement this refactor, add a new checked-in Python module tts/extract_kaldi_compat.py with a main() that:

  1. Parses wheel_path, package_root, and license_root from sys.argv[1:].
  2. Opens the wheel via ZipFile(wheel_path) and reads:
    • torchaudio/compliance/kaldi.py into source (decoded as UTF-8).
    • torchaudio-2.9.1.dist-info/LICENSE into license_text (bytes).
  3. Asserts source.count("import torchaudio\n") == 1 for safety.
  4. Prepends the provenance header:
    PROVENANCE = (
        "# Derived from torchaudio 2.9.1, torchaudio/compliance/kaldi.py.\n"
        "# SPDX-License-Identifier: BSD-2-Clause\n"
        "# Full license: /usr/share/licenses/torchaudio-kaldi-compat/LICENSE\n"
        "# Compatibility contract: only fbank is supported; mfcc is outside this runtime contract.\n"
        "\n"
    )
  5. Writes PROVENANCE + source.replace("import torchaudio\n", "") to
    Path(package_root) / "core/tokenizer_25hz/vq/kaldi_compat.py" with UTF-8 encoding.
  6. Ensures license_root exists (Path(license_root).mkdir(parents=True, exist_ok=True)) and writes license_text to Path(license_root) / "LICENSE".

Include the usual if __name__ == "__main__": main() boilerplate so the script can be invoked as done in the Dockerfile.

"$package_root" \
&& rm -rf /tmp/torchaudio-kaldi

RUN python3 -c 'import faster_qwen3_tts, qwen_tts, torch; import qwen_tts.core; import qwen_tts.inference.qwen3_tts_model; from qwen_tts.core.tokenizer_25hz.vq import kaldi_compat; features = kaldi_compat.fbank(torch.ones(1, 400), num_mel_bins=80, dither=0, sample_frequency=16000); assert features.shape == (1, 80); assert torch.isfinite(features).all()'

# faster-qwen3-tts 0.2.6 has no cancellation hook in its stable decode loop.
# Apply a fail-closed, version-bound patch so barge-in can stop between codec
Expand All @@ -37,13 +55,32 @@ RUN package_root="$(python3 -c \
&& rm /tmp/faster-qwen3-tts-0.2.6-cooperative-cancel.patch \
/tmp/verify_cooperative_cancel_patch.py

FROM ${CUDA_RUNTIME_IMAGE} AS runtime

COPY tts/apt-runtime-packages-arm64.lock /tmp/apt-runtime-packages-arm64.lock
RUN apt-get update \
&& xargs -r apt-get install -y --no-install-recommends \
< /tmp/apt-runtime-packages-arm64.lock \
&& rm -rf /var/lib/apt/lists/*

RUN ! command -v nvcc \
&& ! command -v make \
&& ! dpkg-query -W -f='${db:Status-Status}' build-essential 2>/dev/null | grep -qx installed

COPY --from=builder /opt/tts-venv /opt/tts-venv
COPY --from=builder /opt/tts-licenses /usr/share/licenses
COPY --from=builder /usr/include/python3.14 /usr/include/python3.14
COPY --from=builder /usr/include/aarch64-linux-gnu/python3.14 /usr/include/aarch64-linux-gnu/python3.14
ENV PATH=/opt/tts-venv/bin:$PATH \
CC=gcc-15 \
PYTHONUNBUFFERED=1

WORKDIR /app
COPY tts/server.py /app/dgx/tts/server.py
COPY tts/runtime.py /app/dgx/tts/runtime.py
COPY tts/profiles.py /app/dgx/tts/profiles.py
COPY tts/clone_runtime.py /app/dgx/tts/clone_runtime.py
COPY tts/api.py /app/dgx/tts/api.py

ENV PYTHONUNBUFFERED=1
EXPOSE 8002
CMD ["python3", "-m", "dgx.tts.server"]
6 changes: 6 additions & 0 deletions dgx/tts/apt-builder-packages-arm64.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
python3=3.14.3-0ubuntu2
python3-venv=3.14.3-0ubuntu2
python3-dev=3.14.3-0ubuntu2
build-essential=12.12ubuntu2.26.04.2
ninja-build=1.13.2-1
patch=2.8-2build1
39 changes: 0 additions & 39 deletions dgx/tts/apt-packages-arm64.lock

This file was deleted.

7 changes: 7 additions & 0 deletions dgx/tts/apt-runtime-packages-arm64.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
python3=3.14.3-0ubuntu2
gcc-15=15.2.0-16ubuntu1
libc6-dev=2.43-2ubuntu2.3
libsndfile1=1.2.2-4
libgomp1=16-20260322-1ubuntu1
sox=14.7.0.9+ds1-1
libsox-fmt-base=14.7.0.9+ds1-1
1 change: 1 addition & 0 deletions dgx/tts/flash-attn-arm64.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
flash-attn==2.8.3 --hash=sha256:1e71dd64a9e0280e0447b8a0c2541bad4bf6ac65bdeaa2f90e51a9e57de0370d
39 changes: 0 additions & 39 deletions dgx/tts/requirements-arm64.lock

This file was deleted.

14 changes: 14 additions & 0 deletions dgx/tts/requirements-slim-arm64.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--extra-index-url https://download.pytorch.org/whl/cu132

accelerate==1.12.0
einops==0.8.2
fastapi==0.135.3
huggingface-hub==0.36.2
librosa==0.11.0
onnxruntime==1.28.0
pydantic==2.12.5
soundfile==0.14.0
sox==1.5.0
torch==2.13.0+cu132
transformers==4.57.3
uvicorn[standard]==0.44.0
Loading