Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ TTS_BACKEND=gpt_sovits # Amadeus GPT-SoVITS v3 rewrite | openai_com
# MIMO_TTS_MODEL=mimo-v2.5-tts
# MIMO_TTS_VOICE=冰糖 # 冰糖 | 茉莉 | 苏打 | 白桦 | Mia | Chloe | Milo | Dean
# Embedded model paths (relative to the repository or absolute)
TTS_DEVICE=cuda # auto/cuda | cuda:N | cpu
TTS_DEVICE=auto # Apple Silicon -> mps | NVIDIA -> cuda:0 | Intel macOS -> cpu
# The embedded backend supports GPT-SoVITS v3 checkpoints only.
# Blank uses the canonical filenames installed by voice-kurisu-gpt-sovits-v3.
TTS_GPT_MODEL_PATH=
Expand Down
24 changes: 14 additions & 10 deletions GPT_SoVITS/process_ckpt.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import traceback
from collections import OrderedDict
from time import time as ttime
import shutil,os
import os
import shutil
import sys
import torch

_PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__))
if _PACKAGE_DIR not in sys.path:
sys.path.insert(0, _PACKAGE_DIR)

from utils import HParams
from tools.i18n.i18n import I18nAuto

i18n = I18nAuto()
Expand Down Expand Up @@ -95,12 +103,8 @@ def get_sovits_version_from_path_fast(sovits_path):
return version,model_version,if_lora_v3

def load_sovits_new(sovits_path):
f=open(sovits_path,"rb")
meta=f.read(2)
if meta!="PK":
data = b'PK' + f.read()
bio = BytesIO()
bio.write(data)
bio.seek(0)
return torch.load(bio, map_location="cpu", weights_only=False)
return torch.load(sovits_path,map_location="cpu", weights_only=False)
with open(sovits_path, "rb") as f:
meta = f.read(2)
data = b"PK" + f.read() if meta != b"PK" else meta + f.read()
with torch.serialization.safe_globals([HParams]):
return torch.load(BytesIO(data), map_location="cpu", weights_only=True)
8 changes: 5 additions & 3 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ These are not pending mechanical migrations:

## Compatibility notes

- `TTS_DEVICE=auto` (or `cuda`) still performs the existing local-LLM port
probe. The resolved device is copied to `os.environ` because the bundled
BigVGAN loader directly consumes that variable.
- `TTS_DEVICE=auto` resolves to `mps` on Apple Silicon, `cpu` on Intel macOS,
and the existing `cuda:0` default elsewhere. The resolved device is copied
to `os.environ` because the bundled BigVGAN loader directly consumes that
variable. An explicit indexed CUDA device or explicit `mps`/`cpu` value is
preserved.
- `AMADUES_PRE_TRANSLATION_ENABLED` remains accepted as a deprecated spelling
of `AMADEUS_PRE_TRANSLATION_ENABLED` at the pre-translation boundary.
- The legacy root GPT-SoVITS WebUI/API entry points and their conflicting
Expand Down
11 changes: 8 additions & 3 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import os
import platform
from pathlib import Path

