Mizuoto

性能记录

在结果不变的规则下怎样测量 Mizuoto 的速度(perf.py、train_perf.py),每次测量的条件,以及每一轮工作改了什么:CPU 上的几轮、注意力分发器、各引擎的 CUDA Graph 和 torch.compile。

推理和训练代码的提速工作遵循一条规则:结果不变。这里记录的每项优化,在 CPU 上(引擎与上游的一致性就是在 CPU 上确立的)都与之前的代码给出逐样本相同的音频,以及逐位相同的损失和保存的权重。在 GPU 上,“相同”指在同一块 GPU、同一驱动、同一 torch 和同一设置下与之前的代码完全相同。改变运算的提速是需要主动开启的功能,见加速;面向用户的设置见性能与加速。

怎样测量

工具作用
scripts/parity/perf.py run <cases.jsonl>… --name NAME [--only ID] [--repeat N] [--device D] [--setting k=v] [--reference NAME] [--profile] [--torch-profile [--keep-trace]] [--deterministic]在一个进程中让一致性用例经过整个系统,每个运行 --repeat 次(3 次):第一次加载模型并准备音色,其余为预热后的运行。记录墙钟时间、加载、首块音频、引擎总时间、实时率,以及每个阶段(ctx.use / ctx.run,不含嵌套阶段,另有阶段之外的时间)内的时间。逐样本比较各次运行之间的音频,以及与 --reference 的音频:该名称的一次早先运行,否则为用例的一致性基准文件
perf.py compare --a NAME --b NAME [--report out.md]、perf.py show NAME两轮的预热运行对照,加速比,流式用例的首段音频时间,以及变化最大的阶段
scripts/parity/train_perf.py run <engine> --fixture F --name NAME [--device D] [--setting k=v] [--reference NAME] [--profile]、compare按一致性基准的配方经过整个系统做一次微调,每个阶段都像实际使用时一样是子进程:每个阶段的时间、每步时间、每个指标,以及导出写出的每个权重文件的 SHA-256,并与早先的运行比较
  • --profile 在 cProfile 下多跑一次不计入的运行(train_perf.py 则在进程内运行各阶段);--torch-profile 按自身耗时列出算子。
  • 在 GPU 上(--device cuda:N,它成为每个引擎的设备,也是训练的设备):工具在每个阶段边界等待设备,使阶段时间是它的实际工作而不只是内核的发出;并按阶段记录峰值分配、阶段权重放好之后在已分配量之上的工作内存,以及引擎申请的工作内存(另记下最接近其自身估算的那一次调用:一个阶段最大的用量和最大的估算可能来自不同的调用),另有请求的峰值已分配与已保留内存。此时 --torch-profile 还记录设备活动并按阶段汇总(<case>.gpu.json):墙钟时间与设备忙碌时间、内核数、发出次数、CUDA Graph 重放次数、主机等待设备的次数(cudaStreamSynchronize、cudaEventSynchronize、同步的 cudaMemcpy;工具自身每个阶段的两次 cudaDeviceSynchronize 单独计数),以及主机与设备之间的拷贝。
  • --deterministic 在 torch.use_deterministic_algorithms(True, warn_only=True) 与 CUBLAS_WORKSPACE_CONFIG=:4096:8 下运行,二者由工具设置(原生代码从不改动进程级状态);没有确定性实现的运算列在记录里,而不是让用例失败。train_perf.py --setting training.memory=low 用来测量某个显存预设。
  • 记录写到 MIZUOTO_PERF_DIR(默认 ~/code_workspace/experiment/perf),每个用例每轮一个 JSON 文件。
  • 预热时间取最快的预热运行:机器上的其他工作只会让运行变慢。重要的比较都交错进行,两份代码在各自的进程里先后运行。

冻结要测量的代码(在该提交上建一个工作树),运行基线:对用例集运行 perf.py run,对每个训练器运行 train_perf.py run。

用 --profile、--torch-profile 找出时间花在哪里;涉及内核层面的改动,先写一个微基准,并在引擎实际用到的形状上用 torch.equal 检查该运算的输出。

以基线为 --reference 运行改动后的代码:每个用例的音频都必须完全相同,每次微调的指标和权重哈希都必须完全相同。然后对代码有改动的引擎运行一致性验证,并运行测试套件。

用 perf.py compare 生成表格,并把这一轮记在这里。

推理用例集共 13 个:Qwen3-TTS base-clone-en 与 cv-en-aiden,F5-TTS clone-en,OmniVoice clone-en,CosyVoice v2-clone-en 与 v3-clone-en,GPT-SoVITS v2-clone-zh 与 v4-clone-zh,Fish Speech clone-en,IndexTTS v2-clone-en 与 v15-clone-en,VoxCPM v2-clone-en 与 v15-clone-en。训练用例集是每个训练器在一个 11 段音频的数据集上做一次微调。

测量条件

  • CPU:AMD Ryzen 9 5900X(12 核;torch 用 12 个线程),125 GB 内存,torch 2.8.0(MKL 2024.2、oneDNN 3.7.1),Python 3.11;fp32,VoxCPM 除外(bf16,其上游默认值)。机器上的其他工作约占 9 % 的线程;计算线程被抢占的运行会停顿,所以单次运行的波动可达 ±10 %。
  • GPU:两块 RTX 3090(24 GB),与另一项工作共用,它在每块卡上占约 21 GB 显存并让两块卡都保持 100 % 繁忙。下面所有 GPU 数字都因共用而偏大;比较都在同一块 GPU、同一 torch 上交错进行。放不进剩余显存的模型只测量其解码循环:真实尺寸、随机权重、只取少数几层,用到这种方式的地方都会注明。
  • GPU,2026-10-18 起(GPU 基线):同样两块 RTX 3090,空闲(没有其他工作),驱动 595.99.02,torch 2.8.0+cu128(CUDA 12.8、cuDNN 9.10.2),PCIe 4.0 x16。推理在一块卡上运行,微调同时在另一块卡上运行,二者共用 CPU。

CPU 上时间花在哪里

CPU 优化之前的预热请求:

引擎、用例音频(秒)预热(秒)各阶段(秒)
Qwen3-TTS base-clone-en5.4434.25talker 30.2,codec decoder 4.0
Qwen3-TTS cv-en-aiden3.7620.72talker 19.9,codec decoder 0.9
F5-TTS clone-en4.7469.75transformer 69.7
OmniVoice clone-en4.8232.48language model 32.3
CosyVoice v2-clone-en6.8418.67language model 12.1,flow 6.5
CosyVoice v3-clone-en4.5216.71flow 9.6,language model 7.1
GPT-SoVITS v2-clone-zh7.344.42gpt 3.4,sovits 0.9
GPT-SoVITS v4-clone-zh3.9631.45sovits(CFM + 声码器)29.6,gpt 1.8
Fish Speech clone-en4.32110.55language model 107.3,codec 3.3
IndexTTS v2-clone-en5.1757.20gpt 39.3,s2mel 15.2,bigvgan 2.6
IndexTTS v15-clone-en3.7145.89gpt 44.2,bigvgan 1.7
VoxCPM v2-clone-en6.7276.91language model 75.6,vae 1.3
VoxCPM v15-clone-en5.6021.82language model 20.7,vae 1.1
  • 矩阵乘法占主导(F.linear/addmm:50–90 % 的时间)。自回归语言模型对着 fp32 权重一次生成一个 token,受内存带宽限制(Qwen3-TTS 的 1.7B talker 每帧以约 36 GB/s 读取约 6.8 GB)。更快的内核、更低的精度或其他线程数都会改变求和结果。
  • VoxCPM 2 的 77 秒中有 40 秒花在注意力上:每一步都在掩码下对整个 8192 个位置的静态缓存做注意力,而实际只填了几百个位置。
  • 内存分配:每层都要分配和释放几到几十 MB 的张量,按 glibc 默认设置每次都重新映射,并由内核清零页面。

CPU 第一轮:CPU 设置与精确的清理

