修复直播测试与 TTS 预热

This commit is contained in:
2026-08-17 15:44:23 +08:00
parent f623959fd4
commit da8a82bfd5
8 changed files with 718 additions and 676 deletions
+112 -18
View File
@@ -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(