性能记录
在结果不变的规则下怎样测量 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-en | 5.44 | 34.25 | talker 30.2,codec decoder 4.0 |
| Qwen3-TTS cv-en-aiden | 3.76 | 20.72 | talker 19.9,codec decoder 0.9 |
| F5-TTS clone-en | 4.74 | 69.75 | transformer 69.7 |
| OmniVoice clone-en | 4.82 | 32.48 | language model 32.3 |
| CosyVoice v2-clone-en | 6.84 | 18.67 | language model 12.1,flow 6.5 |
| CosyVoice v3-clone-en | 4.52 | 16.71 | flow 9.6,language model 7.1 |
| GPT-SoVITS v2-clone-zh | 7.34 | 4.42 | gpt 3.4,sovits 0.9 |
| GPT-SoVITS v4-clone-zh | 3.96 | 31.45 | sovits(CFM + 声码器)29.6,gpt 1.8 |
| Fish Speech clone-en | 4.32 | 110.55 | language model 107.3,codec 3.3 |
| IndexTTS v2-clone-en | 5.17 | 57.20 | gpt 39.3,s2mel 15.2,bigvgan 2.6 |
| IndexTTS v15-clone-en | 3.71 | 45.89 | gpt 44.2,bigvgan 1.7 |
| VoxCPM v2-clone-en | 6.72 | 76.91 | language model 75.6,vae 1.3 |
| VoxCPM v15-clone-en | 5.60 | 21.82 | language 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-en | 75.34 → 36.28 | ×2.08 |
| F5-TTS clone-en | 68.14 → 41.22 | ×1.65 |
| VoxCPM v15-clone-en | 21.42 → 14.40 | ×1.49 |
| CosyVoice v2-clone-en | 18.55 → 14.70 | ×1.26 |
| CosyVoice v3-clone-en | 16.66 → 13.58 | ×1.23 |
| IndexTTS v2-clone-en | 52.07 → 43.97 | ×1.18 |
| Qwen3-TTS cv-en-aiden | 20.71 → 18.08 | ×1.15 |
| Qwen3-TTS base-clone-en | 34.22 → 30.63 | ×1.12 |
| Fish Speech clone-en | 110.43 → 98.36 | ×1.12 |
| GPT-SoVITS v4-clone-zh | 29.24 → 26.38 | ×1.11 |
| IndexTTS v15-clone-en | 40.12 → 36.65 | ×1.09 |
| OmniVoice clone-en | 28.73 → 27.92 | ×1.03 |
| GPT-SoVITS v2-clone-zh | 4.32 → 4.44 | 在波动范围内 |
训练,每个训练器一次微调(秒),指标和权重张量完全相同(训练阶段通过环境变量得到 CPU 设置):
| 引擎、配方 | 运行之前 → 之后 | 加速比 |
|---|---|---|
| F5-TTS full,1 个 epoch | 34.8 → 28.8 | ×1.21 |
| CosyVoice 2 llm,2 个 epoch | 98.5 → 82.9 | ×1.19 |
| Fish Speech S2 Pro LoRA,2 步 | 207.4 → 175.9 | ×1.18 |
| Qwen3-TTS 1.7B speaker,1 个 epoch | 121.2 → 103.4 | ×1.17 |
| VoxCPM 0.5B LoRA,3 步 | 29.8 → 26.4 | ×1.13 |
| GPT-SoVITS v2Pro speaker,1 + 1 个 epoch | 246.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.8 | 70.1 |
| 关闭图 | 47.7 | 50.7 | |
| 默认 | 24.6 | 25.4 | |
| qwen-tts-webui 默认(图、合并投影) | 23.3 | 24.5 | |
| qwen-tts-webui,图,不合并 | 28.6 | 29.7 | |
| 2.10.0+cu130 | 之前 | 67.4 | 71.5 |
| 关闭图 | 39.3 | 40.3 | |
| 默认 | 20.9 | 20.9 | |
| qwen-tts-webui 默认 | 18.4 | 19.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+cu128 | 24.6 / 25.5 | 23.7 / 24.6 | 24.1 / 24.5 |
| 2.10.0+cu130 | 20.0 / 20.8 | 19.2 / 20.0 | 17.9 / 19.9 |
快 3–4 %,在共用 GPU 的波动范围内(两轮之间 ±1 毫秒)与 qwen-tts-webui 持平。
真实的 1.7B Base 模型(2026-10-19,空闲的 RTX 3090,torch 2.8.0+cu128,经完整链路进行中文和英文音色克隆,每项四次运行,两轮交替;每帧最佳毫秒数、实时因子中位数、峰值显存):
| 精确(默认) | fused_projections | qwen-tts-webui 的合并快速路径 | |
|---|---|---|---|
| clone-zh | 17.00–17.06 ms,RTF 0.213,4.03 GB | 16.21–16.24 ms,0.203,4.03 GB | 16.23–16.31 ms,0.203,4.58 GB |
| clone-en | 17.12–17.17 ms,RTF 0.214,4.03 GB | 16.23–16.25 ms,0.203,4.03 GB | 16.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.8 | 96.8 |
| 关闭图 | 43.4 | 44.3 |
| 默认 | 38.9 | 38.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.6 | 70.7 |
| 关闭图 | 14.3 | 14.1 |
| 默认 | 9.9 | 10.3 |
约 ×7:一个 token 对应 40 毫秒音频,所以语言模型从比实时慢 1.7 倍变成比实时快 4 倍。三个种子(每个约 400 个 token,直到 EOS)在开关图时 token 都相同。
VoxCPM
真实权重上的完整请求(含 VAE),6.4–6.9 秒音频,三次预热请求中最快的一次,torch 2.8:
| 模型 | 之前 | 关闭图 | 默认 |
|---|---|---|---|
| 0.5B | 4.49 秒(RTF 0.65) | 4.32 秒 | 2.36 秒(RTF 0.34) |
| 1.5 | 3.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-zh | 1.61 秒 | 1.26 秒 | 0.89 秒 | ×1.81 |
| v2-batch-cut5-zh | 3.72 秒 | 2.90 秒 | 2.29 秒 | ×1.62 |
| v2pro-serial-en | 1.78 秒 | 1.35 秒 | 0.89 秒 | ×2.00 |
| v4-batch-cut5-zh | 4.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.5 | 19.2 |
| 关闭图 | 6.0 | 8.6 |
| 默认 | 5.9 | 5.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-en | 5.84 | 1.28 | 0.22 | 17.7(7.9) | talker 1.22、codec decoder 0.05 | 8,296 |
| Qwen3-TTS cv-en-aiden | 3.68 | 0.79 | 0.22 | 20.0(8.8) | talker 0.76、codec decoder 0.03 | 4,004 |
| F5-TTS clone-en | 4.74 | 0.92 | 0.19 | 6.5(1.9) | transformer 0.90 | 758 |
| OmniVoice clone-en | 5.05 | 0.84 | 0.17 | 6.7(2.3) | language model 0.81 | 1,963 |
| CosyVoice v2-clone-en | 5.00 | 1.40 | 0.28 | 19.7(7.0) | language model 0.91、flow 0.47 | 3,107 |
| CosyVoice v3-clone-en | 3.92 | 0.95 | 0.24 | 14.9(6.7) | language model 0.50、flow 0.44 | 7,352 |
| GPT-SoVITS v2-clone-zh | 6.30 | 0.54 | 0.09 | 9.5(2.4) | gpt 0.43、sovits 0.03 | 1,109 |
| GPT-SoVITS v4-clone-zh | 3.68 | 1.19 | 0.32 | 14.4(4.4) | sovits 0.73、gpt 0.38 | 2,860 |
| GPT-SoVITS v2-stream-zh(流式) | 7.02 | 0.96,首段音频 0.95 | 0.14 | — | sovits+t2s 0.83 | 2,866 |
| Fish Speech clone-en | 4.04 | 3.05 | 0.76 | 13.6(4.1) | language model 3.01、codec 0.04 | 9,769 |
| IndexTTS v15-clone-en | 3.97 | 0.57 | 0.14 | 6.4(2.2) | gpt 0.49、bigvgan 0.06 | 1,978 |
| IndexTTS v2-clone-en | 3.97 | 2.03 | 0.51 | 40.2(18.6) | gpt 1.22、s2mel 0.69、bigvgan 0.10 | 7,982 |
| VoxCPM v2-clone-en | 5.92 | 1.24 | 0.21 | 12.8(4.8) | language model 1.21、vae 0.02 | 5,270 |
| VoxCPM v15-clone-en | 4.80 | 0.68 | 0.14 | 5.1(2.0) | language model 0.65、vae 0.02 | 7,387 |
| VoxCPM v2-stream-en(流式) | 3.68 | 1.02,首段音频 0.14 | 0.28 | — | model+vae 1.02 | 5,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:
| 引擎、配方 | 运行秒数 | 每步秒数 | 峰值 MB | low 是否相同 |
|---|---|---|---|---|
| F5-TTS full,1 个 epoch | 23.5 → 22.9 | 0.18 → 0.19 | 8,821 → 7,395 | 是 |
| Qwen3-TTS 1.7B speaker,1 个 epoch | 20.1 → 22.4 | 0.20 → 0.36 | 18,280 → 18,274 | 是 |
| OmniVoice LoRA,3 步 | 24.9 → 18.7 | 0.24 → 0.49 | 7,300 → 2,971 | 是 |
| CosyVoice 2 llm,2 个 epoch | 39.5 → 34.9 | — | 10,570 → 9,690 | 是 |
| VoxCPM 0.5B LoRA,3 步 | 21.0 → 22.0 | 0.41 → 0.62 | 4,343 → 2,747 | 是 |
| GPT-SoVITS v2Pro speaker,1 + 1 个 epoch | 44.2 → 44.2 | — | 3,881 → 3,881 | 在 GPU 上不能重复 |
| Fish Speech S2 Pro LoRA,2 步 | 59.6 → 48.7 | 0.57 → 0.57 | 9,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 GB | 20.0 秒 | 18.6 秒,156 GB |
| VoxCPM v2-clone-en(1.24) | 29.0 秒,325 GB | 13.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 GB | 8 GB | 6 GB | 4 GB |
|---|---|---|---|---|---|
| Fish Speech clone-en | 8.3 秒 | 7.2 秒 | 32.2 秒 | 49.3 秒 | 71.2 秒 |
| IndexTTS v2-clone-en | 5.7 秒 | 5.4 秒 | 5.5 秒 | 5.9 秒 | 34.3 秒 |
| VoxCPM v2-clone-en | 5.9 秒 | 5.6 秒 | 5.7 秒 | 10.0 秒 | 22.6 秒 |
| Qwen3-TTS base-clone-en | 5.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:
| 微调 | standard | low | lowest | lowest 与 standard 是否相同 |
|---|---|---|---|---|
| F5-TTS full | 7,576 / 8,821 | 5,863 / 7,395 | 3,295 / 5,374 | 是 |
| Qwen3-TTS 1.7B speaker | 17,088 / 18,281 | 15,861 / 18,274 | 8,493 / 9,132 | 否(略有不同),用时 294 秒而非 20 秒 |
| OmniVoice LoRA | 4,158 / 7,301 | 3,379 / 2,971 | 3,379 / 2,766 | 否(略有不同) |
| CosyVoice 2 llm | 7,134 / 10,570 | 6,643 / 9,691 | 6,643 / 9,691 | 是 |
| VoxCPM 0.5B LoRA | 4,967 / 4,343 | 2,616 / 2,747 | 1,216 / 2,747 | 是(流式传输的基座也相同) |
| GPT-SoVITS v2Pro speaker | 921 / 3,881 | 921 / 3,881 | 921 / 3,881 | 在 GPU 上不能重复 |
| Fish Speech S2 Pro LoRA | 14,105 / 9,978 | 14,105 / 9,978 | 14,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_precision | VoxCPM ×1.20–1.28(其整个缓存上的注意力) | 无;其他引擎都完全相同 |
fp16_accumulation | F5-TTS ×1.11,IndexTTS 1.5 ×1.21 | GPT-SoVITS ×0.6 |
tf32 | Qwen3-TTS base ×1.13 | GPT-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
llm0.121 → 0.112 秒,VoxCPMfull0.311 → 0.296 秒,Qwen3-TTSspeaker没有变化),且结果会改变:不提供。在 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-aiden | 4.04 → 0.78 秒 | ×5.2 | 13.1 → 4.5 秒 |
| Qwen3-TTS base-clone-en | 6.23 → 1.26 秒 | ×4.9 | 8.1 → 3.5 秒 |
| IndexTTS v15-clone-en | 1.32 → 0.57 秒 | ×2.3 | 15.0 → 3.5 秒 |
| IndexTTS v2-clone-en | 3.09 → 2.01 秒 | ×1.5 | 17.4 → 4.8 秒 |
| GPT-SoVITS v2-clone-zh | 1.22 → 0.56 秒 | ×2.2 | 11.9 → 4.8 秒 |
| GPT-SoVITS v4-clone-zh | 1.39 → 0.93 秒 | ×1.5 | 5.6 → 4.0 秒 |
| CosyVoice v2-clone-en | 2.11 → 1.01 秒 | ×2.1 | 209.5 → 5.6 秒 |
| CosyVoice v3-clone-en | 1.75 → 0.94 秒 | ×1.9 | 16.4 → 2.9 秒 |
| F5-TTS clone-en | 0.98 → 0.92 秒 | ×1.06 | 10.9 → 3.7 秒 |
| OmniVoice clone-en | 0.79 → 0.78 秒 | ×1.01 | 7.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-en | 6.38 → 1.52 s | 6.38 → 0.36 s |
| Qwen3-TTS cv-en-aiden | 4.14 → 0.89 s | 4.13 → 0.32 s |
| F5-TTS clone-en | 0.97 → 0.91 s | 0.97 → 0.91 s |
| OmniVoice clone-en | 0.78 → 0.76 s | 0.78 → 0.76 s |
| CosyVoice v2-clone-en | 2.93 → 1.64 s | 2.17 → 1.14 s |
| CosyVoice v3-clone-en | 2.02 → 1.31 s | 1.98 → 1.30 s |
| GPT-SoVITS v2-clone-zh | 1.10 → 0.44 s | 0.55 → 0.30 s |
| GPT-SoVITS v4-clone-zh | 1.41 → 0.93 s | 1.41 → 0.93 s |
| IndexTTS v1.5-clone-en | 1.26 → 0.53 s | 1.26 → 0.53 s |
| IndexTTS v2-clone-en | 3.00 → 1.92 s | 2.99 → 1.91 s |
首段音频从不晚于 0.1;0.1 返回整段音频的地方(Qwen3-TTS)早了 12–18 倍。每次运行一个模型(每个栈一个用例)时的峰值内存,0.1 → 原生 → 原生且 compute.host_copies = off:
| 模型 | 显存 MB | 内存 MB(匿名) |
|---|---|---|
| Qwen3-TTS 1.7B CustomVoice | 4,493 → 4,495 → 4,487 | 1,558 → 5,235 → 4,244 |
| CosyVoice 2 | 3,375 → 3,651 → 3,647 | 4,885 → 4,231 → 3,445 |
| GPT-SoVITS v2 | 1,973 → 1,509 → 1,507 | 2,959 → 3,779 → 3,467 |
| IndexTTS 2 | 9,337 → 6,907 → 6,889 | 7,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):
| 微调 | standard | low | lowest |
|---|---|---|---|
CosyVoice 2 / 3 llm(以及 2 的 llm_flow) | 10,319 / 10,398 | 9,691 / 9,693 | 9,691 / 9,693 |
F5-TTS full | 8,821 | 7,395 | 5,374 |
| Fish Speech S2 Pro LoRA,批量 1 / 2 / 4 | 12,267 / 15,641 / 显存不足 | 相同 | 相同 |
GPT-SoVITS speaker(批量 4;批量 2:3,881) | 4,676 | 4,676 | 4,670 |
OmniVoice full / LoRA | 11,724 / 7,301 | 11,714 / 2,971 | 4,713 / 2,766 |
Qwen3-TTS 1.7B speaker | 18,276 | 18,267 | 7,356 |
VoxCPM 0.5B full | 16,733 | 12,719 | 12,719 |
| VoxCPM 0.5B / 1.5 / 2 LoRA(批量 16;0.5B 批量 2:4,343) | 8,661 / 10,222 / 14,498 | 3,116 / 3,301 / 9,094 | 2,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,空闲,每个用例三次;"另一次生成":同一请求的另一种结果,如同换了一个种子):
| 用例 | sdpa | flash | xformers | sage(设置 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-TTS | 1.00 相同 | 1.00,37 dB | 1.01,26 dB | 0.99,21 dB | 1.00,37 dB |
| OmniVoice | 1.01 相同(下面的修复之前 0.71) | 1.01 相同 | 0.98 相同 | 1.01 相同 | 1.01 相同 |
| GPT-SoVITS v2 / v4 | 0.93 / 0.99 相同 | 0.91 / 0.96 另一次生成 | 0.62 / 0.73 | 0.87 / 0.96 | 0.91 相同 / 0.98,34 dB |
| Fish Speech | 1.05 另一次生成 | 1.08 | 1.13 | (拒绝:布尔掩码) | 1.05 另一次生成 |
| IndexTTS 1.5 / 2 | 1.06 / 1.01 | 0.82 / 1.00 | 0.97 / 0.97 | 无休止:6 倍 / 1.00 | 1.01 / 1.00 |
| VoxCPM 1.5 / 2 | 1.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 2 | 2,937 | 0.408 s,7.55 GB/s | 0.365 s,8.43 GB/s |
| F5-TTS | 703 | 0.092 s,8.02 GB/s | 0.083 s,8.90 GB/s |
| Fish Speech S2 Pro | 9,456 | 1.012 s,9.80 GB/s | 0.947 s,10.47 GB/s |
| GPT-SoVITS v2 | 1,026 | 0.154 s,6.98 GB/s | 0.149 s,7.24 GB/s |
| IndexTTS 2 | 6,963 | 0.823 s,8.87 GB/s | 0.766 s,9.53 GB/s |
| OmniVoice | 1,869 | 0.230 s,8.53 GB/s | 0.197 s,9.95 GB/s |
| Qwen3-TTS 1.7B Base | 4,004 | 0.471 s,8.91 GB/s | 0.418 s,10.05 GB/s |
| VoxCPM 2 | 4,775 | 0.554 s,9.03 GB/s | 0.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 s | 5.00 s(1.32×) | 相同 |
| Qwen3-TTS 1.7B Base 克隆(4 GB) | 10.45 s | 7.75 s(1.35×) | 相同 |
| CosyVoice 2 / 3(1.5 GB) | 14.93 / 12.46 s | 15.09 / 12.57 s | 相同 |
| GPT-SoVITS v2 / v4(640 MB) | 0.90 / 2.60 s | 0.90 / 2.61 s | 相同 |
| Fish Speech(6 GB) | 36.58 s | 36.35 s | 相同 |
| IndexTTS 1.5 / 2(3 GB) | 1.88 / 30.76 s | 1.87 / 30.83 s | 相同 |
| VoxCPM 2 / 1.5(2.5 GB) | 22.96 / 3.88 s | 24.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:预热后耗时、峰值 | high | none:预热后耗时、峰值 | 音频 |
|---|---|---|---|---|
| Qwen3-TTS Base / CustomVoice | 1.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 / 3 | 1.05 s、3.1 GB / 1.05 s、7.4 GB | 相同 | 14.9 s、0.8 GB / 12.4 s、1.6 GB | 相同 |
| F5-TTS | 0.91 s、0.8 GB | 相同 | 2.06 s、0.2 GB | 相同 |
| Fish Speech S2 Pro | 3.23 s、9.9 GB | 相同 | 106 s、1.6 GB | 相同 |
| GPT-SoVITS v2 / v4 | 0.64 s / 1.06 s | 相同 | 0.95 s / 2.34 s、0.3 / 0.6 GB | 相同 |
| IndexTTS 1.5 / 2 | 0.55 s / 1.94 s、8.2 GB | 相同 | 7.7 s / 30.8 s、1.5 GB | 相同 |
| OmniVoice | 0.78 s、2.0 GB | 相同 | 2.15 s、0.6 GB | 相同 |
| VoxCPM 1.5 / 2 | 0.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-TTS | DiT | −23 %(整个请求 60 → 45 秒) |
| IndexTTS | s2mel DiT / BigVGAN | −7 % / −20 % |
| GPT-SoVITS v4 | CFM 估计器 | −7 % |
| CosyVoice | flow 估计器 | 无(它的卷积和矩阵乘法已经是库内核) |
| 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 内核),以及把长文本的各段合成一批(抽样不同)。