-
Notifications
You must be signed in to change notification settings - Fork 6.3k
fix(backend): prevent unbounded memory accumulation over consecutive TTS generations (#923) #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,6 +156,12 @@ async def run_generation( | |
| finally: | ||
| task_manager.complete_generation(generation_id) | ||
| bg_db.close() | ||
| 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 | ||
|
Comment on lines
+159
to
+164
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Make cleanup failures observable. Both finalizers catch Also applies to: 332-337 🤖 Prompt for AI Agents🩺 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.pyRepository: 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"))
PYRepository: 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"))
PYRepository: jamiepine/voicebox Length of output: 4989 Add a public
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def _notify_speak_end(generation_id: str, *, status: str) -> None: | ||
|
|
@@ -313,14 +319,22 @@ async def generate_audio_sync( | |
| if crossfade_ms is not None: | ||
| gen_kwargs["crossfade_ms"] = crossfade_ms | ||
|
|
||
| audio, sample_rate = await generate_chunked( | ||
| tts_model, text, voice_prompt, **gen_kwargs | ||
| ) | ||
| 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 | ||
|
Comment on lines
+322
to
+337
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 || trueRepository: 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}")
PYRepository: 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.pyRepository: jamiepine/voicebox Length of output: 11845 Extend the finalizer to cover model setup.
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def _save_regenerate( | ||
|
|
||
There was a problem hiding this comment.
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_cachereceives"cpu"and does not clear the active device cache.backend/backends/chatterbox_backend.py#L206-L221: add a publicdeviceproperty that returns_deviceor"cpu".backend/backends/chatterbox_turbo_backend.py#L187-L201: add the same publicdeviceproperty.Proposed fix
📍 Affects 2 files
backend/backends/chatterbox_backend.py#L206-L221(this comment)backend/backends/chatterbox_turbo_backend.py#L187-L201🤖 Prompt for AI Agents