改动为什么结果不变
compute.cpu_allocator 和 compute.cpu_threads(加速)改变的是内存从哪里来、线程在哪里运行,而不是计算什么;线程数相同,工作的切分方式就相同
VoxCPM 只对缓存中当前长度之后下一个 256 的倍数以内的位置做注意力(保留掩码),而不是全部 8192 个位置(自 G1 起仅在 CPU 上,见 GPU 检查)被掩码的位置加上的是精确的零,且在任何 64 的倍数处截断时 CPU 上的 SDPA 给出逐位相同的结果(长度直到 3100,bf16 与 fp32,批大小 1 与 2)
DiT 的旋转位置编码:每次前向只算一次 cos 和 sin,不再乘以 1,不再拼接空尾部(F5-TTS、CosyVoice 3、GPT-SoVITS v4/v5)数值相同
CosyVoice 的 flow U-Net 每次前向只构造一次按头的掩码,而不是在 56 个块中各构造一次同一个张量
IndexTTS:GPT-2 的 GELU 原地计算;跳过保留所有行的 beam 重排同样的运算、同样的顺序

推理,预热请求(秒),每个用例都与之前完全相同:

引擎、用例之前 → 之后加速比
VoxCPM v2-clone-en75.34 → 36.28×2.08
F5-TTS clone-en68.14 → 41.22×1.65
VoxCPM v15-clone-en21.42 → 14.40×1.49
CosyVoice v2-clone-en18.55 → 14.70×1.26
CosyVoice v3-clone-en16.66 → 13.58×1.23
IndexTTS v2-clone-en52.07 → 43.97×1.18
Qwen3-TTS cv-en-aiden20.71 → 18.08×1.15
Qwen3-TTS base-clone-en34.22 → 30.63×1.12
Fish Speech clone-en110.43 → 98.36×1.12
GPT-SoVITS v4-clone-zh29.24 → 26.38×1.11
IndexTTS v15-clone-en40.12 → 36.65×1.09
OmniVoice clone-en28.73 → 27.92×1.03
GPT-SoVITS v2-clone-zh4.32 → 4.44在波动范围内

训练,每个训练器一次微调(秒),指标和权重张量完全相同(训练阶段通过环境变量得到 CPU 设置):

引擎、配方运行之前 → 之后加速比
F5-TTS full,1 个 epoch34.8 → 28.8×1.21
CosyVoice 2 llm,2 个 epoch98.5 → 82.9×1.19
Fish Speech S2 Pro LoRA,2 步207.4 → 175.9×1.18
Qwen3-TTS 1.7B speaker,1 个 epoch121.2 → 103.4×1.17
VoxCPM 0.5B LoRA,3 步29.8 → 26.4×1.13
GPT-SoVITS v2Pro speaker,1 + 1 个 epoch246.0 → 227.3×1.08
OmniVoice LoRA,3 步39.8 → 36.9×1.08

CPU 设置是怎样选定的。 每种配置都在相互交错的独立进程中运行。同时固定 mmap 阈值并保留空闲堆内存对 F5-TTS 有帮助(预热中位数 64.0–68.6 秒 → 53.7–60.9 秒);只改其中一项反而更慢;glibc 的 hugetlb 和 tcmalloc 都不比 torch 自己的大页更好。OmniVoice 看起来每个进程呈双峰(约 28 秒或 38 秒,与分配器无关):原因是线程放置。默认放置的六个进程中有一个跑出 38.5 秒;线程绑定到核心的六个进程中没有一个这样(27.6–28.3 秒)。

CPU 第二轮:训练

对微调做性能剖析,结果显示:与其他地方一样主要是反向传播和矩阵乘法,此外是 AdamW 逐张量的循环、dropout 掩码,以及 Fish Speech 在 123 秒的训练阶段中花 41 秒重放约 180 亿次 32 位随机数抽取,只为把进程级生成器推进到上游开始训练时所处的状态。

改动为什么结果不变
跳跃式推进生成器而不是逐个抽取:在 GF(2) 中计算 MT19937 抽取 n 次后的状态(mizuoto/training/rng.py),204 次跳跃用 2.3 秒,而不是抽取的 41–52 秒状态与逐个抽取逐字节相同(从 1 次到 2·10⁹ 次抽取都测试过)
Fish Speech 合并 LoRA 时每个基座权重只做一份 float32 拷贝,而不是两份数值相同
不求梯度时 Fish Speech 编解码器的 snake 原地计算同样的运算、同样的顺序

Fish Speech S2 Pro LoRA(2 步):CPU 优化前 207.4 秒,第一轮后 172–173 秒,第二轮后 128.5–130.6 秒(训练阶段 146.8 → 83 秒),指标和权重完全相同。它的预热请求从 110.4 秒降到 85.7 秒(编解码器 3.3 → 2.0 秒),一致性用例不变。

注意力分发器

让每次注意力调用都经过 mizuoto.runtime.accel(加速)在 auto 下每次调用约多 1 微秒:解码规模的调用(16 个头、1 个查询、300 个键、头维度 128)多 1.11 微秒(28.8 → 30.0 微秒),小调用多 0.82 微秒。用例集中调用次数最多的请求是 7,152 次(Fish Speech),远低于其时间的 0.1 %。全部 13 个用例和全部 7 次微调都保持完全相同;预热时间在机器的波动范围内变化(Fish Speech 交错测量:之前 86.6/86.9 秒,之后 86.5/87.3 秒)。

在 CPU 上设置 compute.attention = fast 时,每次调用都经过策略,全部由 SDPA 计算,没有回退(每个请求的调用次数:F5-TTS 704,OmniVoice 896,GPT-SoVITS v4 3,044,VoxCPM 1.5 3,928,CosyVoice 2 4,688,Qwen3-TTS 4,877,IndexTTS 2 6,667,Fish Speech 7,152)。七个引擎的音频仍完全相同;IndexTTS 2 最多相差 2 个 int16 步长(SNR 86.1 dB,长度相同),因为它的 perceiver 中手写的 softmax 改由 SDPA 计算。

GPU:不等待设备的解码,以 CUDA Graph 运行

每个自回归解码器都逐个启动每步的几千个小内核,并且每步要等待设备多次(torch.multinomial 的检查、每步复制的掩码、读回的填充掩码、停止检查),于是 CPU 的启动时间和 GPU 的计算时间相加。现在每个解码器每步只等待一次,并在开启 compute.cuda_graphs 时以 CUDA Graph 重放一步的内核(加速)。下面每个结果都与旧代码的音频或 token 相同;每个引擎在 CPU 上的一致性用例都保持完全相同。全部在上文所述的共用 GPU 上测量。

Qwen3-TTS

按 1.7B 模型尺寸、随机权重、28 层 talker 中取 4 或 8 层、100 帧、批大小 1 的解码循环(1.7B 模型放不进剩余显存);每帧毫秒数,两轮交错中最快的预热运行:

torch配置4 层8 层
2.8.0+cu128之前66.870.1
关闭图47.750.7
默认24.625.4
qwen-tts-webui 默认(图、合并投影)23.324.5
qwen-tts-webui,图,不合并28.629.7
2.10.0+cu130之前67.471.5
关闭图39.340.3
默认20.920.9
qwen-tts-webui 默认18.419.9

快 ×2.7–3.4,与 qwen-tts-webui 的合并路径相差 5–13 %。每帧(4 层)Mizuoto 启动 2,519 个内核,qwen-tts-webui 启动 2,121 个;差别在于合并投影(588 个 GEMM 对 327 个)以及缓存是拼接增长而不是写入缓冲区,这两者在 CUDA 上都会改变结果的二进制位。

合并投影(fused_projections,需主动开启),4 / 8 层下每帧毫秒数:

torch精确合并qwen-tts-webui
2.8.0+cu12824.6 / 25.523.7 / 24.624.1 / 24.5
2.10.0+cu13020.0 / 20.819.2 / 20.017.9 / 19.9

快 3–4 %,在共用 GPU 的波动范围内(两轮之间 ±1 毫秒)与 qwen-tts-webui 持平。

真实的 1.7B Base 模型(2026-10-19,空闲的 RTX 3090,torch 2.8.0+cu128,经完整链路进行中文和英文音色克隆,每项四次运行,两轮交替;每帧最佳毫秒数、实时因子中位数、峰值显存):

