Compare commits

..

6 Commits

Author SHA1 Message Date
e7a03b9f0f Switch build trigger from push to workflow_dispatch 2026-04-06 21:41:51 -04:00
66445fad84 Revert "Switch to ROCm 6.4 + Python 3.11 + PyTorch 2.9.1"
All checks were successful
Build ROCm Image / build (push) Successful in 45s
This reverts commit eb6a39d292.
2026-04-06 18:05:43 -04:00
eb6a39d292 Switch to ROCm 6.4 + Python 3.11 + PyTorch 2.9.1
Some checks failed
Build ROCm Image / build (push) Has been cancelled
- Base image: rocm/dev-ubuntu-22.04:6.4
- Python 3.10 → 3.11 via deadsnakes PPA
- torch/torchaudio: 2.5.1 → 2.9.1 (rocm6.4 wheel index)
- torchvision: 0.20.1 → 0.24.1
- pytorch_triton_rocm: 3.1.0 → 3.5.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:04:59 -04:00
f292ace76c Trigger rebuild to restore latest tag
All checks were successful
Build ROCm Image / build (push) Successful in 14m47s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 17:33:16 -04:00
967ed41239 Revert FP16 autocast — increases TTFA on first request
All checks were successful
Build ROCm Image / build (push) Successful in 3m21s
autocast triggers fp16 kernel selection at first call for each tensor
shape. Since the warmup uses short text, real requests re-trigger
selection and are slower net. Keeping FP32 + conditionals cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:30:49 -04:00
29b66e24bb Cache voice conditionals and add FP16 autocast
All checks were successful
Build ROCm Image / build (push) Successful in 3m17s
Voice conditionals (s3tokenizer + voice encoder + mel embeddings) are
expensive to compute but depend only on the reference audio, not the
text. Previously they ran on every synthesis chunk — 3x wasted work for
a 3-chunk request. Now computed once at startup and reused.

Also wrap generate() in torch.amp.autocast(float16) for ~2x speedup on
all model computation (T3 LLM, S3Gen CFM, HiFiGAN vocoder).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:22:13 -04:00
7 changed files with 67 additions and 43 deletions

View File

@@ -1,10 +1,7 @@
name: Build ROCm Image name: Build ROCm Image
on: on:
push: workflow_dispatch:
branches:
- main
- dev
jobs: jobs:
build: build:
@@ -23,21 +20,14 @@ jobs:
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set tags
id: tags
run: |
if [ "${{ gitea.ref_name }}" = "main" ]; then
echo "tags=git.sdgarren.com/scott/rocm-chatterbox-whisper:latest,git.sdgarren.com/scott/rocm-chatterbox-whisper:${{ gitea.sha }}" >> $GITHUB_OUTPUT
else
echo "tags=git.sdgarren.com/scott/rocm-chatterbox-whisper:${{ gitea.ref_name }},git.sdgarren.com/scott/rocm-chatterbox-whisper:${{ gitea.sha }}" >> $GITHUB_OUTPUT
fi
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: Dockerfile.rocm file: Dockerfile.rocm
push: true push: true
tags: ${{ steps.tags.outputs.tags }} tags: |
git.sdgarren.com/scott/rocm-chatterbox-whisper:latest
git.sdgarren.com/scott/rocm-chatterbox-whisper:${{ gitea.sha }}
cache-from: type=registry,ref=git.sdgarren.com/scott/rocm-chatterbox-whisper:latest cache-from: type=registry,ref=git.sdgarren.com/scott/rocm-chatterbox-whisper:latest
cache-to: type=inline cache-to: type=inline

View File

@@ -1,4 +1,4 @@
FROM rocm/dev-ubuntu-22.04:7.2 FROM rocm/dev-ubuntu-22.04:6.1
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
@@ -7,37 +7,27 @@ ENV DEBIAN_FRONTEND=noninteractive \
PIP_NO_CACHE_DIR=1 PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common \ python3 \
&& add-apt-repository ppa:deadsnakes/ppa \ python3-pip \
&& apt-get update && apt-get install -y --no-install-recommends \ python3-dev \
python3.11 \
python3.11-dev \
python3.11-distutils \
git \ git \
ffmpeg \ ffmpeg \
libsndfile1 \ libsndfile1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install pip for Python 3.11
RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11
# Make python3.11 the default python3
RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 \
&& update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1
WORKDIR /app WORKDIR /app
# Step 1: Install ROCm-compatible PyTorch stack first. # Step 1: Install ROCm-compatible PyTorch stack first.
# This must happen before anything else to prevent pip from pulling CPU wheels. # This must happen before anything else to prevent pip from pulling CPU wheels.
COPY requirements-rocm-init.txt . COPY requirements-rocm-init.txt .
RUN python3 -m pip install -r requirements-rocm-init.txt RUN pip3 install -r requirements-rocm-init.txt
# Step 2: Install remaining dependencies (pinned to avoid overwriting torch). # Step 2: Install remaining dependencies (pinned to avoid overwriting torch).
COPY requirements-rocm.txt . COPY requirements-rocm.txt .
RUN python3 -m pip install -r requirements-rocm.txt RUN pip3 install -r requirements-rocm.txt
# Step 3: Install chatterbox with --no-deps so pip cannot replace ROCm torch. # Step 3: Install chatterbox with --no-deps so pip cannot replace ROCm torch.
RUN python3 -m pip install --no-deps chatterbox-tts RUN pip3 install --no-deps chatterbox-tts
# Application source # Application source
COPY engine.py config.py wyoming_handler.py wyoming_voices.py main.py ./ COPY engine.py config.py wyoming_handler.py wyoming_voices.py main.py ./

View File

@@ -13,11 +13,11 @@ tts_engine:
generation_defaults: generation_defaults:
# Turbo model: uses temperature only (exaggeration/cfg_weight ignored) # Turbo model: uses temperature only (exaggeration/cfg_weight ignored)
# Standard model: uses exaggeration and cfg_weight (temperature ignored) # Standard model: uses exaggeration and cfg_weight (temperature ignored)
temperature: 0.9 temperature: 0.8
exaggeration: 0.5 exaggeration: 0.5
cfg_weight: 0.5 cfg_weight: 0.5
# seed: 0 = random each call, >0 = reproducible output # seed: 0 = random each call, >0 = reproducible output
seed: 1960 seed: 0
wyoming: wyoming:
host: "0.0.0.0" host: "0.0.0.0"

View File

@@ -8,6 +8,12 @@ chatterbox_model = None
_sample_rate = 24000 _sample_rate = 24000
_is_turbo = False _is_turbo = False
# Cache: voice file path → prepared conditionals object.
# prepare_conditionals loads audio, runs s3tokenizer + voice encoder, and
# builds mel embeddings — expensive work that only depends on the reference
# audio, not the text. Cache it so multi-chunk requests pay the cost once.
_cond_cache: dict = {}
def _test_cuda() -> bool: def _test_cuda() -> bool:
try: try:
@@ -51,6 +57,26 @@ def load_model() -> bool:
return False return False
def prepare_voice(audio_prompt_path: str) -> None:
"""
Pre-compute and cache the voice conditionals for a reference audio file.
Calling this once avoids repeating the s3tokenizer + voice encoder work
on every synthesis chunk that uses the same voice.
"""
if chatterbox_model is None:
return
if audio_prompt_path in _cond_cache:
return
if not _is_turbo:
return # only turbo exposes prepare_conditionals
logger.info(f"Preparing voice conditionals for '{audio_prompt_path}'")
with torch.inference_mode():
chatterbox_model.prepare_conditionals(audio_prompt_path)
_cond_cache[audio_prompt_path] = chatterbox_model.conds
logger.info("Voice conditionals cached")
def get_sample_rate() -> int: def get_sample_rate() -> int:
return _sample_rate return _sample_rate
@@ -71,8 +97,16 @@ def synthesize(
if torch.cuda.is_available(): if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed) torch.cuda.manual_seed_all(seed)
# Restore cached conditionals so generate() skips prepare_conditionals.
if audio_prompt_path and _is_turbo:
if audio_prompt_path not in _cond_cache:
prepare_voice(audio_prompt_path)
chatterbox_model.conds = _cond_cache[audio_prompt_path]
kwargs: dict = {} kwargs: dict = {}
if audio_prompt_path: # Don't pass audio_prompt_path — conds are already set above.
# For non-turbo models there's no cache, pass path as normal.
if audio_prompt_path and not _is_turbo:
kwargs["audio_prompt_path"] = audio_prompt_path kwargs["audio_prompt_path"] = audio_prompt_path
if _is_turbo: if _is_turbo:

12
main.py
View File

@@ -19,8 +19,18 @@ logger = logging.getLogger(__name__)
def _warmup(voices: dict) -> None: def _warmup(voices: dict) -> None:
"""Run one synthesis to populate MIOpen's in-memory kernel cache.""" """Pre-compute voice conditionals and populate MIOpen's kernel cache."""
from wyoming_voices import resolve_voice from wyoming_voices import resolve_voice
# Pre-compute conditionals for all discovered voices so the first real
# request doesn't pay the s3tokenizer + voice encoder cost.
for name, path in voices.items():
try:
engine.prepare_voice(path)
except Exception:
logger.warning(f"Failed to prepare voice '{name}' (non-fatal)", exc_info=True)
# Synthesis warmup to populate MIOpen's in-memory kernel cache.
audio_prompt = resolve_voice(None, voices) if voices else None audio_prompt = resolve_voice(None, voices) if voices else None
logger.info("Running warmup synthesis...") logger.info("Running warmup synthesis...")
try: try:

View File

@@ -1,5 +1,5 @@
--index-url https://download.pytorch.org/whl/rocm7.2 --index-url https://download.pytorch.org/whl/rocm6.1
torch==2.11.0 torch==2.5.1
torchaudio==2.11.0 torchaudio==2.5.1
torchvision==0.26.0 torchvision==0.20.1
pytorch_triton_rocm==3.3.0 pytorch_triton_rocm==3.1.0

View File

@@ -5,9 +5,9 @@ librosa==0.11.0
pyloudnorm pyloudnorm
# ML dependencies (pinned to match chatterbox without overwriting ROCm torch) # ML dependencies (pinned to match chatterbox without overwriting ROCm torch)
transformers>=4.50.0 transformers==4.46.3
diffusers>=0.32.0 diffusers==0.29.0
safetensors>=0.4.5 safetensors>=0.4.1
huggingface-hub huggingface-hub
omegaconf omegaconf