← 全部文章

同一句话,9.32 秒变 5.29 秒——Qwen3-TTS 从 PyTorch 迁到 Apple MLX

本地 TTS 框架迁移实测:体积 -31%、推理快 1.76×、音质零损失——以及三个差点让我翻车的坑

同一句话,9.32 秒变 5.29 秒

voxflow 用 Qwen3-TTS 做本地中文语音克隆和音色设计,一直跑在 PyTorch + MPS 上。 这次把它迁到 Apple MLX,全部数字来自同一台 M3 Pro 上的 A/B 实测

先看数据

同一模型、同一句文本、同一参考音频,只换推理框架:

指标 PyTorch + MPS Apple MLX 8-bit 变化
模型体积(Base + VoiceDesign) 8.4 GB 5.8 GB −31%
推理耗时 9.32 s 5.29 s 快 1.76×
输出时长 4.64 s 4.56 s 一致
音质(whisper 转写回读) 一字不差 一字不差 无损失
峰值内存 6.97 GB(实测) 有据可依

改完代码再复测一次端到端:加载 7.90s、推理 5.93s、产出 4.40s——比基线快 2.35×

为什么 MLX 在 Mac 上是「对的」框架

Apple Silicon 把 CPU、GPU、内存焊在一颗芯片上,共享同一块统一内存。 传统 PC 是 CPU 一块内存、独显另一块显存,中间隔着 PCIe 搬数据。

这个差别决定本地模型能不能跑得顺:

Apple M 系列 x86 + 独显
跑模型靠什么 统一内存(CPU/GPU 同池) 显存 VRAM
数据要搬吗 不搬,CPU 写完 GPU 直接读 要搬,每次过 PCIe
显存不够 本就没这个概念 直接崩,只能换更小量化版

但真正的根因藏在一行环境变量里。

根因:MPS 的静默降级

voxflow 旧代码第一行就是:

os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"

意思是 MPS 不支持的算子静默回退到 CPU,中间还要把张量搬回来。 「统一内存」在这一步被浪费,而且外部看不出来——日志干净、没有告警、就是慢。

MLX 没有 fallback:要么全 Metal GPU,要么报错。不存在静默降级

这才是迁移最大的收益:不只是快 1.76×,是从此不存在「GPU 算子偷偷跑 CPU」这种事。

三个差点翻车的坑

坑 1:ref_text 留空 = 输出乱码

MLX base 模型的克隆模式必填 ref_text(参考音频的转写文本)。 留空不报错,只是输出截断 + 内容乱码:

ref_text 产出 ASR 回读
"" 2.08s(截断) 「这是一字语音色争要了根根」❌
真实文本 4.56s 「这是一次语音合成测试…」✅ 一字不差

解法:用 whisper 把样音转写出来,填进 personas.json。 代价:每个角色多一个必填字段——但这换来的是克隆质量本身。

坑 2:「情绪指令」是我以为的能力,不是实际在用的能力

PyTorch 版 base 模型同时接受 x_vector_only_mode=Trueinstruct_ids。 读源码时我把它们当成「两条独立通道,都生效」,差点据此判定迁移有损。

翻项目里全部两个角色的配置才发现:

  • jxx_host 走克隆,根本没依赖情绪指令
  • demo_narrator 走设计路径,它的 instruction 是音色描述,不是动态情绪

「情绪控制是不是硬需求」这个问题,答案是看数据——项目里没有任何角色在用。 读代码推演出的能力矩阵,输给翻一遍真实配置。

坑 3:依赖打架——transformers 4 vs 5

mlx-audiotransformers>=5.14,项目锁 ==4.57.3,同一个 venv 装不下。 第一反应是「保留 PyTorch 作回退」——后来发现这个回退是负资产:

  • 每次改动都要确认旧链路还活着
  • A/B 测试要跑两遍
  • 而 MLX 已经实测跑通

。MLX 成为唯一后端,core/engine.py 从 122 行减到 61 行。 回滚方式从「git checkout 三个文件」变成「git revert 一个提交」——PyTorch 权重留在原地,随时可用。

迁移成本与验证

改动不到 50 行,集中在引擎加载 + 3 处调用点:

# MLX 的 API 跟 PyTorch 几乎同签名
results = list(model.generate(
    text=text,
    ref_audio=seed_path,      # 文件路径直接传
    ref_text=ref_text,        # 必填
    lang_code="chinese",
    temperature=0.7, top_k=50, top_p=0.9,
))
audio = results[0].audio      # mx.array

验证方法:whisper 把两版产出都转写回文字,一字不差才算过。 「听起来差不多」不算数——用 ASR 回读做客观比对,比人耳 A/B 更严格。

对 18GB Mac 的实际意义

迁移前:TTS 8.4G + ASR 1.5G + 视觉 3.0G = 12.9G  ← 18G 机器爆边缘
迁移后:TTS 5.8G + ASR 1.5G + 视觉 3.0G = 10.3G  ← 留出余量

TTS 减出来的 2.6 GB,正好是让「语音 + 图片识别」同时常驻的钥匙。 懒加载 + 空闲卸载不再是优化项,是之前就装不下的硬约束。

什么时候不该迁

  • 非 Apple Silicon —— MLX 只有 Mac 有
  • 模型 > 统一内存 —— MLX 优势来自零拷贝,放不进内存一切归零
  • 跑在云端 —— 云上用 CUDA 更成熟,MLX 没有意义
  • 需要 MPS 已调优的算子 —— 迁移前先跑一次你真实负载的 A/B,别信我的数字

复盘这一轮

最有价值的不是那张对比表,是过程中被推翻的两个判断:

  1. 「AI 出图写错数字,技术对比图不适合 AI 画」——被 gpt-image-2 一发打脸, 中文和全部数字一次成型。旧印象不验证就当结论用,是我这轮最大的错误
  2. 「能力矩阵来自读源码」——读得再细,不如翻一遍项目的真实配置。 两次「差点有损迁移」的误判,都死在没先看数据上。

别猜,测。跑一次 A/B 的时间,比争论一个下午短。