精确(默认)fused_projectionsqwen-tts-webui 的合并快速路径
clone-zh17.00–17.06 ms,RTF 0.213,4.03 GB16.21–16.24 ms,0.203,4.03 GB16.23–16.31 ms,0.203,4.58 GB
clone-en17.12–17.17 ms,RTF 0.214,4.03 GB16.23–16.25 ms,0.203,4.03 GB16.29–16.32 ms,0.204,4.64 GB

精确路径比 qwen-tts-webui 慢 5 %,合并路径与之持平,两者的显存都少 12 %;精确路径的音频在两轮之间逐采样相同。

Fish Speech

带真实快速 transformer、36 个慢速层中取 2 或 4 层、随机权重、60 帧的解码循环(11 GB 的 S2 Pro 放不进);每帧毫秒数,torch 2.8:

代码2 个慢速层4 个慢速层
之前93.896.8
关闭图43.444.3
默认38.938.1

大部分收益来自去掉等待(×2.2);在满载的 GPU 上图再快约 10 %,此时每帧读取快速 transformer 的权重(约 4 GB)限制了速度。每个慢速层每帧的开销从约 1.5 毫秒降到 0.45 毫秒。

CosyVoice 2/3

真实的 CosyVoice 2 语言模型(2 GB,放得下),150 个 token,每个 token 毫秒数:

代码第 1 轮第 2 轮
之前69.670.7
关闭图14.314.1
默认9.910.3

约 ×7:一个 token 对应 40 毫秒音频,所以语言模型从比实时慢 1.7 倍变成比实时快 4 倍。三个种子(每个约 400 个 token,直到 EOS)在开关图时 token 都相同。

VoxCPM

真实权重上的完整请求(含 VAE),6.4–6.9 秒音频,三次预热请求中最快的一次,torch 2.8:

模型之前关闭图默认
0.5B4.49 秒(RTF 0.65)4.32 秒2.36 秒(RTF 0.34)
1.53.00 秒(RTF 0.47)2.95 秒1.87 秒(RTF 0.29)

×1.9 和 ×1.6。在 CUDA 上,进程中第一个 VoxCPM 请求解码其(相同的)潜变量时得到的二进制位与之后的请求不同(VAE);之后的请求完全相同,之前的代码也是如此,所以比较从一次丢弃的请求之后开始。原因是 TorchScript 的融合器作用于 Snake(GPU 基线)。

GPT-SoVITS

在 GPU 上经过整个系统(perf.py --device cuda:0),最快的预热运行:

用例之前关闭图默认加速比
v2-clone-zh1.61 秒1.26 秒0.89 秒×1.81
v2-batch-cut5-zh3.72 秒2.90 秒2.29 秒×1.62
v2pro-serial-en1.78 秒1.35 秒0.89 秒×2.00
v4-batch-cut5-zh4.05 秒3.58 秒3.14 秒×1.29

v4 收益最小:它的 flow 和声码器占主导,而且在拥挤的 GPU 上,模型管理器在两次请求之间为它们把 GPT 移出设备,因此每个请求都要重新捕获 GPT 的图。直接调用 capture_begin/capture_end 而不是 torch.cuda.graph(后者在 25 次捕获的每一次都运行 gc.collect() 和 empty_cache()),才使 v4 没有因此变慢。

IndexTTS

真实模型在剩余显存中放不下整个,所以经过整个系统时权重是流式加载的,图不会启用:仅去掉等待就让 v1.5 从 10.25 降到 9.59 秒,v2 从 36.3 降到 33.4 秒(×1.07–1.09),音频相同。按 1.5 GPT 尺寸、24 层中取 4 或 8 层、随机权重、fp16、3 个 beam、120 步的解码循环;每步毫秒数:

代码4 层8 层
之前15.519.2
关闭图6.08.6
默认5.95.95

×2.6–3.2;即时执行每层约 0.65 毫秒,图几乎不花时间,所以完整的 24 层收益更大。

在 CPU 上

同样的改动对 CPU 也略有帮助:Qwen3-TTS cv-en-aiden 17.05 → 16.76 秒,base-clone-en 26.19 → 25.83 秒,音频相同。

GPU 基线(G0)

第一次在空闲 GPU 上测量(条件见上),测的是冻结在工作树中的提交 4e6fe35。每个引擎都用其 GPU 默认值(dtype = auto,CUDA Graph 开启),每个用例在每个引擎各自的进程里运行三次,另加两个流式用例。预热时间取后两次中较快的一次;首次请求包含模型加载。

引擎、用例音频秒数预热秒数实时率首次请求秒数(加载)预热时的阶段(秒)峰值 MB
Qwen3-TTS base-clone-en5.841.280.2217.7(7.9)talker 1.22、codec decoder 0.058,296
Qwen3-TTS cv-en-aiden3.680.790.2220.0(8.8)talker 0.76、codec decoder 0.034,004
F5-TTS clone-en4.740.920.196.5(1.9)transformer 0.90758
OmniVoice clone-en5.050.840.176.7(2.3)language model 0.811,963
CosyVoice v2-clone-en5.001.400.2819.7(7.0)language model 0.91、flow 0.473,107
CosyVoice v3-clone-en3.920.950.2414.9(6.7)language model 0.50、flow 0.447,352
GPT-SoVITS v2-clone-zh6.300.540.099.5(2.4)gpt 0.43、sovits 0.031,109
GPT-SoVITS v4-clone-zh3.681.190.3214.4(4.4)sovits 0.73、gpt 0.382,860
GPT-SoVITS v2-stream-zh(流式)7.020.96,首段音频 0.950.14—sovits+t2s 0.832,866
Fish Speech clone-en4.043.050.7613.6(4.1)language model 3.01、codec 0.049,769
IndexTTS v15-clone-en3.970.570.146.4(2.2)gpt 0.49、bigvgan 0.061,978
IndexTTS v2-clone-en3.972.030.5140.2(18.6)gpt 1.22、s2mel 0.69、bigvgan 0.107,982
VoxCPM v2-clone-en5.921.240.2112.8(4.8)language model 1.21、vae 0.025,270
VoxCPM v15-clone-en4.800.680.145.1(2.0)language model 0.65、vae 0.027,387
VoxCPM v2-stream-en(流式)3.681.02,首段音频 0.140.28—model+vae 1.025,103

峰值 MB 是请求的峰值分配量,包括进程里已加载的所有模型。GPT-SoVITS 的流式用例在第一个音频块之前解码完整段文本(首段音频 0.95 秒,共 0.96 秒);VoxCPM 按块流式输出(0.14 秒)。

重复性。 在默认模式下,每个用例在同一进程的多次运行之间、以及在新进程中都给出相同的音频,只有 VoxCPM 2 的第一个请求例外。--deterministic 会改变 CosyVoice 2、IndexTTS 2、OmniVoice 和 VoxCPM 的二进制位(cuBLAS 和 cuDNN 选了别的算法),同样可以重复,并且没有列出任何缺少确定性内核的运算。所以之后的 GPU 轮次在默认模式下精确比较输出,丢弃 VoxCPM 的第一个请求。

VoxCPM 的第一个请求。 Snake 是 TorchScript 函数,与上游一样。在 CUDA 上,TorchScript 的 profiling executor 对某个形状的第一次调用按单独的运算执行(与即时执行的代码和 CPU 逐位相同),之后改用融合内核,结果最多相差 4.8·10⁻⁷。没见过的形状会退回未融合的运算,直到它被分析过。因此哪些调用被融合取决于进程之前解码过什么,在加载时预热也不能让每个请求都重复。关闭融合器后(仅为实验;原生代码从不设置它),第一个请求与之后的请求一致。融合器还在默认设备上编译内核:在 cuda:1 上工作的进程会在 cuda:0 上打开一个 256 MB 的上下文(在 VoxCPM 和 GPT-SoVITS 的训练中都观察到,后者的 WaveNet 门控同样是 TorchScript 函数)。CPU 不受影响(那里不做融合)。(之后已修复:Snake 以 eager 方式运行,每个请求都相同,也与 CPU 相同;VAE 慢约 1 %。见 VoxCPM。)

