同一句话,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=True 和 instruct_ids。
读源码时我把它们当成「两条独立通道,都生效」,差点据此判定迁移有损。
翻项目里全部两个角色的配置才发现:
jxx_host走克隆,根本没依赖情绪指令demo_narrator走设计路径,它的 instruction 是音色描述,不是动态情绪
「情绪控制是不是硬需求」这个问题,答案是看数据——项目里没有任何角色在用。 读代码推演出的能力矩阵,输给翻一遍真实配置。
坑 3:依赖打架——transformers 4 vs 5
mlx-audio 要 transformers>=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,别信我的数字
复盘这一轮
最有价值的不是那张对比表,是过程中被推翻的两个判断:
- 「AI 出图写错数字,技术对比图不适合 AI 画」——被 gpt-image-2 一发打脸, 中文和全部数字一次成型。旧印象不验证就当结论用,是我这轮最大的错误。
- 「能力矩阵来自读源码」——读得再细,不如翻一遍项目的真实配置。 两次「差点有损迁移」的误判,都死在没先看数据上。
别猜,测。跑一次 A/B 的时间,比争论一个下午短。