Skip to content

fix(backend): prevent unbounded memory accumulation over consecutive TTS generations (#923) - #1032

Open
devangkantharia wants to merge 1 commit into
jamiepine:mainfrom
devangkantharia:fix/memory-leak-cpu-backend-923
Open

fix(backend): prevent unbounded memory accumulation over consecutive TTS generations (#923)#1032
devangkantharia wants to merge 1 commit into
jamiepine:mainfrom
devangkantharia:fix/memory-leak-cpu-backend-923

Conversation

@devangkantharia

@devangkantharia devangkantharia commented Aug 9, 2026

Copy link
Copy Markdown

Summary & Context

Fixes #923 where voicebox-server memory accumulated continuously over consecutive speech synthesis runs on CPU (as well as GPU/MPS) backends. On long texts or consecutive generation requests, memory would grow step-wise (+120 MB to +250 MB per run), eventually causing RAM inflation and stalling subsequent queue items in generating state.


Root Cause Analysis

  1. Autograd Activation Graph Retention: Model forward inference was invoked without torch.inference_mode() or torch.no_grad() contexts across TTS engine backends. PyTorch retained computation graphs and intermediate activation tensors in RAM across consecutive synthesis passes.
  2. Missing Post-Generation Cleanup: Unreferenced audio arrays and spectrogram buffers were not collected after individual synthesis jobs completed; cache clearing was only performed during full model unloads.
  3. Incomplete Allocator Flushing: empty_device_cache() lacked explicit Python garbage collection (gc.collect()) for CPU memory reclamation and did not flush Apple Silicon MPS pools.

Proposed Changes

1. PyTorch Inference Mode Guards

Wrapped all forward synthesis calls in with torch.inference_mode(): across engine drivers:

  • backend/backends/kokoro_backend.py
  • backend/backends/qwen_custom_voice_backend.py
  • backend/backends/pytorch_backend.py
  • backend/backends/luxtts_backend.py
  • backend/backends/chatterbox_backend.py
  • backend/backends/chatterbox_turbo_backend.py

2. Mandatory Post-Generation Reclaim Hooks

Added empty_device_cache(device) invocations in the finally blocks of run_generation() and generate_audio_sync() within backend/services/generation.py.

3. Comprehensive Device & Host Cache Clearing

Updated empty_device_cache(device) in backend/backends/base.py to trigger gc.collect() across all platforms and added torch.mps.empty_cache() support for Apple Silicon.


Empirical Benchmark & Verification Results

Test Environment & Cross-Platform Roadmap

  • Tested Environment: Windows 11 Home (64-bit), CPU mode (CUDA_VISIBLE_DEVICES="-1"), Kokoro 82M engine (Heart female preset voice).
  • Upcoming Verification: Testing will further continue on Linux environments.
  • Community Testing: Simultaneous verification testing by macOS / Apple Silicon (MPS) device owners is highly welcomed and encouraged!

Benchmark 1: 10-Run Baseline Test (97 Words / 685 Characters)

Run Status Time Worker RAM Delta Total Growth
Run 1 Completed 24.6s 1962.20 MB +1176.63 MB (Model Load) Baseline
Run 2 Completed 14.3s 1958.38 MB -3.82 MB -3.82 MB
Run 3 Completed 14.3s 1988.52 MB +30.14 MB +26.32 MB
Run 4 Completed 15.3s 1964.56 MB -23.96 MB +2.36 MB
Run 5 Completed 15.3s 1962.56 MB -2.00 MB +0.36 MB
Run 6 Completed 15.3s 1959.17 MB -3.39 MB -3.03 MB
Run 7 Completed 15.3s 1984.88 MB +25.71 MB +22.68 MB
Run 8 Completed 14.3s 1983.71 MB -1.17 MB +21.51 MB
Run 9 Completed 16.3s 1952.79 MB -30.92 MB -9.41 MB
Run 10 Completed 16.3s 1954.48 MB +1.69 MB -7.72 MB

Benchmark 2: Exhaustive 7-Run Stress Test (195 Words / 1,533 Characters Passage)

Run Status Time Worker RAM Run-to-Run Delta Net Fluctuation
Run 1 Completed 42.9s 1970.29 MB Baseline (Inference Alloc)
Run 2 Completed 33.7s 2026.25 MB +55.96 MB Warm-up peak
Run 3 Completed 37.8s 2003.23 MB -23.02 MB Reclaimed
Run 4 Completed 37.8s 2004.92 MB +1.69 MB Stable
Run 5 Completed 34.8s 1992.73 MB -12.19 MB Reclaimed
Run 6 Completed 33.6s 2017.71 MB +24.98 MB Stable
Run 7 Completed 33.6s 2011.77 MB -5.94 MB -14.48 MB vs Run 2

Key Observations:

  1. Unbounded Accumulation Resolved: Before the fix, memory increased monotonically by +120 MB to +250 MB per run. With the fix, memory remains strictly bounded within a narrow 1992 MB – 2026 MB window across 7 consecutive large-text passes.
  2. Active Garbage Collection: Negative deltas on Runs 3, 5, and 7 confirm that unreferenced arrays and tensors are actively reclaimed between passes.
  3. Zero Queue Degradation: All generations completed cleanly without latency degradation or stall conditions.

PowerShell Reproduction & Testing Snippet

I used the following PowerShell script to verify this fix against a running dev server (http://localhost:17493):

$prof = (Invoke-RestMethod -Uri "http://localhost:17493/profiles") | Where-Object { $_.default_engine -eq "kokoro" -or $_.name -like "*Kokoro*" } | Select-Object -First 1
if (-not $prof) { $prof = (Invoke-RestMethod -Uri "http://localhost:17493/profiles")[0] }

$hugeText = @"
Voicebox is a modern, open-source desktop speech synthesis application and local voice cloning system engineered with Tauri, React, and Python FastAPI. It empowers creators, developers, and voice enthusiasts to generate natural-sounding speech locally on their own hardware without relying on cloud APIs or subscription services. Voicebox features modular support for cutting-edge text-to-speech architectures including Qwen3-TTS, Kokoro 82M, LuxTTS, Chatterbox, and HumeAI TADA.

When performing consecutive speech synthesis tasks on CPU architectures, resource management becomes paramount. Neural network backends process tokenized text inputs through multi-head self-attention mechanisms and recurrent decoder blocks to generate high-resolution audio spectrograms and raw audio waveforms. Without strict control over PyTorch autograd computation graphs, intermediate activation tensors, and device memory pools, consecutive generation passes can retain unreferenced allocation blocks in the process heap. This accumulation leads to progressive memory inflation and queue stall conditions.

By wrapping neural inference inside strict PyTorch inference mode context managers and invoking explicit host and accelerator cache clearing routines upon every task completion, Voicebox ensures that intermediate memory blocks are reclaimed immediately after synthesis. This guarantees that process memory remains flat, predictable, and bounded across dozens of consecutive generation requests regardless of input length or audio duration.
"@

$mainPid = (Get-NetTCPConnection -LocalPort 17493 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess
$initialMem = $null

1..7 | ForEach-Object {
    $loopNum = $_
    $allPy = Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' }
    $workerBefore = $allPy | Where-Object { $_.ParentProcessId -eq $mainPid -or $_.ProcessId -eq $mainPid } | Sort-Object WorkingSetSize -Descending | Select-Object -First 1
    $memBefore = [math]::Round($workerBefore.WorkingSetSize / 1MB, 2)
    if ($null -eq $initialMem) { $initialMem = $memBefore }

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $body = @{
        profile_id = $prof.id
        engine = if ($prof.default_engine) { $prof.default_engine } else { "kokoro" }
        text = $hugeText
    } | ConvertTo-Json

    $res = Invoke-RestMethod -Uri "http://localhost:17493/generate" -Method Post -ContentType "application/json" -Body $body
    
    do {
        Start-Sleep -Seconds 1
        $st = (Invoke-RestMethod -Uri "http://localhost:17493/history/$($res.id)").status
    } while ($st -eq "generating" -or $st -eq "queued" -or $st -eq "loading_model")
    $sw.Stop()

    $allPy = Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' }
    $workerAfter = $allPy | Where-Object { $_.ParentProcessId -eq $mainPid -or $_.ProcessId -eq $mainPid } | Sort-Object WorkingSetSize -Descending | Select-Object -First 1
    $memAfter = [math]::Round($workerAfter.WorkingSetSize / 1MB, 2)
    $delta = [math]::Round($memAfter - $memBefore, 2)
    $totalGrowth = [math]::Round($memAfter - $initialMem, 2)
    $elapsedSec = $sw.Elapsed.TotalSeconds.ToString("F1")

    Write-Host "Run $loopNum | Time: ${elapsedSec}s | After: ${memAfter} MB (Delta: +${delta} MB | Total Growth: +${totalGrowth} MB)"
}






<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **Performance**
  * Improved audio generation efficiency and memory usage across supported speech synthesis engines.
  * Added automatic cleanup after audio generation to help release unused device memory.

* **Bug Fixes**
  * Improved memory cleanup for CPU, CUDA, XPU, and MPS devices.
  * Reduced the risk of retained memory after unloading models or completing generation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change runs TTS inference under torch.inference_mode(). It expands device-cache cleanup to include garbage collection and MPS. Asynchronous and synchronous generation paths now perform best-effort cleanup during finalization.

Changes

Generation memory cleanup

Layer / File(s) Summary
Inference-mode generation
backend/backends/chatterbox_backend.py, backend/backends/chatterbox_turbo_backend.py, backend/backends/kokoro_backend.py, backend/backends/luxtts_backend.py, backend/backends/pytorch_backend.py, backend/backends/qwen_custom_voice_backend.py
TTS generation and waveform conversion now run inside torch.inference_mode() while preserving existing arguments and output handling.
Device-cache cleanup contract
backend/backends/base.py
empty_device_cache now runs garbage collection and supports CUDA, XPU, MPS, and CPU cleanup.
Generation finalization cleanup
backend/services/generation.py
Asynchronous and synchronous generation paths now clear the TTS device cache during finalization. Cleanup errors are suppressed.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GenerationService
  participant TTSBackend
  participant DeviceCache
  GenerationService->>TTSBackend: Generate audio in inference mode
  TTSBackend-->>GenerationService: Return waveform
  GenerationService->>DeviceCache: Clear device cache during finalization
  DeviceCache-->>GenerationService: Suppress cleanup errors
Loading

Possibly related issues

  • Issue 905: Both changes update PyTorch/MPS inference and device-cache cleanup behavior.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary fix for unbounded memory accumulation during consecutive TTS generations.
Linked Issues check ✅ Passed The changes address issue [#923] by bounding memory through inference mode, garbage collection, and device-cache cleanup after generation.
Out of Scope Changes check ✅ Passed All changed files support the stated memory-management fix across TTS backends and remain within issue [#923] scope.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
backend/backends/base.py (2)

186-187: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Verify the XPU availability guard.

hasattr(torch, "xpu") only confirms that the namespace exists. PyTorch exposes torch.xpu.is_available() as the runtime check and documents XPU as lazily importable. (docs.pytorch.org) If "xpu" reaches this helper while no XPU is usable, the code still calls torch.xpu.empty_cache(). Add an availability check and an empty_cache method check, or enforce that callers pass only an available device.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/backends/base.py` around lines 186 - 187, Update the XPU branch in
the device-cache helper to verify torch.xpu is runtime-available via
is_available() before clearing the cache, and also confirm empty_cache is
callable before invoking it; do not rely on hasattr(torch, "xpu") alone.

182-190: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Verify that cleanup does not block the async event loop.

gc.collect() and the device-cache calls execute synchronously. run_generation and generate_audio_sync call this helper from async def functions. A full collection can delay queued jobs and the synchronous /profiles/{id}/speak response. Offload cleanup to a worker thread or dedicated executor. Keep task_manager.complete_generation after cleanup if it releases queue capacity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/backends/base.py` around lines 182 - 190, Update the cleanup helper
containing gc.collect and device cache calls so all synchronous cleanup work
runs in a worker thread or dedicated executor when invoked by async
run_generation and generate_audio_sync flows. Preserve the existing
device-specific cleanup behavior, and ensure task_manager.complete_generation
remains after cleanup so queue capacity is released only once cleanup finishes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/backends/chatterbox_backend.py`:
- Around line 206-221: Add a public device property to the Chatterbox backend
class in backend/backends/chatterbox_backend.py at lines 206-221, returning
_device when set and "cpu" otherwise. Add the same property to the corresponding
Chatterbox Turbo backend class in backend/backends/chatterbox_turbo_backend.py
at lines 187-201 so tts_model.device exposes the selected device for cache
cleanup.

In `@backend/services/generation.py`:
- Around line 159-164: Update both cleanup finalizers around empty_device_cache
to record cleanup failures instead of silently passing, including the engine and
resolved device in a warning or metric. Preserve the original generation
exception and successful generation result while ensuring cleanup observability
does not replace or re-raise the primary error.
- Around line 159-164: Add a public device property to both ChatterboxTTSBackend
and ChatterboxTurboTTSBackend that returns their configured _device value, so
generation.py finalizer cleanup receives the selected device instead of
defaulting to CPU.
- Around line 322-337: The generate_audio_sync flow currently starts its
finalizer after backend/model/prompt setup, so setup failures bypass
empty_device_cache. Move get_tts_backend_for_engine, load_engine_model, and
create_voice_prompt_for_profile into the same outer try/finally as
generate_chunked, while preserving bg_db.close() in its existing nested cleanup.

---

Nitpick comments:
In `@backend/backends/base.py`:
- Around line 186-187: Update the XPU branch in the device-cache helper to
verify torch.xpu is runtime-available via is_available() before clearing the
cache, and also confirm empty_cache is callable before invoking it; do not rely
on hasattr(torch, "xpu") alone.
- Around line 182-190: Update the cleanup helper containing gc.collect and
device cache calls so all synchronous cleanup work runs in a worker thread or
dedicated executor when invoked by async run_generation and generate_audio_sync
flows. Preserve the existing device-specific cleanup behavior, and ensure
task_manager.complete_generation remains after cleanup so queue capacity is
released only once cleanup finishes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d5514121-3c18-4848-8245-37d7c47d2dfc

📥 Commits

Reviewing files that changed from the base of the PR and between 51f49de and a819cae.

📒 Files selected for processing (8)
  • backend/backends/base.py
  • backend/backends/chatterbox_backend.py
  • backend/backends/chatterbox_turbo_backend.py
  • backend/backends/kokoro_backend.py
  • backend/backends/luxtts_backend.py
  • backend/backends/pytorch_backend.py
  • backend/backends/qwen_custom_voice_backend.py
  • backend/services/generation.py

Comment on lines +206 to +221
with torch.inference_mode():
wav = self.model.generate(
text,
language_id=language,
audio_prompt_path=ref_audio,
exaggeration=lang_defaults["exaggeration"],
cfg_weight=lang_defaults["cfg_weight"],
temperature=lang_defaults["temperature"],
repetition_penalty=lang_defaults["repetition_penalty"],
)

# Convert tensor -> numpy
if isinstance(wav, torch.Tensor):
audio = wav.squeeze().cpu().numpy().astype(np.float32)
else:
audio = np.asarray(wav, dtype=np.float32)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Expose the selected device for both Chatterbox backends.

The generation finalizer reads tts_model.device, but both backends store the selected device only in _device. On CUDA or XPU, empty_device_cache receives "cpu" and does not clear the active device cache.

  • backend/backends/chatterbox_backend.py#L206-L221: add a public device property that returns _device or "cpu".
  • backend/backends/chatterbox_turbo_backend.py#L187-L201: add the same public device property.
Proposed fix
 class ChatterboxTTSBackend:
+    `@property`
+    def device(self) -> str:
+        return self._device or "cpu"

 class ChatterboxTurboTTSBackend:
+    `@property`
+    def device(self) -> str:
+        return self._device or "cpu"
📍 Affects 2 files
  • backend/backends/chatterbox_backend.py#L206-L221 (this comment)
  • backend/backends/chatterbox_turbo_backend.py#L187-L201
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/backends/chatterbox_backend.py` around lines 206 - 221, Add a public
device property to the Chatterbox backend class in
backend/backends/chatterbox_backend.py at lines 206-221, returning _device when
set and "cpu" otherwise. Add the same property to the corresponding Chatterbox
Turbo backend class in backend/backends/chatterbox_turbo_backend.py at lines
187-201 so tts_model.device exposes the selected device for cache cleanup.

Comment on lines +159 to +164
try:
from ..backends.base import empty_device_cache
device = getattr(tts_model, "device", "cpu") if "tts_model" in locals() else "cpu"
empty_device_cache(device)
except Exception:
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make cleanup failures observable.

Both finalizers catch Exception and execute pass. A successful generation can therefore be reported as complete even when cache cleanup failed. Log a warning or metric with the engine and device while preserving the original generation error.

Also applies to: 332-337

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/generation.py` around lines 159 - 164, Update both cleanup
finalizers around empty_device_cache to record cleanup failures instead of
silently passing, including the engine and resolved device in a warning or
metric. Preserve the original generation exception and successful generation
result while ensuring cleanup observability does not replace or re-raise the
primary error.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'def device|self\._device|device = getattr' \
  backend/backends/chatterbox_backend.py \
  backend/backends/chatterbox_turbo_backend.py \
  backend/backends/luxtts_backend.py \
  backend/backends/kokoro_backend.py \
  backend/services/generation.py

Repository: jamiepine/voicebox

Length of output: 10865


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generation.py outlines =="
ast-grep outline backend/services/generation.py --view compact || true

echo
echo "== generation.py relevant sections =="
sed -n '110,175p' backend/services/generation.py
sed -n '300,345p' backend/services/generation.py

echo
echo "== backend device attributes =="
python3 - <<'PY'
import ast
from pathlib import Path

files = [
    Path("backend/backends/chatterbox_backend.py"),
    Path("backend/backends/chatterbox_turbo_backend.py"),
    Path("backend/backends/luxtts_backend.py"),
    Path("backend/backends/kokoro_backend.py"),
]
for path in files:
    tree = ast.parse(path.read_text())
    classes = [n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
    print(f"--- {path} classes: {[c.name for c in classes]}")
    for cls in classes:
        names = []
        assigns = []
        for node in ast.walk(cls):
            if isinstance(node, ast.FunctionDef) and node.name == "device":
                names.append(f"def {node.name}")
            if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == "self":
                names.append(node.attr)
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == "self":
                        assigns.append(target.attr)
        print("  ", path.name, cls.name, sorted(set(names + assigns)))
        
print("== getattr None behavior ==")
class C:
    pass
print(genattr(C()), getattr(C(), "device", "cpu"))
print(genattr(C), getattr(object(), "device", "cpu"))
PY

Repository: jamiepine/voicebox

Length of output: 4989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generation.py outlines =="
ast-grep outline backend/services/generation.py --view compact || true

echo
echo "== generation.py relevant sections =="
sed -n '110,175p' backend/services/generation.py
sed -n '300,345p' backend/services/generation.py

echo
echo "== backend device attributes =="
python3 - <<'PY'
import ast
from pathlib import Path

files = [
    Path("backend/backends/chatterbox_backend.py"),
    Path("backend/backends/chatterbox_turbo_backend.py"),
    Path("backend/backends/luxtts_backend.py"),
    Path("backend/backends/kokoro_backend.py"),
]
for path in files:
    tree = ast.parse(path.read_text())
    classes = [n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]
    print(f"--- {path} classes: {[c.name for c in classes]}")
    for cls in classes:
        names = []
        assigns = []
        for node in ast.walk(cls):
            if isinstance(node, ast.FunctionDef) and node.name == "device":
                names.append(f"def {node.name}")
            if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == "self":
                names.append(node.attr)
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and target.value.id == "self":
                        assigns.append(target.attr)
        print("  ", path.name, cls.name, sorted(set(names + assigns)))
        
print("== getattr None behavior ==")
class C:
    pass
print(genattr(C()), getattr(C(), "device", "cpu"))
print(genattr(C), getattr(C), getattr(object(), "device", "cpu"))
PY

Repository: jamiepine/voicebox

Length of output: 4989


Add a public device property to the Chatterbox backends.

backend/services/generation.py reads tts_model.device in its finalizers; LuxTTS and Kokoro expose it, but ChatterboxTTSBackend and ChatterboxTurboTTSBackend only set _device. That defaults Chatterbox generation cleanup to CPU and can skip the selected device cache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/generation.py` around lines 159 - 164, Add a public device
property to both ChatterboxTTSBackend and ChatterboxTurboTTSBackend that returns
their configured _device value, so generation.py finalizer cleanup receives the
selected device instead of defaulting to CPU.

Comment on lines +322 to +337
try:
audio, sample_rate = await generate_chunked(
tts_model, text, voice_prompt, **gen_kwargs
)

if normalize:
audio = normalize_audio(audio)
if normalize:
audio = normalize_audio(audio)

return tts.audio_to_wav_bytes(audio, sample_rate)
return tts.audio_to_wav_bytes(audio, sample_rate)
finally:
try:
from ..backends.base import empty_device_cache
device = getattr(tts_model, "device", "cpu") if "tts_model" in locals() else "cpu"
empty_device_cache(device)
except Exception:
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate generation.py =="
fd -a 'generation\.py$' . || true

echo "== file stats =="
wc -l backend/services/generation.py 2>/dev/null || true

echo "== relevant generation.py lines 260-360 =="
sed -n '260,360p' backend/services/generation.py 2>/dev/null | nl -ba -v260

echo "== search for empty_device_cache definitions/usages =="
rg -n "empty_device_cache|load_engine_model|get_tts_backend_for_engine|create_voice_prompt_for_profile|run_generation" backend/services/generation.py backend -S || true

echo "== outline generation.py =="
ast-grep outline backend/services/generation.py --view compact 2>/dev/null || true

Repository: jamiepine/voicebox

Length of output: 434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant generation.py lines 260-360 =="
sed -n '260,360p' backend/services/generation.py

echo "== search for empty_device_cache definitions/usages and setup =="
rg -n "empty_device_cache|load_engine_model|get_tts_backend_for_engine|create_voice_prompt_for_profile|run_generation|class .*Generation|def .*generation" backend/services/generation.py backend -S || true

echo "== control-flow summary around finalizers =="
python3 - <<'PY'
from pathlib import Path
p = Path("backend/services/generation.py")
text = p.read_text().splitlines()
for i, line in enumerate(text, 1):
    if "def " in line or "try:" in line or "finally:" in line or "empty_device_cache" in line or "load_engine_model" in line or "get_tts_backend_for_engine" in line or "create_voice_prompt_for_profile" in line or "return tts.audio_to_wav_bytes" in line or "bg_db.close()" in line:
        print(f"{i:04d}: {line}")
PY

Repository: jamiepine/voicebox

Length of output: 17283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== run_generation try/finalizer context =="
sed -n '54,165p' backend/services/generation.py

echo "== load engine model and backend setup snippets =="
sed -n '520,675p' backend/backends/__init__.py

echo "== empty_device_cache context =="
sed -n '150,190p' backend/backends/base.py

Repository: jamiepine/voicebox

Length of output: 11845


Extend the finalizer to cover model setup.

generate_audio_sync starts the finalizer only after get_tts_backend_for_engine, load_engine_model, and create_voice_prompt_for_profile. If setup raises after allocating tensors, this synchronous path skips empty_device_cache, while run_generation already includes setup and cleanup in one try/finally. Wrap setup and generation together, and keep bg_db.close() in the nested cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/services/generation.py` around lines 322 - 337, The
generate_audio_sync flow currently starts its finalizer after
backend/model/prompt setup, so setup failures bypass empty_device_cache. Move
get_tts_backend_for_engine, load_engine_model, and
create_voice_prompt_for_profile into the same outer try/finally as
generate_chunked, while preserving bg_db.close() in its existing nested cleanup.

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.

voicebox-server memory grows ~unbounded over consecutive generations (CPU backend); once large, new generations hang in "generating" forever

1 participant