时间花在哪里(在 torch 的 profiler 下多跑一次,按阶段统计设备忙碌时间,对比不开 profiler 时该阶段的预热时间;profiler 会拖慢主机一侧):

  • 受设备计算量限制(忙碌约 90 % 或更高):F5-TTS 的 DiT、IndexTTS 的 s2mel、BigVGAN 和 1.5 的 GPT、CosyVoice 3 的 flow、Fish Speech、Qwen3-TTS 1.7B、VoxCPM 和 OmniVoice 的语言模型。(G2 中尝试把 OmniVoice 每步的前向捕获为 CUDA Graph,只快了 2 %。)
  • 受内核发出限制:GPT-SoVITS 的 GPT(约 56 %)和 CosyVoice 2 的语言模型(约 69 %)。
  • 主机等待:每个自回归解码器大约每生成一个 token 等待一次(GPT-SoVITS v2 157 个 token 等待 157 次,Fish Speech 约 87 帧等待 92 次,Qwen3-TTS 约 73 帧等待 82 次),与设计一致。

工作内存与各阶段的估计值比较(估计值必须不小于阶段实际用量):语言模型只用到估计值的 2–17 %(Qwen3-TTS 的 talker 用 28 MB,估计 328 MB;Fish Speech 用 128 MB,估计 733 MB)。两个解码器用量超过估计:IndexTTS 1.5 的 BigVGAN 用 326 MB,估计 279 MB;VoxCPM 1.5 的 VAE 用 278 MB,估计 183 MB;VoxCPM 2 的 VAE 接近(190 MB 对 203 MB)。在显存紧张的 GPU 上由 OOM 重试兜底;这些估计值需要调高。

与 0.1 的 GPU 一致性,限于 0.1 规定了 GPU 行为的情形(每边各运行两次;两边都能精确重复自身):

用例0.1 在 GPU 上原生与 0.1 比较
F5-TTS clone-en,fp16正常运行逐样本相同
IndexTTS v15-clone-en,fp16 GPT 带 KV 缓存正常运行长度相同,SNR 2.3 dB:不相同(2026-10-19 查明:参考 mel 在 GPU 上计算、空位置编码为 fp16、权重归一化在 CPU 上折叠;均已修正,现在相差不超过一个 int16 刻度,见 IndexTTS)。关闭 CUDA Graph 后原生音频不变,所以差异出在运算上而不是 Graph;原因待查
Fish Speech clone-en,bf16失败:Input type (float) and bias type (c10::BFloat16) should be the same(编解码器是 bf16,参考音频是 fp32)原生正常运行(Fish Speech)

GPU 上的训练,training.memory 从 standard 到 low:

引擎、配方运行秒数每步秒数峰值 MBlow 是否相同
F5-TTS full,1 个 epoch23.5 → 22.90.18 → 0.198,821 → 7,395是
Qwen3-TTS 1.7B speaker,1 个 epoch20.1 → 22.40.20 → 0.3618,280 → 18,274是
OmniVoice LoRA,3 步24.9 → 18.70.24 → 0.497,300 → 2,971是
CosyVoice 2 llm,2 个 epoch39.5 → 34.9—10,570 → 9,690是
VoxCPM 0.5B LoRA,3 步21.0 → 22.00.41 → 0.624,343 → 2,747是
GPT-SoVITS v2Pro speaker,1 + 1 个 epoch44.2 → 44.2—3,881 → 3,881在 GPU 上不能重复
Fish Speech S2 Pro LoRA,2 步59.6 → 48.70.57 → 0.579,978 → 9,978在 GPU 上不能重复

五个训练器在多次运行之间可以重复,并在 low 下给出相同的损失和权重。GPT-SoVITS 的 SoVITS 阶段前七步可以重复,之后在多次运行之间逐渐偏离;在其阶段中启用确定性算法后(仅为实验)两次运行完全相同,所以偏离来自有确定性版本的运算(反向传播中的原子操作)。Fish Speech 的损失可以重复,但导出的权重每次运行都不同;原因待查。峰值 MB 是 memory/peak_mb,即阶段的峰值分配量。

CPU 各轮的 GPU 检查(G1)

只在 CPU 上验证过的代码和设置,在 GPU 基线所用的空闲 GPU 上检查(2026-10-18)。

第一轮之前以来的每一处改动,逐步比较。 用每一步的代码在同一块 GPU 上运行 G0 用例集(冻结的工作树,各用自己的测量工具),每一步的音频与上一步比较:

从 → 到在 GPU 上是否相同
第一轮之前 → 第一轮(8a021de)除 VoxCPM 外都相同(其注意力跨度,见下)
第一轮 → 第二轮(60776a4)除 Fish Speech 外都相同(其原地 Snake,见下)
第二轮 → 注意力分发器(7600f61)全部相同
→ 显存修复(d65c482)全部相同
→ 各引擎的 CUDA Graph(0b70d0f)全部相同(CosyVoice 在 cuda:1 上出错,见下)
→ 0.2.5(4e6fe35)全部相同;且在 cuda:1 上与 G0 的 cuda:0 相同

已修复,使 GPU 上的音频重新与之前的代码相同:

  • VoxCPM 的注意力跨度。 在 CUDA 上一步走 SDPA 的 math 路径,其矩阵向量乘和 softmax 按键的长度划分求和:在下一个 256(或 512 … 4096)的倍数处截断,在 bf16、fp16 和 fp32 下大多数位置都与整个 8192 位置的缓存不同。跨度现在只在 CPU 上使用(minicpm.attend_span);在 GPU 上一步对整个缓存计算,与上游一样。重新与第一轮之前的代码相同,代价是:预热请求 1.24 → 2.13 秒(VoxCPM 2)、0.68 → 0.96 秒(1.5)。
  • Fish Speech 的原地 Snake。 在 GPU 上编解码器在 bf16 autocast 下运行,autocast 以 float32 计算 pow 和 reciprocal;因此上游的 Snake 以 float32 结束,而保持缓冲区数据类型的原地写法停留在 bf16(最多相差 0.1)。原地写法现在只在没有 autocast 时使用;重新与第一轮相同,速度不变。单元测试覆盖了 autocast。
  • CosyVoice 在 cuda:1 上因非法内存访问而出错:其解码步骤的 CUDA Graph 在当前设备为 cuda:0 时捕获和重放,而重放在当前设备的流上运行。现在像其他引擎一样在 torch.cuda.device(graphs.device) 下捕获和重放(加速)。

旋转位置表、IndexTTS 的原地 GELU 和 Fish Speech 的 Snake 在 CUDA 上以 fp32、fp16 和 bf16 给出与参考相同的结果位(有 GPU 时精确性测试现在也在 CUDA 上运行);CosyVoice 的 U-Net 掩码是只构造一次的同一个张量。Fish Speech 在 GPU 上训练时仍在 CPU 生成器上重放上游的初始化,因为上游在 CPU 上构建模型。

显存方案的机制在 CUDA 上(G0 用例集,每次都与 G0 的音频比较):每个模型在 compute.vram_limit_mb 下只加载约一半,其余流式传输,预取分别用 2、1、0 个副流;向每个阶段注入显存不足错误,每个阶段以一半权重流式传输、生成器恢复原状后重新运行;compute.kv_cache = static。全部 56 次运行都相同。

用例(整体加载:预热秒数)部分加载,2 个副流1 个0 个
Qwen3-TTS base-clone-en(1.28)24.6 秒,流式传输 171 GB20.0 秒18.6 秒,156 GB
VoxCPM v2-clone-en(1.24)29.0 秒,325 GB13.9 秒13.5 秒,124 GB
CosyVoice v2-clone-en(1.40)10.1 秒9.2 秒7.4 秒
F5-TTS clone-en(0.92)2.17 秒2.08 秒2.67 秒
OmniVoice clone-en(0.84)2.39 秒2.41 秒3.15 秒
  • 主机到设备的拷贝在锁页内存下为 13.3 GB/s,可分页内存下为 12.3 GB/s:两块卡插在 x8 的 PCIe 4.0 插槽上。
  • 预取对每个请求只运行一次的模型有帮助(F5-TTS、OmniVoice:变慢的时间是流式传输字节所需传输时间的 0.8 倍,在方案的 1.2 倍之内),对自回归模型有害,而且副流越多流式传输越多:预取缓冲占用了留给常驻权重的空间。Qwen3-TTS 的 talker:2 个副流时为传输时间的 1.8 倍,没有副流时 1.3 倍。
  • compute.kv_cache = static 在 CUDA 上结果相同,但并不更快(Qwen3-TTS 1.28 → 1.30 秒,CosyVoice 3 0.95 → 0.95 秒),峰值也相同:默认仍为 dynamic。

