Skip to content

Bound the voice conditioning cache to stop CUDA OOM after switching voices - #170

Open
j2deen wants to merge 1 commit into
devnen:mainfrom
j2deen:main
Open

j2deen wants to merge 1 commit into
devnen:mainfrom
j2deen:main

Conversation

@j2deen

@j2deen j2deen commented Sep 8, 2026

Copy link
Copy Markdown

Problem

The voice conditioning cache introduced in 9d0b139 (_conds_cache in engine.py) is unbounded and holds GPU-resident tensors for every distinct voice ever used. On a long-running server each new predefined voice or clone permanently pins roughly 200-300 MiB of VRAM. After a few dozen voices the GPU is exhausted and every request fails with:

TTS engine failed to synthesize audio for chunk 1.
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 20.00 MiB. GPU 0 has a total capacity of 11.63 GiB of which 1.62 MiB is free.

Measured on a 12 GiB card with the current main, short text, one request per voice:

Request Process VRAM
Idle after load 4.8 GiB
Same voice x4 5.2 GiB (flat)
8 different voices 6.8 GiB (+~220 MiB each, never released)

This is the same user-facing symptom reported in #27, though that issue predates the cache so it may have a different cause.

Fix

  • engine.py: the cache is now an LRU bounded by a new tts_engine.voice_cache_size config key (default 8, 0 disables caching). Entries are stored on the CPU and copied to the device on a hit, so the GPU only ever holds the active voice. Because both Conditionals.to() and T3Cond.to() mutate in place, the copy shallow-clones the containers first. After a cache miss the freed allocator blocks are handed back with torch.cuda.empty_cache(). A clear_voice_cache() helper is added for anyone who wants to wire it to the unload endpoint.
  • config.py: default for voice_cache_size.
  • docker-compose.yml: sets PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True, as recommended by the OOM message, to reduce fragmentation on long-running servers.
  • README.md / documentation.md: document the new key.

Verification

Rebuilt the container and swept 31 distinct predefined voices plus 3 clones through /v1/audio/speech and /tts, watching nvidia-smi after each request. VRAM climbs while the cache fills to 8 entries, then stays flat:

Stage Process VRAM
Idle after load 4.3 GiB
After 8 distinct voices 6.4 GiB
After 31 distinct voices + 3 clones 6.4 GiB

Cache hits still skip prepare_conditionals, so repeat-voice latency is unchanged (~1.0-1.4 s for a short sentence on this card).

🤖 Generated with Claude Code

https://claude.ai/code/session_01AGsQBKMnHzENiqR5oRQrSy

The voice conditioning cache added with the bf16 work was unbounded and
kept GPU tensors alive for every distinct voice ever used. Each new voice
pinned roughly 200-300 MiB of VRAM, so a server that cycled through a few
dozen predefined voices and clones exhausted a 12 GiB GPU and every request
failed with "TTS engine failed to synthesize audio for chunk 1".

- engine.py: make the cache an LRU bounded by tts_engine.voice_cache_size
  (default 8, 0 disables), store entries on CPU and copy to the device on a
  hit, evict the oldest beyond the limit, and release CUDA allocator blocks
  after each newly encoded voice. Add clear_voice_cache().
- config.py: add tts_engine.voice_cache_size default.
- docker-compose.yml: set PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True.
- docs: document the new key.

Verified on an RTX-class 12 GiB card: VRAM climbs while the cache fills,
then holds flat at ~6.4 GiB across 31 distinct voices and 3 clones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGsQBKMnHzENiqR5oRQrSy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant