Skip to content

把 RTF 从 1.087 压到 0.68:一次推理性能优化的全过程、层次与方法

上一篇 测试复盘 是“那天测了什么、踩了什么坑”的数据流水账。这篇抽出来讲更值钱的那一层:整个优化是怎么推进、怎么拆解、怎么思考的。数据会过期,框架能复用。

一句话先讲完:用数据定位瓶颈,不凭感觉调参;从目标层层往下拆,每层都用实测说话。


一、北极星:一个公式守住目标

Section titled “一、北极星:一个公式守住目标”
RTF = 一个音频 chunk 的生成耗时 ÷ 这个 chunk 的音频时长
RTF < 1 即实时(生成快于播放)

全程只盯这一件事:让“生成时间”短于“音频时间”。官方基线 1.087(略慢于实时),目标是把它压下去。

关键纪律:别把手段当目标。量化、绑核、改队列、加线程——全是手段。每次有人提“要不要试试 X”,先回问一句“X 动的是哪段、预期能降多少 RTF”。答不上来,就先不试。


二、整体过程:四个阶段,一条 RTF 下降曲线

Section titled “二、整体过程:四个阶段,一条 RTF 下降曲线”

整个优化不是一蹴而就,是四个阶段、逐层深入,每个阶段解决一类问题、把 RTF 推下一个台阶:

阶段 解决的问题 关键动作 RTF 轨迹(SPEAK→WAV)
0. 先跑通 910B 上全链路能出声 cann 6 补丁 + Q4_K_M 跑通 T2W 0.86(单段,非全链路)
1. 上 NPU + 解耦 LLM 在 CPU(没走 NPU)+ 队列锁步 F16 战略 + host_buffer + use_mmap + 队列 1→16 0.83(P1.7,首次稳态)
2. 攻瓶颈段 TTS 段里的 vocoder CPU 太慢 vocoder 多线程 + NUMA 绑核 + 极限分析 0.57(P4 调优)/ 0.68(P8 报告口径)
3. 验收 精度 benchmark + Demo + 复现 三项 benchmark + 加固接入 + 复测取中位 0.68(3 次中位)

RTF 下降曲线:1.087(基线)→ 0.83(上 NPU+解耦)→ 0.62(vocoder 加线程)→ 0.57(+NUMA 绑核)→ 0.68(正式复测中位)。

注意最后那个“反弹”:0.57 是调优最优值(24 线程 + 手动 NUMA 绑核),0.68 是正式报告口径(16 线程默认、最可复现)。报告标保守值,调优值作为“还能更好”的证据——别把需要手动 taskset 才能复现的数当默认成绩。

每个阶段的本质:前一阶段把“能不能用 / 在不在 NPU”解决后,瓶颈就转移到下一层。阶段 1 解决 offload 后,瓶颈才暴露成 vocoder;阶段 2 解决 vocoder 后,才看得清理论极限在哪。不解决上一层,下一层的瓶颈根本看不见。


三、层次结构:从目标到算子的六层分解

Section titled “三、层次结构:从目标到算子的六层分解”

这是这篇文章的核心。优化天然是个金字塔,从顶往下拆,永远自顶向下工作——上层一个开关抵下层一堆参数。

L0 目标 RTF < 1(攻哪段?)
L1 链路三段 LLM 解码 → TTS → Token2Wav
L2 瓶颈定位 每段计时 + npu-smi 看真在不在 NPU
L3 杠杆排序 优化收益 = 该段占比 × 可降幅度
L4 手段工具箱 后端 → 量化 → 编译 → 运行时 → 算子(从大到小)
L5 验证 + 红线 npu-smi + 质量 + 同口径 / 不越红线

逐层展开:

“把 SPEAK→WAV RTF 压到 <1,beat 1.087。” 说不出这句话,就是在乱调。

L1 链路三段——拆开才知道哪步慢

Section titled “L1 链路三段——拆开才知道哪步慢”
音频 chunk 生成 = LLM 解码(语义) → TTS(音频 token) → Token2Wav(波形)
RTF = (LLM + TTS + T2W 耗时) ÷ 音频时长

比喻:做一道菜 = 备料 → 烹调 → 装盘,总时长要短于客人等待。不拆开就平均用力,一定扑空。

L2 瓶颈定位——用数据,不靠猜

Section titled “L2 瓶颈定位——用数据,不靠猜”

两件武器:

  • perf-duplex:分模块计时(OMNI_T2W_PROFILE=1 能把 T2W 再拆成 token2mel / vocoder)。
  • npu-smi:看 HBM / AICore,确认真在 NPU 算(戳穿“数字降了但实际在 CPU”的假象)。

因“机”制宜:不同硬件瓶颈位置不同——4090 上 LLM 快(145ms),瓶颈在 TTS/T2W;910B 上 LLM 起初在 CPU(9133ms),瓶颈在“没上 NPU”。照搬 4090 的量化策略到 910B 直接扑空(cann 不支持 Q4_K_M)。

优化收益 = 该段占比 × 可降幅度

先攻“占比大且可降”的。本次实例:

  • LLM 在 CPU(慢 ~10×)→ 让它上 NPU = 最大杠杆(prefill 7.9s → 0.77s)
  • vocoder 占 T2W 80% → 加线程 = 第二大杠杆(591ms → 346ms)
  • 图模式 910B 不支持 = 0 杠杆,砍掉

比喻:先修高速公路(LLM 上 NPU),再调红绿灯(参数微调)。顺序错则事倍功半。

L4 手段工具箱——从大到小,按层试

Section titled “L4 手段工具箱——从大到小,按层试”
层 手段 本次实例 杠杆
1 后端/部署 权重放对的设备 host_buffer=false + use_mmap=false → LLM 上 NPU 🔴 最大
2 量化档 选后端支持的 cann 支持 F16、不支持 Q4_K_M → 用 F16 🔴 大
3 流水线/调度 队列/并发 LLM↔TTS 队列 1→16 解耦(P50 8.3s→977ms) 🔴 大
4 运行时参数 线程/NUMA/ctx vocoder 16→24 threads + NUMA 绑核 🟡 中
5 算子级 改 cann 算子 SQR 断言放宽、device 绑定 🟢 小(最后)

铁律:永远从第 1 层往下试。一个后端开关(host_buffer)抵得上一堆线程参数。本次最戏剧性的例子:调半天线程不如一行“队列 1→16”——因为队列锁步是 L3 层、线程是 L4 层,上层堵着,下层再调也没用。

L5 验证 + 红线——防自欺、防出局

Section titled “L5 验证 + 红线——防自欺、防出局”

验证三件套(缺一不可):

  1. npu-smi 看 HBM/AICore:确认真在 NPU 算。本次靠它戳破“LLM 在 CPU 却以为在优化”。
  2. 质量检查:RTF 低但乱码 = 负优化。4090 Q8_0 教训——RTF 0.32 是 bug 不是性能。
  3. 同口径对比:双工 perf vs 双工 perf(跨硬件可比);单工 vs 双工不可比。

三条红线(碰了出局):精度降幅 ≤2pp / Demo 可用 / 材料可复现。F16 选它不只因为快,还因精度无损——守红线。


四、思考方法:六条反复被实测印证的准则

Section titled “四、思考方法:六条反复被实测印证的准则”

这套方法论不是先验的,是被实测打脸打出来的。六条:

1. runtime 实测 > 静态推理(被打脸两次,最贵的一条)

Section titled “1. runtime 实测 > 静态推理(被打脸两次,最贵的一条)”
  • 第一次:P1.6 看到decode 期 AICore 仅 4%,结论“compute 没真走 NPU”。复跑 + 后台 npu-smi 细粒度(0.5s)采样,decode 活跃窗口 AICore burst 到 60–84%、HBM 带宽 50%——一直在 NPU,“4%“是采样时机/时间均值的伪影。
  • 第二次:P7 文本乱码,三个静态分析 agent 给了三个互相矛盾的根因,其中一个还读错了关键行。改做 runtime 隔离实验 + token id 日志,一条 id=30, audio=0 直接推翻“audio token 没 mask”的假设——真因是 stack_frames=8 触发模型退化。

教训沉淀成闭环:plan → 实测定位 → 修 → 三件套验证 → 落盘。静态分析只用来生成假设,从不用来下结论。

2. 判断“NPU 在不在算”要看占空比,不看均值

Section titled “2. 判断“NPU 在不在算”要看占空比,不看均值”

npu-smi info -t usages -i 1 必须细粒度(≤0.5s)采样,看 AICore Usage Rate + HBM Bandwidth Usage Rate 的占空比(AI>5 且 HB>5 的时间占比)。绝不能看单次或粗均值——batch=1 自回归 decode 天然 memory-bound、util 低,2 分钟窗口含 prefill 空闲 + 帧间等待 + drain,平均下来 14% 但实际 compute burst 到 72%。

3. 因“机”制宜,别跨硬件照搬

Section titled “3. 因“机”制宜,别跨硬件照搬”

4090 上“量化是 RTF 主杠杆”,910B 上 cann 不支持 Q4_K_M → 整个量化策略失效。换硬件第一件事是重测瓶颈位置,不是复用优化清单。

4. 上层开关抵下层参数(自顶向下)

Section titled “4. 上层开关抵下层参数(自顶向下)”

见 L4。一句话:上层堵着,下层白调。 先开后端/部署层的开关,再谈参数。

5. 验证防自欺——RTF 降了不等于优化成了

Section titled “5. 验证防自欺——RTF 降了不等于优化成了”

三件套里质量检查最容易被忘。Q8_0 在 4090 上 RTF 0.32 看起来美,输出是 ?????? 重复循环——虚假性能。任何 RTF 下降都要配输出质量检查,否则可能在优化一个 bug。

6. 诚实记录未达,极限分析划边界

Section titled “6. 诚实记录未达,极限分析划边界”
  • P5 冲理论极限 0.34 没冲到(CPU 竞争),不 merge、如实记录回退。
  • 极限分析(Roofline)划出红线内硬极限 = 0.34:vocoder CPU 346ms 物理锁,且 vocoder NPU 化因 cann 无 CNN 算子不可行。
  • P1.7 没达 exit 0(LLM P95 临界 + 首响 1493ms),诚实标 exit 2 + 写下突破方向。

知道“到顶了 / 还差多少 / 为什么差”,比硬报一个漂亮数更有价值。


五、一个闭环案例:vocoder 优化走完全流程

Section titled “五、一个闭环案例:vocoder 优化走完全流程”

把上面的框架套到 P3(vocoder 多线程)这一步,看方法怎么从头走到尾:

步骤 这一步做了什么
L0 目标 把 TTS RTF 从 0.83 继续往下压
L1 拆 LLM 已近极限(14ms/tok vs 下限 13.7ms),攻 TTS+T2W 段
L2 定位 OMNI_T2W_PROFILE=1 拆 T2W → vocoder 591ms 占 80%,token2mel 144ms 占 20%。瓶颈是 CPU vocoder(256 核只用 8)
L3 杠杆 vocoder 占比 80% × 可降(hifigan 非自回归可并行)= 最大杠杆;候选 E(队列)Δ<0.03 证伪,砍
L4 手段 L4 运行时参数层:threads 8→16(红线内,不改数学)
L5 验证 vocoder 591→395ms(-33%);RTF 0.83→0.62;wav RMS 不变(质量);token2mel 不变(NPU 段不受影响);未触 cann 补丁(红线)✅
延展 threads 24+NUMA→0.57;overlap 冲 0.34 未达(CPU 竞争)回退;极限分析划出硬极限 0.34

整个过程没有一步是“感觉加个线程试试”——每一步都先定位、算杠杆、守红线、三件套验证。


六、每次优化前过一遍的检查清单

Section titled “六、每次优化前过一遍的检查清单”
  • 目标明确:RTF < 1?攻哪一段?
  • 瓶颈已用 perf-duplex + npu-smi 定位(不是猜)
  • 杠杆排序:先最大头(占比 × 可降幅度)
  • 手段从大到小试(后端 → 量化 → 编译/流水线 → 参数 → 算子)
  • 验证三件套:npu-smi(真上 NPU)+ 质量(不乱码)+ 同口径(可比)
  • 红线:精度 / Demo / 复现
  • 单变量 + 多次取中位(防噪声,≥3 次,Δ<0.03 视噪声)
  • 落盘 + 诚实记录未达

七、最后:性能优化之外的一条认知更正

Section titled “七、最后:性能优化之外的一条认知更正”

这次还学到一个超出性能范畴的教训,值得单独记下:

“不改推理数学 → 精度 = 基线”只对纯文本/数学等价推理成立,对多模态 benchmark 不成立。

我们前期假设“F16 不改数学,精度必然 = 基线、准入必过”。实测打脸:Daily-Omni / VideoMME 的精度严重受 omni 框架配置影响(视觉帧数、音频窗口、输出模态),不是 F16 数学等价能保证的——多帧视觉会触发模型退化、whisper 30s 窗口会截断长音频。性能优化是“不改数学”,但精度还受“框架怎么调用模型”制约,两者不是一回事。

这也是为什么性能 RTF 我们能 beat 基线 37%,而两项多模态精度却卡在框架代际上限——性能和精度是两条独立的战线,不能互相兜底。


一句话收尾:性能优化不是玄学,是目标→拆解→定位→排序→手段→验证的工程闭环。每一步都用实测数据说话,每一步都守红线。框架比数据耐用——换到下一个模型、下一块卡,这套拆法照样能使。