方案的目标(§9),来自 scripts/parity/memory.py(每个用例一个进程;它现在可以在 GPU 上运行:上限以 compute.vram_limit_mb 传给运行时):

用例整卡12 GB8 GB6 GB4 GB
Fish Speech clone-en8.3 秒7.2 秒32.2 秒49.3 秒71.2 秒
IndexTTS v2-clone-en5.7 秒5.4 秒5.5 秒5.9 秒34.3 秒
VoxCPM v2-clone-en5.9 秒5.6 秒5.7 秒10.0 秒22.6 秒
Qwen3-TTS base-clone-en5.1 秒4.4 秒4.1 秒4.7 秒15.2 秒
其余八个用例2.5–5.4 秒最多多 1 秒

(新进程中的第一个请求,包括加载)

  • 最小显存:每个模型都能在 4 GB 的卡上运行,包括 Fish Speech S2 Pro(方案的目标是 8 GB)。在两块 GPU 都忙时,0.2.5 的代码有一次在 6 GB 下运行 Fish Speech 时显存不足;之后又运行两次都没有再出现。
  • 有主机副本时卸载用时 0.05–0.12 秒,没有传输。
  • 重新加载达到 5.7–8.3 GB/s,即锁页带宽的 43–62 %,未达到方案的 80 %:主机副本是可分页内存(pinned_bytes 为 0),逐个张量拷贝。

GPU 运行时的设置。 compute.cpu_allocator 和 compute.cpu_threads 对预热时间和加载时间的影响都在波动范围之内(交错两轮,四个引擎)。CUDA 分配器中,expandable 给出相同的音频和权重,保留的显存略少;async 使每个使用 CUDA Graph 的引擎崩溃(捕获后非法内存访问):现在在它之下不捕获 CUDA Graph(graphs.usable),可以运行且结果相同,但没有了 CUDA Graph 的速度(Fish Speech 3.1 → 7.3 秒,Qwen3-TTS 1.3 → 2.0 秒)。训练阶段现在无论主机是否已加载模型,都会在环境中得到分配器设置。

长文本的工作内存(约 30 秒语音,整体加载;之后已校准:显存估算):每个语言模型都远在估计值之内(2–64 %),但解码器超出估计,且文本越长超出越多:VoxCPM 1.5 的 VAE 用 810 MB,估计 290 MB;VoxCPM 2 的用 440 MB,估计 302 MB;Fish Speech 的编解码器用 806 MB,估计 486 MB;IndexTTS 1.5 的 BigVGAN 用 596 MB,估计 369 MB;IndexTTS 2 的用 421 MB,估计 358 MB。阶段实测超出后,校准会调高估计值,第一次则由显存不足重试兜底;这些估计值需要调高。

训练显存预设在 GPU 上,估计值(MethodInfo.memory_mb)对比实测峰值,单位 MB:

微调standardlowlowestlowest 与 standard 是否相同
F5-TTS full7,576 / 8,8215,863 / 7,3953,295 / 5,374是
Qwen3-TTS 1.7B speaker17,088 / 18,28115,861 / 18,2748,493 / 9,132否(略有不同),用时 294 秒而非 20 秒
OmniVoice LoRA4,158 / 7,3013,379 / 2,9713,379 / 2,766否(略有不同)
CosyVoice 2 llm7,134 / 10,5706,643 / 9,6916,643 / 9,691是
VoxCPM 0.5B LoRA4,967 / 4,3432,616 / 2,7471,216 / 2,747是(流式传输的基座也相同)
GPT-SoVITS v2Pro speaker921 / 3,881921 / 3,881921 / 3,881在 GPU 上不能重复
Fish Speech S2 Pro LoRA14,105 / 9,97814,105 / 9,97814,105 / 9,978其导出不能重复

(之后已校准:显存估算。)大多数估计值低于峰值,有的低很多(GPT-SoVITS 低 4 倍),所以运行前的警告在该提醒时没有提醒:需要校准。lowest 使 Qwen3-TTS 的峰值减半,但其主机端优化器使运行慢 15 倍(第一步之前 152 秒,每步 6.2 秒,原为 0.2 秒);其中大部分来自透明大页,现在这类阶段不再使用(见下文 G2)。

GPU 优化(G2)

在空闲 GPU 上测量(2026-10-18),对比 G1 之后代码(aeeba79,冻结)的精确路径,G0 用例集每个用例运行三次。

已完成,结果不变。

  • 只在有用的地方预取(显存):sequence 组件(自回归模型;GPT-SoVITS 的 T2S 现在也是)既不预取,也不为传输中的副本留空间。部分加载(G1 的上限)时,在空闲 GPU 上交错运行:Qwen3-TTS base-clone-en 24.6 → 16.2 秒,VoxCPM v2-clone-en 29.0 → 13.6 秒,CosyVoice v2-clone-en 10.1 → 7.4 秒,都与没有副流时相同;F5-TTS 和 OmniVoice 保留预取。音频与整体加载时相同。
  • compute.compile 下 CosyVoice 的 flow U-Net 每个请求都重新编译(每一层的注意力掩码以掩码对象的身份为键缓存,而 torch.compile 会对此设置守卫):预热请求要 12.8 秒而不是 1.0 秒。现在以 U-Net 的层级为键;编译后 0.97 秒;即时执行的音频与之前相同。
  • IndexTTS 的束缓存放在一个缓冲区中(在这一轮之后完成,对比 fc4b6b3,交错运行两轮):所有层的键和值位于一个缓冲区,所以每一步的束重排是每束一次复制(没有束移动时只复制一次),每层在原处写入新的键和值,而不是每个张量各做一次 index_select 和一次 torch.cat,每次都复制整个张量。两个留有增长空间的缓冲区轮流使用。GPU 和 CPU 上结果都相同。预热:v15-clone-en 0.58 → 0.54 秒,v2-clone-en 2.02 → 1.93 秒,v25-clone-zh 1.73 → 1.67 秒;约 30 秒语音 ×1.11–1.12(1.5 4.79 → 4.29 秒,2 17.57 → 15.81 秒);CPU 上 ×1.02–1.05。
  • OmniVoice 的合并引导(主动开启,请求选项 packed_guidance,在这一轮之后完成,对比 a552038):每一项的有条件序列和无条件序列放在同一行,使用块对角掩码,而不是两行都补齐到有条件序列的长度。精确路径不变(结果相同,速度相同)。合并后,在 3090 上交错运行两轮:clone-en 0.77 → 0.61 秒,finetuned-en 0.76 → 0.60 秒(×1.26–1.27),clone-en-long 3.37 → 3.01 秒(×1.12),design-en 和 auto-zh 不变(0.54 秒:没有参考音频,补齐很少);音频是另一次生成(舍入不同使决定翻转)。CPU 上 clone-en 24.0 → 18.3 秒(×1.31),design-en ×1.02,两者都与精确路径相同。
  • lowest 下的训练(在这一轮之后完成,对比 a1350b3,在 3090 上微调 Qwen3-TTS 1.7B):这个阶段的大部分时间花在内核里,为 AdamW 每一步临时张量占用的透明大页整理内存(采样的调用栈:每一步的 addcdiv_),而页缓存占满了内存。lowest 阶段不再使用大页(compute.cpu_allocator 对其他阶段保留大页,没有大页时它们的微调慢 5–15 %),主机端优化器的副本各放在一个缓冲区中,按 dtype 经过锁页缓冲区传输:201 秒 → 82.8 秒(第一步 59.5 → 13.3 秒,每步 12.8 → 5.5 秒),GPU 峰值 9.1 → 7.3 GB,权重和损失相同;lowest 下的 F5-TTS 和 OmniVoice 不变且相同。每一步其余的时间是 CPU 上 AdamW 的运算。每个训练器标准微调的剖析没有显示数据加载的开销;它们在主机上的时间花在写检查点(F5-TTS 9 秒的阶段中占 4 秒)和模块初始化的随机抽取上,训练器按上游的做法重放这些抽取。