from config.environment import load_project_environment
Expand Down Expand Up @@ -285,8 +286,9 @@ def declared_environment_fields():
def _resolve_tts_device() -> str:
"""
自动选择 TTS 设备:
- .env / 环境变量明确写了 cuda:0 / cuda:1 / cpu → 直接使用
- 未设置 / 写了 "cuda" / 写了 "auto" → 返回 "cuda:0"
- .env / 环境变量明确写了 cuda:0 / cuda:1 / mps / cpu → 直接使用
- 未设置 / 写了 "cuda" / 写了 "auto" → Apple Silicon 返回 MPS,
Intel macOS 返回 CPU,其他平台返回 cuda:0

本地 LLM 的 endpoint 并不能证明它占用了哪张 GPU;多 GPU 分配必须由
TTS_DEVICE 与 LOCAL_LLM_CUDA_VISIBLE_DEVICES 分别显式声明。
Expand All @@ -299,7 +301,10 @@ def _resolve_tts_device() -> str:
if raw and raw not in ("cuda", "auto"):
return raw

device = "cuda:0"
if platform.system() == "Darwin":
device = "mps" if platform.machine().lower() == "arm64" else "cpu"
else:
device = "cuda:0"
# GPT-SoVITS/BigVGAN still reads TTS_DEVICE directly from the process
# environment, so this compatibility write is part of the current contract.
os.environ["TTS_DEVICE"] = device
Expand Down
9 changes: 7 additions & 2 deletions electron/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type McpConnectionUpdate,
} from './desktopSettings.js'
import { ChatAvatarStore, type ChatAvatarRole } from './chatAvatars.js'
import { defaultMpsFallbackEnvironment } from './mpsFallbackPolicy.js'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
Expand Down Expand Up @@ -368,11 +369,15 @@ async function startBackend(): Promise<void> {
AEC_REALTIME_DELAY_MS: '280',
ASR_ECHO_TAIL_GUARD_MS: '650',
})
const backendProcessEnvironment = {
...backendEnvironment,
...process.env,
}
pythonProcess = spawn(python, ['-m', 'server.app', '--port', String(BACKEND_PORT)], {
cwd: PROJECT_ROOT,
env: {
...backendEnvironment,
...process.env,
...backendProcessEnvironment,
...defaultMpsFallbackEnvironment(process.platform, process.arch, backendProcessEnvironment),
PYTHONUNBUFFERED: '1',
PYTHONUTF8: '1',
PYTHONIOENCODING: 'utf-8',
Expand Down
26 changes: 26 additions & 0 deletions electron/src/main/mpsFallbackPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type Environment = Record<string, string | undefined>

export function defaultMpsFallbackEnvironment(
platform: NodeJS.Platform,
architecture: string,
environment: Environment,
): Environment {
if (String(environment.PYTORCH_ENABLE_MPS_FALLBACK || '').trim()) return {}

const backend = String(environment.TTS_BACKEND || 'gpt_sovits').trim().toLowerCase()
const device = String(environment.TTS_DEVICE || '').trim().toLowerCase()
const resolvesToMps = !device
|| device === 'auto'
|| device === 'cuda'
|| device.startsWith('mps')

if (
platform === 'darwin'
&& architecture === 'arm64'
&& backend === 'gpt_sovits'
&& resolvesToMps
) {
return { PYTORCH_ENABLE_MPS_FALLBACK: '1' }
}
return {}
}
41 changes: 41 additions & 0 deletions electron/tests/mpsFallbackPolicy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import assert from 'node:assert/strict'
import test from 'node:test'

import { defaultMpsFallbackEnvironment } from '../src/main/mpsFallbackPolicy.ts'

test('Apple Silicon local GPT-SoVITS defaults MPS fallback on', () => {
assert.deepEqual(defaultMpsFallbackEnvironment('darwin', 'arm64', {
TTS_BACKEND: 'gpt_sovits',
TTS_DEVICE: 'auto',
}), { PYTORCH_ENABLE_MPS_FALLBACK: '1' })
})

test('an explicit MPS fallback value is preserved', () => {
assert.deepEqual(defaultMpsFallbackEnvironment('darwin', 'arm64', {
TTS_BACKEND: 'gpt_sovits',
TTS_DEVICE: 'mps',
PYTORCH_ENABLE_MPS_FALLBACK: '0',
}), {})
})

test('CPU and remote TTS paths do not receive the MPS fallback default', () => {
assert.deepEqual(defaultMpsFallbackEnvironment('darwin', 'arm64', {
TTS_BACKEND: 'gpt_sovits',
TTS_DEVICE: 'cpu',
}), {})
assert.deepEqual(defaultMpsFallbackEnvironment('darwin', 'arm64', {
TTS_BACKEND: 'openai_compatible',
TTS_DEVICE: 'auto',
}), {})
})

test('Intel macOS and non-macOS platforms do not receive the MPS fallback default', () => {
assert.deepEqual(defaultMpsFallbackEnvironment('darwin', 'x64', {
TTS_BACKEND: 'gpt_sovits',
TTS_DEVICE: 'auto',
}), {})
assert.deepEqual(defaultMpsFallbackEnvironment('win32', 'arm64', {
TTS_BACKEND: 'gpt_sovits',
TTS_DEVICE: 'auto',
}), {})
})
74 changes: 57 additions & 17 deletions local_tts_infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import tempfile
import traceback
import numpy as np
from contextlib import nullcontext
from pathlib import Path
import string
from string import punctuation
Expand Down Expand Up @@ -58,6 +59,16 @@ def _default_ref_free_prompt() -> str:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('tts_inference')


def _uses_torch_cuda_device_api(device_name: str) -> bool:
"""Return whether the device is addressed through torch.cuda (CUDA or HIP)."""
return device_name.startswith("cuda") and torch.cuda.is_available()


def _allows_nvidia_cuda_extensions(uses_torch_cuda_api: bool) -> bool:
"""NVIDIA CUDA extensions are incompatible with PyTorch ROCm/HIP builds."""
return uses_torch_cuda_api and not bool(getattr(torch.version, "hip", None))

# 获取当前项目根目录
root_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, root_dir)
Expand Down Expand Up @@ -121,7 +132,21 @@ def __init__(self,
base_dir = root_dir
self.device = device
device_name = str(device).lower()
self.is_half = torch.cuda.is_available() and device_name.startswith("cuda")
self._uses_torch_cuda_api = _uses_torch_cuda_device_api(device_name)
self._allows_nvidia_cuda_extensions = _allows_nvidia_cuda_extensions(
self._uses_torch_cuda_api
)
if device_name.startswith("cuda") and not self._uses_torch_cuda_api:
raise RuntimeError(
f"TTS device {device!r} requires a usable PyTorch CUDA/HIP device"
)
if device_name.startswith("mps") and not (
hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
):
raise RuntimeError(
f"TTS device {device!r} requires an available PyTorch MPS backend"
)
self.is_half = self._uses_torch_cuda_api

# CUDA device index — used to set the current CUDA device before custom
# CUDA kernel calls (BigVGAN's fused anti-alias activation), which rely on
Expand Down Expand Up @@ -205,14 +230,22 @@ def _get_effective_max_sec(self, override_value):
return effective

def _sync_t2s_timing(self):
if self.is_half and str(self.device).lower().startswith("cuda") and torch.cuda.is_available():
with torch.cuda.device(self._tts_device_idx):
torch.cuda.synchronize()
self._synchronize_device()

def _sync_sovits_timing(self):
if str(self.device).lower().startswith("cuda") and torch.cuda.is_available():
self._synchronize_device()

def _device_context(self):
if self._uses_torch_cuda_api:
return torch.cuda.device(self._tts_device_idx)
return nullcontext()

def _synchronize_device(self):
if self._uses_torch_cuda_api:
with torch.cuda.device(self._tts_device_idx):
torch.cuda.synchronize()
elif str(self.device).lower().startswith("mps"):
torch.mps.synchronize()

def _record_t2s_stat(
self,
Expand Down Expand Up @@ -496,7 +529,7 @@ def _load_gpt_model(self):
"""加载GPT模型"""

logger.info(f"Loading GPT model: {self.gpt_path}")
dict_s1 = torch.load(self.gpt_path, map_location="cpu")
dict_s1 = torch.load(self.gpt_path, map_location="cpu", weights_only=True)
self.gpt_config = dict_s1["config"]
self.hz = 50 # 默认值
self.max_sec = self.gpt_config["data"]["max_sec"]
Expand Down Expand Up @@ -708,10 +741,10 @@ def _load_bigvgan_model(self):
/ "anti_alias_activation_cuda.pyd"
)
_use_cuda_kernel = False
if _cuda_pyd.exists():
if self._allows_nvidia_cuda_extensions and _cuda_pyd.exists():
_use_cuda_kernel = True
logger.info("[BigVGAN] compiled CUDA kernel cache found; loading directly")
else:
elif self._allows_nvidia_cuda_extensions:
try:
import subprocess as _sp
_nvcc = _sp.run(["nvcc", "--version"], capture_output=True, timeout=5)
Expand All @@ -720,26 +753,34 @@ def _load_bigvgan_model(self):
logger.info("[BigVGAN] nvcc available; trying to compile the CUDA kernel")
except Exception:
logger.info("[BigVGAN] nvcc unavailable; using the PyTorch implementation")
elif self._uses_torch_cuda_api:
logger.info("[BigVGAN] ROCm/HIP device; using the PyTorch implementation")
else:
logger.info("[BigVGAN] non-CUDA device; using the PyTorch implementation")

kernel_override = os.environ.get("BIGVGAN_USE_CUDA_KERNEL", "").strip().lower()
if kernel_override in {"0", "false", "off", "no"}:
_use_cuda_kernel = False
logger.info("[BigVGAN] BIGVGAN_USE_CUDA_KERNEL=0, forcing PyTorch path")
elif kernel_override in {"1", "true", "on", "yes"}:
_use_cuda_kernel = True
logger.info("[BigVGAN] BIGVGAN_USE_CUDA_KERNEL=1, forcing CUDA kernel path")
if self._allows_nvidia_cuda_extensions:
_use_cuda_kernel = True
logger.info("[BigVGAN] BIGVGAN_USE_CUDA_KERNEL=1, forcing CUDA kernel path")
else:
_use_cuda_kernel = False
logger.warning("[BigVGAN] CUDA kernels are unavailable on this TTS device; using PyTorch")

# activation1d.py 在首次 import 时执行模块级 load.load(),
# 若此时无设备上下文则 CUDA kernel 内部状态绑定到 cuda:0,
# 之后模型移到 cuda:1 会触发 CUDNN_STATUS_MAPPING_ERROR。
# 用 torch.cuda.device() 确保 kernel 初始化在正确设备上进行。
try:
with torch.cuda.device(_tts_device_idx):
with self._device_context():
self.bigvgan_model = bigvgan.BigVGAN.from_pretrained(bigvgan_path, use_cuda_kernel=_use_cuda_kernel)
except Exception as _kernel_err:
if _use_cuda_kernel:
logger.warning(f"[BigVGAN] CUDA kernel compilation failed; falling back to PyTorch implementation: {_kernel_err}")
with torch.cuda.device(_tts_device_idx):
with self._device_context():
self.bigvgan_model = bigvgan.BigVGAN.from_pretrained(bigvgan_path, use_cuda_kernel=False)
else:
raise
Expand Down Expand Up @@ -1336,7 +1377,7 @@ def infer(self,
# torch.cuda.device() ensures at::cuda::getCurrentCUDAStream()
# inside the fused CUDA kernel uses the correct device stream,
# preventing CUDNN_STATUS_MAPPING_ERROR on non-default GPUs.
with torch.cuda.device(self._tts_device_idx):
with self._device_context():
with torch.inference_mode():
wav_gen = self.bigvgan_model(cmf_res)
audio = wav_gen[0][0]
Expand Down Expand Up @@ -1819,7 +1860,7 @@ def _yield_audio_segments(audio_np: np.ndarray, text_payload: str):
if self._sovits_sync_timing_enabled:
self._sync_sovits_timing()
_t1 = time.perf_counter()
with torch.cuda.device(self._tts_device_idx):
with self._device_context():
with torch.inference_mode():
wav_gen = self.bigvgan_model(chunk_mel)
audio = wav_gen[0][0]
Expand Down Expand Up @@ -1876,7 +1917,7 @@ def _yield_audio_segments(audio_np: np.ndarray, text_payload: str):
if self._sovits_sync_timing_enabled:
self._sync_sovits_timing()
_t1 = time.perf_counter()
with torch.cuda.device(self._tts_device_idx):
with self._device_context():
with torch.inference_mode():
wav_gen = self.bigvgan_model(cmf_res)
audio = wav_gen[0][0]
Expand All @@ -1889,8 +1930,7 @@ def _yield_audio_segments(audio_np: np.ndarray, text_payload: str):
cmf_res.shape[2],
))
else:
if str(self.device) != "cpu":
torch.cuda.synchronize()
self._synchronize_device()
print("[sovits-timing] cfm=%.1fms bigvgan=%.1fms mel_T=%s" % (
_t_cfm_total * 1000.0,
(time.perf_counter() - _t1) * 1000.0,
Expand Down
Loading
Loading