修复直播测试与 TTS 预热
This commit is contained in:
+112
-18
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import io
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
@@ -13,13 +14,14 @@ from typing import Any, Callable
|
||||
|
||||
MAX_NEW_TOKENS = 384
|
||||
DEFAULT_SYNTHESIS_TIMEOUT_SECONDS = 120.0
|
||||
DEFAULT_STARTUP_TIMEOUT_SECONDS = 480.0
|
||||
DEFAULT_STARTUP_TIMEOUT_SECONDS = 900.0
|
||||
# 启动失败后再次拉起 worker 的最小间隔:避免"启动超时→立即重启→再超时"的死循环
|
||||
# 在系统高负载时持续加载 torch/CUDA,进一步加剧卡顿。
|
||||
STARTUP_FAILURE_BACKOFF_SECONDS = 60.0
|
||||
DEFAULT_CPU_THREADS = 4
|
||||
DEFAULT_CPU_AFFINITY_COUNT = 8
|
||||
DEFAULT_PROCESS_PRIORITY = "below_normal"
|
||||
STARTUP_PROGRESS_POLL_SECONDS = 1.0
|
||||
|
||||
|
||||
class FasterQwenWorkerError(RuntimeError):
|
||||
@@ -122,15 +124,26 @@ def _apply_worker_process_limits(settings: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _load_runtime(settings: dict[str, Any]) -> dict[str, Any]:
|
||||
def _load_runtime(
|
||||
settings: dict[str, Any],
|
||||
progress: Callable[[str], None] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
def report(stage: str) -> None:
|
||||
if progress:
|
||||
progress(stage)
|
||||
|
||||
device = str(settings.get("device") or "cuda")
|
||||
if device == "cpu" and "CUDA_VISIBLE_DEVICES" not in os.environ:
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
||||
|
||||
cpu_threads = _configure_worker_environment(settings)
|
||||
report("torch_import_started")
|
||||
import torch
|
||||
report("torch_import_complete")
|
||||
_configure_torch_threads(torch, cpu_threads)
|
||||
report("tts_library_import_started")
|
||||
from faster_qwen3_tts import FasterQwen3TTS
|
||||
report("tts_library_import_complete")
|
||||
|
||||
if device == "cpu":
|
||||
torch.cuda.is_available = lambda: False
|
||||
@@ -138,10 +151,12 @@ def _load_runtime(settings: dict[str, Any]) -> dict[str, Any]:
|
||||
load_kwargs: dict[str, Any] = {}
|
||||
if device == "cpu":
|
||||
load_kwargs["device"] = "cpu"
|
||||
report("from_pretrained_started")
|
||||
model = FasterQwen3TTS.from_pretrained(
|
||||
str(settings.get("model_name_or_path") or "Qwen/Qwen3-TTS-12Hz-0.6B-Base"),
|
||||
**load_kwargs,
|
||||
)
|
||||
report("from_pretrained_complete")
|
||||
|
||||
# voice_clone_prompt 不再预计算:当前 faster_qwen3_tts 的 FasterQwen3TTS 没有
|
||||
# create_voice_clone_prompt 方法,预计算只会失败。改为每次合成时在
|
||||
@@ -193,21 +208,63 @@ def _synthesize_wav(runtime: dict[str, Any], text: str) -> bytes:
|
||||
|
||||
|
||||
def faster_qwen_worker_main(connection: Connection, settings: dict[str, Any]) -> None:
|
||||
def report(stage: str, **details: Any) -> None:
|
||||
try:
|
||||
connection.send({
|
||||
"type": "progress",
|
||||
"stage": stage,
|
||||
"pid": os.getpid(),
|
||||
**details,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
report("process_started", executable=sys.executable, prefix=sys.prefix)
|
||||
cpu_threads = _configure_worker_environment(settings)
|
||||
process_limits = _apply_worker_process_limits(settings)
|
||||
report("environment_ready", cpu_threads=cpu_threads)
|
||||
load_started = time.monotonic()
|
||||
runtime = _load_runtime(settings)
|
||||
report("model_loading", model_name=str(settings.get("model_name_or_path") or ""))
|
||||
runtime = _load_runtime(settings, progress=report)
|
||||
load_ms = int((time.monotonic() - load_started) * 1000)
|
||||
report("model_loaded", load_ms=load_ms)
|
||||
|
||||
warmup_started = time.monotonic()
|
||||
graph_warmup_ms = 0
|
||||
report("warmup_started")
|
||||
warmup = getattr(runtime["model"], "warmup", None)
|
||||
if callable(warmup):
|
||||
graph_warmup_started = time.monotonic()
|
||||
warmup()
|
||||
graph_warmup_ms = int((time.monotonic() - graph_warmup_started) * 1000)
|
||||
report("graph_warmup_complete", graph_warmup_ms=graph_warmup_ms)
|
||||
|
||||
# model.warmup() 只捕获模型内部 CUDA Graph,不会初始化参考音频和
|
||||
# voice-clone 生成路径。若把这一步留到首条播报,后台会在近一分钟内
|
||||
# 看起来毫无响应,因此在降低进程优先级之前完成一次真实短句合成。
|
||||
voice_clone_warmup_started = time.monotonic()
|
||||
report("voice_clone_warmup_started")
|
||||
_synthesize_wav(runtime, "系统启动")
|
||||
voice_clone_warmup_ms = int((time.monotonic() - voice_clone_warmup_started) * 1000)
|
||||
report("voice_clone_warmup_complete", voice_clone_warmup_ms=voice_clone_warmup_ms)
|
||||
warmup_ms = int((time.monotonic() - warmup_started) * 1000)
|
||||
report(
|
||||
"warmup_complete",
|
||||
warmup_ms=warmup_ms,
|
||||
graph_warmup_ms=graph_warmup_ms,
|
||||
voice_clone_warmup_ms=voice_clone_warmup_ms,
|
||||
)
|
||||
|
||||
# 加载和 CUDA Graph 捕获需要完整 CPU 调度能力。仅在预热完成后降低
|
||||
# worker 优先级和亲和性,避免繁忙直播环境下启动时间被放大到超时。
|
||||
process_limits = _apply_worker_process_limits(settings)
|
||||
connection.send({
|
||||
"type": "ready",
|
||||
"pid": os.getpid(),
|
||||
"load_ms": load_ms,
|
||||
"warmup_ms": warmup_ms,
|
||||
"graph_warmup_ms": graph_warmup_ms,
|
||||
"voice_clone_warmup_ms": voice_clone_warmup_ms,
|
||||
"max_new_tokens": MAX_NEW_TOKENS,
|
||||
"cpu_threads": cpu_threads,
|
||||
**process_limits,
|
||||
@@ -278,7 +335,7 @@ class FasterQwenWorkerClient:
|
||||
self.settings = dict(settings)
|
||||
self.logger = logger
|
||||
self.synthesis_timeout_seconds = max(1.0, float(synthesis_timeout_seconds))
|
||||
self.startup_timeout_seconds = max(10.0, float(startup_timeout_seconds))
|
||||
self.startup_timeout_seconds = max(0.1, float(startup_timeout_seconds))
|
||||
self._context = context or multiprocessing.get_context("spawn")
|
||||
self._process_target = process_target or faster_qwen_worker_main
|
||||
self._lock = threading.RLock()
|
||||
@@ -330,21 +387,55 @@ class FasterQwenWorkerClient:
|
||||
self._worker_pid = int(getattr(process, "pid", 0) or 0)
|
||||
self._log("info", "[FasterQwenTTS] worker 已启动, pid=%s,正在加载和预热", self.worker_pid)
|
||||
|
||||
if not parent_connection.poll(self.startup_timeout_seconds):
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
self._terminate_worker_locked("startup_timeout", graceful=False)
|
||||
raise FasterQwenWorkerError(
|
||||
f"Faster-Qwen3-TTS worker startup exceeded {self.startup_timeout_seconds:.0f}s"
|
||||
)
|
||||
try:
|
||||
message = parent_connection.recv()
|
||||
except (EOFError, OSError) as exc:
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
self._terminate_worker_locked("startup_connection_closed", graceful=False)
|
||||
raise FasterQwenWorkerError("Faster-Qwen3-TTS worker exited during startup") from exc
|
||||
started = time.monotonic()
|
||||
deadline = started + self.startup_timeout_seconds
|
||||
last_stage = "process_spawned"
|
||||
message: Any = None
|
||||
while True:
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
self._terminate_worker_locked("startup_timeout", graceful=False)
|
||||
raise FasterQwenWorkerError(
|
||||
f"Faster-Qwen3-TTS worker startup exceeded {self.startup_timeout_seconds:.0f}s "
|
||||
f"(last_stage={last_stage})"
|
||||
)
|
||||
if not parent_connection.poll(min(STARTUP_PROGRESS_POLL_SECONDS, remaining)):
|
||||
if not process.is_alive():
|
||||
exit_code = getattr(process, "exitcode", None)
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
self._terminate_worker_locked("startup_process_exited", graceful=False)
|
||||
raise FasterQwenWorkerError(
|
||||
f"Faster-Qwen3-TTS worker exited during startup "
|
||||
f"(exit_code={exit_code}, last_stage={last_stage})"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
message = parent_connection.recv()
|
||||
except (EOFError, OSError) as exc:
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
self._terminate_worker_locked("startup_connection_closed", graceful=False)
|
||||
raise FasterQwenWorkerError(
|
||||
f"Faster-Qwen3-TTS worker connection closed during startup "
|
||||
f"(last_stage={last_stage})"
|
||||
) from exc
|
||||
if isinstance(message, dict) and message.get("type") == "progress":
|
||||
last_stage = str(message.get("stage") or "unknown")
|
||||
elapsed_ms = int((time.monotonic() - started) * 1000)
|
||||
self._log(
|
||||
"info",
|
||||
"[FasterQwenTTS] worker 启动进度: %s, elapsed=%sms",
|
||||
last_stage,
|
||||
elapsed_ms,
|
||||
)
|
||||
continue
|
||||
break
|
||||
if not isinstance(message, dict) or message.get("type") != "ready":
|
||||
if isinstance(message, dict):
|
||||
error = str(message.get("error") or message.get("error_type") or "unknown startup error")
|
||||
worker_traceback = str(message.get("traceback") or "").strip()
|
||||
if worker_traceback:
|
||||
self._log("error", "[FasterQwenTTS] worker 启动异常:\n%s", worker_traceback)
|
||||
else:
|
||||
error = "invalid startup response"
|
||||
self._next_start_after = time.monotonic() + STARTUP_FAILURE_BACKOFF_SECONDS
|
||||
@@ -353,10 +444,13 @@ class FasterQwenWorkerClient:
|
||||
self._next_start_after = 0.0
|
||||
self._log(
|
||||
"info",
|
||||
"[FasterQwenTTS] worker 预热完成, pid=%s load=%sms warmup=%sms max_new_tokens=%s",
|
||||
"[FasterQwenTTS] worker 预热完成, pid=%s load=%sms warmup=%sms "
|
||||
"graph=%sms voice_clone=%sms max_new_tokens=%s",
|
||||
self.worker_pid,
|
||||
message.get("load_ms"),
|
||||
message.get("warmup_ms"),
|
||||
message.get("graph_warmup_ms"),
|
||||
message.get("voice_clone_warmup_ms"),
|
||||
message.get("max_new_tokens"),
|
||||
)
|
||||
self._log(
|
||||
|
||||
Reference in New Issue
Block a user