compute.compile(主动开启,inductor 默认模式),预热请求和首次请求的代价,对比即时执行:

引擎预热编译代价音频
F5-TTS×1.34+63 秒SNR 11 dB
GPT-SoVITS v4×1.14+69 秒20 dB
IndexTTS 1.5 / 2、CosyVoice 2 / 3×1.03–1.06+55–134 秒38–53 dB
VoxCPM×0.41–0.64+20–47 秒编译时其 CUDA Graph 关闭
Qwen3-TTS、Fish Speech、GPT-SoVITS v2、OmniVoice——没有编译任何模块

compute.fast(主动开启),对比精确路径:

开关收益损失
sdpa_reduced_precisionVoxCPM ×1.20–1.28(其整个缓存上的注意力)无;其他引擎都完全相同
fp16_accumulationF5-TTS ×1.11,IndexTTS 1.5 ×1.21GPT-SoVITS ×0.6
tf32Qwen3-TTS base ×1.13GPT-SoVITS ×0.67–0.75(音频相同)
cudnn_benchmark测不出改变声码器的结果位
四个一起VoxCPM、F5-TTS、IndexTTS 1.5、Fish Speech、CosyVoice 3、GPT-SoVITS v4 ×1.1–1.3

同时处理多个请求(同时提交 8 段不同文本,不带种子):微批处理(tts.micro_batch)使 Qwen3-TTS 快 2.6 倍(6.34 → 2.42 秒,中位延迟 3.97 → 2.39 秒);对 OmniVoice 只快 10 %,中位延迟翻倍;对 GPT-SoVITS 快 27 %,而不批处理、两个任务同时运行更好(5.32 → 4.00 秒:文本前端与另一个任务的合成重叠)。单独设置 compute.gpu_jobs = 2 对单个引擎没有收益:一个引擎在一个设备上一次只运行一次调用。

尝试过但没有保留。

  • 训练中融合的 AdamW:每步优化快 1.5 倍(0.7 B 个 float32 参数:69 → 47 毫秒),但全量微调的每步时间几乎不变(CosyVoice 2 llm 0.121 → 0.112 秒,VoxCPM full 0.311 → 0.296 秒,Qwen3-TTS speaker 没有变化),且结果会改变:不提供。在 CUDA 上每个训练器默认已经使用 torch 的多张量(foreach)Adam,与上游一样。
  • 把 OmniVoice 每步的前向捕获为 CUDA Graph:结果相同,快 2 %。不开 profiler 时它的语言模型使 GPU 约 90 % 忙碌,可节省的内核发出时间很少。
  • 直接加载到设备:页缓存已热时一次加载用时 1.6–4.8 秒,其中 0.5–1.7 秒是在主机上复制张量,进程中的第一个模型还要付出约 2 秒的导入时间(torch、inflect);直接加载到设备会失去使卸载零开销的主机副本。
  • 无分类器引导合成一批:F5-TTS、CosyVoice、IndexTTS 的 s2mel、OmniVoice 和 VoxCPM 本来就是这样(与上游一样);GPT-SoVITS v3–v5 的 CFM 调用两次估计器,合成一批会改变结果位。

训练的 low 预设(梯度检查点)对比方案的每步 +35 %:F5-TTS +6 %,VoxCPM +51 %,Qwen3-TTS +80 %,OmniVoice +104 %(G0 的运行)。

对比 0.1,以及 GPU 上的契约测试(G3)

原生实现对比 0.1(引擎工作进程版本,运行上游的代码;标签 v1-engine-workers),在空闲的 cuda:0 上测量(2026-10-18),双方都用各自的 GPU 默认设置:G0 用例集去掉 Fish Speech(0.1 的克隆在 GPU 上失败,Fish Speech)和 VoxCPM(0.1 没有),每一方一个进程,每个用例运行三次。预热取第 2–3 次中最快的一次;首次请求包含加载。

用例预热,0.1 → 原生提升首次请求,0.1 → 原生
Qwen3-TTS cv-en-aiden4.04 → 0.78 秒×5.213.1 → 4.5 秒
Qwen3-TTS base-clone-en6.23 → 1.26 秒×4.98.1 → 3.5 秒
IndexTTS v15-clone-en1.32 → 0.57 秒×2.315.0 → 3.5 秒
IndexTTS v2-clone-en3.09 → 2.01 秒×1.517.4 → 4.8 秒
GPT-SoVITS v2-clone-zh1.22 → 0.56 秒×2.211.9 → 4.8 秒
GPT-SoVITS v4-clone-zh1.39 → 0.93 秒×1.55.6 → 4.0 秒
CosyVoice v2-clone-en2.11 → 1.01 秒×2.1209.5 → 5.6 秒
CosyVoice v3-clone-en1.75 → 0.94 秒×1.916.4 → 2.9 秒
F5-TTS clone-en0.98 → 0.92 秒×1.0610.9 → 3.7 秒
OmniVoice clone-en0.79 → 0.78 秒×1.017.7 → 3.6 秒

每个用例中原生实现都至少一样快,预热请求和首次请求都是;每个原生用例三次运行的音频都相同。第一次测量与下面限制显存的契约测试同时运行(主机有 10 GB 被换出),OmniVoice 慢了 12 %,两个原生加载超过 100 秒:这类对比要在空闲的机器上测量。首段音频时间和峰值内存:见下面的 G8。

GPU 上的契约测试(测试),所有已安装的模型,包括需要接受许可的引擎:整块 cuda:0 上 144 个通过(126 个用时 52 分钟,另有 VoxCPM 自己数据文件夹中的 18 个),cuda:1 限制为 4 GB 时 144 个通过(MIZUOTO_TEST_SETTINGS='{"compute": {"vram_limit_mb": 4096}}',小于大模型(Fish Speech S2 Pro、IndexTTS 2、Qwen3-TTS 1.7B)所需,所以它们在整个栈中流式加载、部分加载;2 小时 6 分钟)。限制显存时最慢的测试是一个很长的 Qwen3-TTS 请求(旁白微调模型的种子测试:单独运行 2,778 秒,整体加载时 322 秒):流式加载的自回归模型每生成一个 token 都要复制一次它的流式层。

与 0.1 对比首段音频和内存(G8)

2026-10-19,一张 RTX 3090(cuda:0),机器空闲,G0 用例以流式运行(实验工作区的 perf/g8/:run.sh、single.sh、monitor.py,每 0.2 秒采样显卡已用显存和进程树的内存;0.1 的引擎 worker 与其主进程一起计入)。温请求和首段音频时间取第 2–3 次运行中最好的一次;stack.py 记录 first_chunk_seconds。

用例温请求,0.1 → 原生首段音频,0.1 → 原生
Qwen3-TTS base-clone-en6.38 → 1.52 s6.38 → 0.36 s
Qwen3-TTS cv-en-aiden4.14 → 0.89 s4.13 → 0.32 s
F5-TTS clone-en0.97 → 0.91 s0.97 → 0.91 s
OmniVoice clone-en0.78 → 0.76 s0.78 → 0.76 s
CosyVoice v2-clone-en2.93 → 1.64 s2.17 → 1.14 s
CosyVoice v3-clone-en2.02 → 1.31 s1.98 → 1.30 s
GPT-SoVITS v2-clone-zh1.10 → 0.44 s0.55 → 0.30 s
GPT-SoVITS v4-clone-zh1.41 → 0.93 s1.41 → 0.93 s
IndexTTS v1.5-clone-en1.26 → 0.53 s1.26 → 0.53 s
IndexTTS v2-clone-en3.00 → 1.92 s2.99 → 1.91 s

首段音频从不晚于 0.1;0.1 返回整段音频的地方(Qwen3-TTS)早了 12–18 倍。每次运行一个模型(每个栈一个用例)时的峰值内存,0.1 → 原生 → 原生且 compute.host_copies = off:

