Auto-unload idle models and evict on model switch across all workers
Every ASR/TTS worker (faster-whisper, qwen3, vibevoice, xtts) kept every model it ever loaded resident in GPU memory forever, and switching to a different model (e.g. a different whisper size) just added another one alongside it rather than freeing the old one. Combined with the shared 24GB GPU, this made memory pressure only ever go up. Now: only one model stays resident per worker at a time (loading a different model_id evicts the previous one first), and the whole cache (plus, for faster-whisper, the diarization pipeline) is dropped after 2 minutes of no requests. Verified end-to-end: the idle timer actually fires and reclaims memory, and switching qwen3 models evicts the old one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import gc
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
@@ -28,6 +31,13 @@ app = FastAPI(title="TTS XTTS Worker")
|
||||
MODEL_NAME = "tts_models/multilingual/multi-dataset/xtts_v2"
|
||||
_MODEL_CACHE: Dict[str, Any] = {}
|
||||
|
||||
# Idle-unload: the model is dropped after IDLE_UNLOAD_SECONDS of no synthesis
|
||||
# requests, so this backend doesn't permanently hog GPU memory shared with
|
||||
# the ASR workers.
|
||||
IDLE_UNLOAD_SECONDS = 120
|
||||
_last_used: float = 0.0
|
||||
_active_requests: int = 0
|
||||
|
||||
|
||||
def _log(msg: str) -> None:
|
||||
print(f"[xtts] {msg}", flush=True)
|
||||
@@ -41,6 +51,31 @@ def _device() -> str:
|
||||
return "cpu"
|
||||
|
||||
|
||||
def _free_gpu() -> None:
|
||||
gc.collect()
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _unload_model() -> None:
|
||||
if not _MODEL_CACHE:
|
||||
return
|
||||
_log("unloading model (idle)")
|
||||
_MODEL_CACHE.clear()
|
||||
_free_gpu()
|
||||
|
||||
|
||||
async def _idle_unload_loop() -> None:
|
||||
while True:
|
||||
await asyncio.sleep(30)
|
||||
if _active_requests == 0 and _last_used and (time.monotonic() - _last_used) >= IDLE_UNLOAD_SECONDS:
|
||||
_unload_model()
|
||||
|
||||
|
||||
def _load_model() -> Any:
|
||||
if MODEL_NAME in _MODEL_CACHE:
|
||||
return _MODEL_CACHE[MODEL_NAME]
|
||||
@@ -55,8 +90,9 @@ def _load_model() -> Any:
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup() -> None:
|
||||
async def startup() -> None:
|
||||
ensure_runtime_dirs()
|
||||
asyncio.create_task(_idle_unload_loop())
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
@@ -74,10 +110,14 @@ def synthesize(
|
||||
if not speaker_path.exists():
|
||||
raise HTTPException(status_code=400, detail=f"speaker_wav not found: {speaker_wav}")
|
||||
|
||||
global _active_requests, _last_used
|
||||
_active_requests += 1
|
||||
try:
|
||||
tts = _load_model()
|
||||
except Exception as e:
|
||||
_log(f"model load error: {type(e).__name__}: {e}")
|
||||
_active_requests -= 1
|
||||
_last_used = time.monotonic()
|
||||
raise HTTPException(status_code=500, detail=f"모델 로드 실패: {type(e).__name__}: {e}")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
|
||||
@@ -105,6 +145,9 @@ def synthesize(
|
||||
raise HTTPException(status_code=500, detail=f"{type(e).__name__}: {e}")
|
||||
finally:
|
||||
out_path.unlink(missing_ok=True)
|
||||
_free_gpu()
|
||||
_active_requests -= 1
|
||||
_last_used = time.monotonic()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user