模型显存 MB内存 MB(匿名)
Qwen3-TTS 1.7B CustomVoice4,493 → 4,495 → 4,4871,558 → 5,235 → 4,244
CosyVoice 23,375 → 3,651 → 3,6474,885 → 4,231 → 3,445
GPT-SoVITS v21,973 → 1,509 → 1,5072,959 → 3,779 → 3,467
IndexTTS 29,337 → 6,907 → 6,8897,459 → 8,537 → 8,318

显存与 0.1 相差不超过 10 % 或更少(CosyVoice 2 多 8 %,GPT-SoVITS 和 IndexTTS 少四分之一)。内存在 0.1 直接加载到 GPU 的地方(transformers 的 device_map,Qwen3-TTS)更高:Mizuoto 先在内存中构建每个模型,再由模型管理器放置,并默认把它保留为主机副本,这样卸载时无需传输(内存);关闭主机副本时,峰值就是这一步的暂存。0.1 的可选加速器(vLLM、TensorRT)在这里的任何环境中都没有安装,没有对比。

显存估算的校准

在 RTX 3090 上测量(2026-10-18),测试工具现在记录每个阶段中最紧的那一次调用(工作内存对比它自己的估算;一个阶段最大的工作内存和最大的估算可能来自不同的调用,例如参考音频的编码和解码)。

解码器,每个输出位置的字节数(在短文本和约 30 秒的文本上测量,再加五分之一):VoxCPM 的 VAE 解码每个位置 1,277(1.5)和 643(2),原估算 256;参考音频编码每个采样 1,395 和 3,100,原估算 256;Fish Speech 的编解码器每个采样 1,922(自 G1 起非原地的 Snake 用得更多),原估算 384;IndexTTS 的 BigVGAN 1,057(2)以及 771 加 256 MB(1.5),原估算 256。提高之后,每一次解码器调用,无论冷启动还是预热、短文本、长文本还是 8 条一批(OmniVoice、GPT-SoVITS),都不超过估算的 82 %(IndexTTS 1.5 的 BigVGAN;Fish 的编解码器 73 %,VoxCPM 的 VAE 74 %);所有阶段中最接近估算的是长文本上 IndexTTS 2 的 GPT,90 %,未改动且在估算之内。Qwen3-TTS 1.7B 在 8 GB 上限下完整加载;在 4 GB 下它的 talker 部分加载,meta.memory.partial 和日志说明是哪个组件、为什么、预算多少。

训练,每种方法以配方的默认批量运行(G1 用的是测试夹具较小的批量),各预设下的峰值(MB):

微调standardlowlowest
CosyVoice 2 / 3 llm(以及 2 的 llm_flow)10,319 / 10,3989,691 / 9,6939,691 / 9,693
F5-TTS full8,8217,3955,374
Fish Speech S2 Pro LoRA,批量 1 / 2 / 412,267 / 15,641 / 显存不足相同相同
GPT-SoVITS speaker(批量 4;批量 2:3,881)4,6764,6764,670
OmniVoice full / LoRA11,724 / 7,30111,714 / 2,9714,713 / 2,766
Qwen3-TTS 1.7B speaker18,27618,2677,356
VoxCPM 0.5B full16,73312,71912,719
VoxCPM 0.5B / 1.5 / 2 LoRA(批量 16;0.5B 批量 2:4,343)8,661 / 10,222 / 14,4983,116 / 3,301 / 9,0942,970 / 3,301 / 9,094

原来的经验公式几乎都低于这些峰值(GPT-SoVITS 低 5 倍,VoxCPM 的 lowest LoRA 低至 3.4 倍),对 Fish Speech 的 LoRA 估算为 14.1 GB,而它在默认批量下会超出 24 GB 显卡。现在的估算是这些峰值按基础模型大小缩放,再加一成和 512 MB,峰值随批量变化的地方也随批量调整;没有预设放得下时,警告会给出放得下的批量。

GPU 上的注意力后端

在 RTX 3090 上测量(2026-10-18),torch 2.8.0+cu128、flash-attn 2.8.3(torch 2.8 唯一的 wheel)、xFormers 0.0.32.post2(单独一个环境:它与 flash-attn 2.8.3 不能同时导入)和 SageAttention 1.0.6(Triton;2.x 需要 nvcc,SageAttention 3 需要 Blackwell GPU)。从 G0 用例集的请求中记录了每个调用点的调用(17 个调用点,36 种调用),每个能接受某次调用的后端都在它上面运行,并与 auto 的结果比较。

内核。 FlashAttention 和 xFormers 与 auto 的差别处处都在 bf16/fp16 的舍入以内(余弦 1.000000);SageAttention 会量化:在 BERT 上余弦 0.98,在 VoxCPM 的双向 DiT 注意力上 0.78(最大误差 12)。在这些尺寸下,这些包每次调用都比 SDPA 慢(单查询步骤 0.13–0.5 倍,提示 0.15–0.9 倍),只有 FlashAttention 能跳过填充键的地方例外:VoxCPM 在预分配缓存上的步骤,4.4 倍(head dim 128)和 2.3 倍(64)。

请求,预热时间对比 auto,音频对比 auto 的音频(G0 用例集,一块 GPU,空闲,每个用例三次;"另一次生成":同一请求的另一种结果,如同换了一个种子):

用例sdpaflashxformerssage(设置 approx_ok 之前)fast(现在)
Qwen3-TTS base / 自定义音色1.00 相同1.02 / 0.99 另一次生成0.98 / 0.94 另一次生成无休止:112 倍 / 6 倍的音频1.00 相同
CosyVoice 2 / 3(fp32 调用点)1.07 相同1.07 相同1.00 / 0.97 相同1.07 相同1.07 相同
F5-TTS1.00 相同1.00,37 dB1.01,26 dB0.99,21 dB1.00,37 dB
OmniVoice1.01 相同(下面的修复之前 0.71)1.01 相同0.98 相同1.01 相同1.01 相同
GPT-SoVITS v2 / v40.93 / 0.99 相同0.91 / 0.96 另一次生成0.62 / 0.730.87 / 0.960.91 相同 / 0.98,34 dB
Fish Speech1.05 另一次生成1.081.13(拒绝:布尔掩码)1.05 另一次生成
IndexTTS 1.5 / 21.06 / 1.010.82 / 1.000.97 / 0.97无休止:6 倍 / 1.001.01 / 1.00
VoxCPM 1.5 / 21.00 相同1.39 / 1.65 另一次生成0.76 / 0.98无休止:3–4 倍的音频1.41 / 1.57 另一次生成

训练(每个训练器的测试夹具,training.attention):在调用点的参照就是普通 SDPA 的地方(F5-TTS、Qwen3-TTS、VoxCPM),sdpa 给出与 auto 相同的损失和权重;FlashAttention 和 xFormers 的损失相差不超过 1e-3,Qwen3-TTS 第一步相差 0.25 %,之后它的 bf16 训练轨迹逐渐偏离(任何舍入变化都会如此);峰值显存和每步时间变化不到 7 %。回退在真实内核上(布尔掩码交给 flash、head dim 80 或需要梯度的调用交给 SageAttention、fp32 交给任一个、无法导入的 xFormers)都带着原因回退,给出参照的结果;严格模式拒绝它们。

发现并修复: 直接调用 SDPA 的 math 内核时,它把布尔掩码当作数值相加(math 后端和 Fish Speech 的解码步骤;见 Fish Speech);SageAttention 在当前设备以外的 GPU 上出错;Qwen 的注意力在策略下把带掩码的分组头交给 SDPA 的 math 内核(任何策略下 OmniVoice 慢到 0.71 倍且数值不同),现在按参照的做法先重复头。

决定: NVIDIA 上的 fast 对键有填充的 fp16/bf16 调用使用 FlashAttention,其余使用 SDPA(PADDED_CHAIN);SageAttention 和 xFormers 移出它的链,仍可按名称选用;自回归调用点拒绝量化后端(approx_ok=False);auto 仍是默认值(D-A6:参照为普通 SDPA 的地方 sdpa 给出相同音频,并不更快)。doctor 给出的 flash-attn 安装命令会说明 torch 2.8 下它与 xFormers 互斥。

部分加载的模型

在 RTX 3090 上测量(2026-10-18),每个模型都设了限额(compute.vram_limit_mb),使其解码器流式加载;比较的是预热后的请求与之前的代码,每个用例三次运行,并对比音频。

重新加载一个已卸载到内存的组件,复制速度原为 7–9.8 GB/s,是这块卡从锁页内存读取速度(13.45 GB/s)的 52–73 %:保留的主机副本是可分页内存,每次复制都要等上一次完成。现在它们在组件加载时就地锁页(runtime/pinning.py,见显存);卸载、首次加载和峰值显存都不变:

重新加载(整块 GPU)MB可分页就地锁页
CosyVoice 22,9370.408 s,7.55 GB/s0.365 s,8.43 GB/s
F5-TTS7030.092 s,8.02 GB/s0.083 s,8.90 GB/s
Fish Speech S2 Pro9,4561.012 s,9.80 GB/s0.947 s,10.47 GB/s
GPT-SoVITS v21,0260.154 s,6.98 GB/s0.149 s,7.24 GB/s
IndexTTS 26,9630.823 s,8.87 GB/s0.766 s,9.53 GB/s
OmniVoice1,8690.230 s,8.53 GB/s0.197 s,9.95 GB/s
Qwen3-TTS 1.7B Base4,0040.471 s,8.91 GB/s0.418 s,10.05 GB/s
VoxCPM 24,7750.554 s,9.03 GB/s0.485 s,10.33 GB/s

复制本身现在达到 13.2 GB/s(链路的 98 %,用 talker 自己的主机张量测得);重新加载剩下的时间是重新分配显存,talker 为 0.08–0.1 s,因为让出显存时会清空分配器的缓存(有意为之:让其他进程拿回显存)。可扩展段(expandable segments)分配得并不更快。较小的模型有更多小张量,它们(小于 1 MB)仍为可分页内存。在限额下,流式加载器原本在第一个请求时以 1.5 GB/s 把流式权重复制到锁页内存中;改为就地注册后,冷启动请求变快:F5-TTS 5.40 → 4.99 s,OmniVoice 5.96 → 5.07 s,CosyVoice 2/3 20.82/16.93 → 19.36/14.10 s,GPT-SoVITS v4 6.51 → 5.65 s,Fish Speech 44.89 → 41.23 s,IndexTTS 2 36.10 → 33.54 s,VoxCPM 2 29.12 → 26.85 s,Qwen3-TTS 9.22/10.90 → 8.57/9.86 s;预热后的请求与所有输出都相同。

流式加载时,Qwen3-TTS 在限额下受算子启动限制,而不是受复制限制:12.9 s 中 GPU 只忙了 7.5 s,因为流式加载的 talker 不用 CUDA 图解码。现在,层留在锁页内存中的流式 sequence 句柄也用图运行,每步的图把流式加载的权重复制进来(见显存):

用例(限额)流式加载时 eager流式加载时用图音频
Qwen3-TTS 1.7B 预置说话人(4 GB)6.62 s5.00 s(1.32×)相同
Qwen3-TTS 1.7B Base 克隆(4 GB)10.45 s7.75 s(1.35×)相同
CosyVoice 2 / 3(1.5 GB)14.93 / 12.46 s15.09 / 12.57 s相同
GPT-SoVITS v2 / v4(640 MB)0.90 / 2.60 s0.90 / 2.61 s相同
Fish Speech(6 GB)36.58 s36.35 s相同
IndexTTS 1.5 / 2(3 GB)1.88 / 30.76 s1.87 / 30.83 s相同
VoxCPM 2 / 1.5(2.5 GB)22.96 / 3.88 s24.07 / 8.62 s相同

其他模型原本就受复制限制(约 12.4 GB/s),VoxCPM 1.5 还慢了 2.2 倍,所以只有 Qwen3-TTS 在流式加载时用图;meta.memory.streamed_mb 计入重放中的复制(与 eager 时的计数相差不到 2 %)。

显存模式与真实的显存不足

显存工作的最后一道检验,在 RTX 3090 上测量(2026-10-19):G0 用例在 compute.vram_mode=high 和 none 下各运行三次,与同一块 GPU 上新跑的默认一轮对照;以及 none 下的契约测试。

用例auto:预热后耗时、峰值highnone:预热后耗时、峰值音频
Qwen3-TTS Base / CustomVoice1.27 s、8.3 GB / 0.82 s、4.0 GB相同30.3 s、2.1 GB / 19.1 s、1.0 GB相同
CosyVoice 2 / 31.05 s、3.1 GB / 1.05 s、7.4 GB相同14.9 s、0.8 GB / 12.4 s、1.6 GB相同
F5-TTS0.91 s、0.8 GB相同2.06 s、0.2 GB相同
Fish Speech S2 Pro3.23 s、9.9 GB相同106 s、1.6 GB相同
GPT-SoVITS v2 / v40.64 s / 1.06 s相同0.95 s / 2.34 s、0.3 / 0.6 GB相同
IndexTTS 1.5 / 20.55 s / 1.94 s、8.2 GB相同7.7 s / 30.8 s、1.5 GB相同
OmniVoice0.78 s、2.0 GB相同2.15 s、0.6 GB相同
VoxCPM 1.5 / 20.97 s / 2.14 s相同8.8 s / 23.6 s、1.0 GB相同

none 在每个阶段流式加载每个单元,所以模型只用其显存的十分之一到五分之一;自回归模型每个 token 都要复制一遍流式加载的层,时间主要花在这里,而较小的模型(F5-TTS、OmniVoice、GPT-SoVITS)慢不到约 2.5 倍。none 下的契约测试(在 cuda:1 上运行所有已安装的模型,包括需要接受许可的):165 项通过(3 项跳过:不支持流式输出的模型),耗时 3 小时 20 分。

真实的显存不足,而不是测试中注入的:在 8 或 12 GB 限额下(分配器也一并限额),一个线程在请求生成期间占满剩余显存,直到只剩 64 KB 的碎片。Qwen3-TTS 的 talker 和 IndexTTS 2 的 GPT(在两个时刻)都在分配器中真正用尽了显存,然后把一半权重改为流式加载后重新运行(oom_retries、警告,以及 meta.memory.partial 中的“capped at half its weights after running out of memory”),给出的音频与未受干扰的运行逐采样相同。如果晚一点才占满(talker 已经结束),编解码器解码器放置时只看到 218 MB 可用,于是由 talker 让出显存:没有报错,音频相同。(单独设置 compute.vram_limit_mb 只告知运行时,并不限制分配器:那样占满的就是整块 GPU,所以这次检验也限制了分配器。)

在 CPU 上的 torch.compile

compute.compile,fp32,8 线程;被编译模块预热后的耗时:

引擎模块变化
F5-TTSDiT−23 %(整个请求 60 → 45 秒)
IndexTTSs2mel DiT / BigVGAN−7 % / −20 %
GPT-SoVITS v4CFM 估计器−7 %
CosyVoiceflow 估计器无(它的卷积和矩阵乘法已经是库内核)
VoxCPM(bf16)局部 DiT无

第一个请求要付出编译时间:多 30 到 160 秒。音频不完全相同(加速)。

因改变结果而没有采用的做法

  • bf16 或量化权重、其他线程数、GELU 的 tanh 近似、Fish Speech 重复头的 SDPA 分组查询路径、默认使用静态 KV 缓存。
  • 把 VoxCPM 的注意力截到恰好已填充的位置:bf16 下相同,fp32 下不同(内核的分块方式变了);在 256 的倍数处截断两者都相同。
  • foreach=True 的 Adam/AdamW:对 float32 参数结果相同,但在 CPU 上更慢(100 个 2048 × 2048 张量每步 1032 毫秒对 748 毫秒;Qwen3-TTS 训练 ×0.92)。fused=True 的 AdamW:结果不同。
  • GPT-SoVITS 判别器在一步内缓存权重归一化:梯度会以另一种顺序求和。
  • Qwen3-TTS 在 CUDA 上:按 256 分桶并带掩码的静态缓存(另一个 SDPA 内核),以及把长文本的各段合成一批(抽样不同)。

本页目录