18. 时延、功耗与内存怎么测
17. 图转换与算子适配 结束时,模型已经被设备收下了。接下来要回答的是"好不好用",而这一步最容易出事——不是测不出数字,是很容易测出一堆看起来合理、实际上没有意义的数字。
这个项目里作废过三组功耗数据、撤回过两个结论,原因都不是设备问题,是口径没立住。所以这一篇按顺序讲四件事:时延怎么测才可比,功耗在什么条件下根本测不出来,温升为什么没结论,内存怎样拆开归因。
时延:定速和背靠背测的是两件事
最直觉的做法是循环跑 N 次取中位数。这个做法能用,但它测的是峰值能力——背靠背没有间隙,缓存一直是热的,队列一直是满的。
实时语音识别的真实形态不是这样:每 640 ms 来一拍,算完就空着等下一拍。所以需要第二种跑法——按真实节拍定速,每拍的起点按绝对时间边界排(第 i 拍的起点是循环开始时间加上 i 乘以拍长),而不是"算完睡一会"。后者会把每次的抖动累加进去,跑十分钟之后节拍已经漂没了。
定速模式必须同时记录一个数:超时拍数,也就是上一拍还没算完、下一拍就该开始的次数。这是实时能力的判据,比平均耗时更直接。
两种跑法的结果:
| 加载/编译 | min | 中位 | 均值 | p95 | |
|---|---|---|---|---|---|
| CPU(INT8,4 线程,背靠背) | 4392 ms | 61.94 | 64.52 | 66.76 | 90.97 |
| NPU(离线 INT8 模型,背靠背) | 3671 ms | 110.38 | 143.58 | 141.21 | 148.68 |
按 640 ms 定速连续跑 10 分钟(937 拍):
| 中位 | 均值 | p95 | 超时 | 占空比 | |
|---|---|---|---|---|---|
| CPU | 69.06 | 77.67 | 101.30 | 0/937 | 11% |
| NPU | 146.91 | 146.28 | 152.85 | 0/937 | 23% |
两张表合起来才说得清楚:NPU 每拍确实更慢,但 937 拍里一次超时都没有,占空比 23%,实时能力完全够。只看第一张表很容易得出"NPU 跑不动"的错误印象——它不是跑不动,是每拍更慢。
还有两条数值上的注意事项。一是样本数要够:NPU 在 5 次采样时中位数是 124.7 ms,加到 20 次变成 143.6 ms,而 CPU 一直稳定在 64.5 ms。前几次偏快,少量采样会系统性高估 NPU。二是两种跑法的数字不能混比:CPU 背靠背 64.5 ms、定速 69.1 ms,差的是缓存热度,拿一边的 CPU 去比另一边的 NPU 会得到错的倍数。
功耗:先过门禁,再谈数字
功耗这一段作废过两次,教训比数字本身更值得记。
第一条:插着数据线测不出功耗。 插线时电池电流被充电回路钳住,和负载完全脱钩。实测两档读数是 −631 mW 和 −627 mW,几乎一模一样。这不是"差异小",是信号根本没进来——无论跑多久、跑多少次,都不会变。所以采集脚本的第一件事是检查供电状态,不满足就直接退出,不产出数据。
第二条:两档必须处理等量音频。 如果两档都按"尽量快地跑满十分钟",快的那一档在同样墙钟时间里多处理了一倍多的音频,功耗对比毫无意义。所以两档都按 640 ms 定速、都跑 937 拍,吃掉的音频完全一样。
第三条:要有空载基线。 屏幕、系统和采样器本身就在耗电,而且占大头。基线用同一个应用、同一块屏幕、同一个采样器,只把模型换成一个 100 节点的极小图(占空比 0.29%),这样减出来的才是推理本身。
按这套协议跑出来的结果:
| 整机功耗 | 推理净增 | 95% 置信区间 | 每拍能耗 | 每拍耗时 | 超时 | |
|---|---|---|---|---|---|---|
| 空载 | 1.21 W | — | — | — | — | — |
| NPU | 1.31 W | +98 mW | ±22 mW | 63 mJ | 147 ms | 0/937 |
| CPU | 1.52 W | +311 mW | ±42 mW | 199 mJ | 69 ms | 0/937 |
NPU 每拍能耗低约 69%。整机只差 14%,因为屏幕把差异摊薄了——如果是关屏的后台转写场景,整机上的节电比例会明显更大。
为什么只能用均值
这一条单独拿出来说,因为按中位数算会得到相反的结论。
采样频率是 1 Hz,而 CPU 的计算是突发式的:每拍只忙 69 ms,然后空闲 571 ms。绝大多数采样点落在空闲期里,中位数反映的是空闲功率而不是平均功率。按中位数算,CPU 净增只有 +80 mW,看起来比 NPU 还省电——这是纯粹的采样假象。
均值是时间平均功率的无偏估计,三种算法(全段均值、去掉前 60 秒、去掉两头 10%)排序一致。峰值也要记录:p90 是 NPU 1.43 W、CPU 2.10 W,这个差距和"CPU 算得快但瞬时功率高"的解释吻合。
遇到任何突发式负载的功耗测量,都要先问采样频率和占空比的关系。采样周期远大于单次计算耗时时,中位数一定会低估。
被撤回的那几个数字
写下来是因为它们比正确结果更能说明这件事有多容易做错。
- 曾经引用过一组"合成模型测得净功耗低 38%"的数据,后来溯源不到可信的采样记录,而且与项目里两份"从未直接测到瓦数"的记录矛盾,已经撤回。
- 曾经给出过"每次推理温升低 37%",精确到这个程度本身就不该出现在一次性顺序测量里,主动收回。
- 一处代码缺陷:对毫瓦读数取了绝对值,把充电电流当成了消耗,复测时出现 2675 mW 这种假值。
共同点是都没有先确认测量前提是否成立。功耗这类数据的特点是"永远能给你一个数",所以门禁必须做成自动的,而不是靠记得检查。
温升:这一项没有结论
两档各跑十分钟,机身温度分别升了约 1 °C(26→28、28→29、29→30 °C)。顺序执行时每一段的起点都被上一段抬高,加上电池温度传感器响应慢,这个口径区分不出两档的差别。
所以温升这一项的结论是没有结论,而不是"两档差不多"。要测温升需要换设计:每段之间充分冷却到同一起点、每档重复多次、最好用更快的温度源。在没有做之前,不能用"NPU 更省电所以温升也更低"来替代——这是推理,不是测量。
同样要写清楚的还有几项已知局限:每档只跑了一次且顺序固定(CPU 在最后、机身最热,对 CPU 略不利);CPU 档走的是一个次优的计算后端(见下一节),换成更优的后端预计能降约 24%,但按这个估算仍是 NPU 的约 2.4 倍;只测了编码器,不是端到端链路;整机功耗包含屏幕。
一个长期被记成"正常"的 CPU 后端缺陷
功耗表里的 CPU 档走的是默认计算后端,而不是本该启用的那个优化后端。查下来的链条是这样的:
端侧那个推理运行时的动态库,和上游预编译发布的版本是同一个校验和,里面不含优化后端的任何实现符号。而代码在创建会话时无条件追加这个后端,在这样一个库上会抛异常——另一条链路因此加载即崩。源码本身没错,坏的是构建输入:编译脚本里后端列表是硬编码的字面量、从不校验,而那一步编译从未成功过,进度记录却把它写成"功能正常"。
两个可复用的做法:
- 产物要带来源信息并在构建时校验,不能靠一个硬编码的列表声明自己编进了什么。一个最低限度的检查是在产物里查关键符号是否存在。
- 可选后端的降级必须显式告警,不能静默回退。静默回退的代价在这里是性能低 24%,而且谁也不知道。
内存:先确认采集手段,再谈归因
内存排查一开始就建立在一个错误前提上:文档里记着某个场景从 181 MB 涨到 611.8 MB,多出来的 428 MB 被整个归给了说话人分离。实测发现纯转写的活跃识别就已经是 502 MB,而且 611.8 MB 是从外部采的进程常驻内存总量,里面包含约 170 MB 可回收的模型文件页和测试外壳本身的运行时堆。前提错了,后面所有定位都会走偏。
这台设备上还有一个采集手段的问题:进程的内存映射明细对调试 shell 是拒绝访问的(安全策略),常见的那套读法用不了。可用的是系统自带的内存信息工具,它由系统服务侧读取,能按类别汇总,也能逐映射输出,单次约 530 ms,1 秒一次的采样可行;进程状态文件里的常驻内存总量是公开可读的,可以用来交叉校验。
按类别拆开之后,带说话人分离的 NPU 档峰值是这样的:
| 类别 | 占用 | 说明 |
|---|---|---|
| 应用包文件页 | 169.3 MB | 文件页,内存紧张时可回收 |
| 原生堆 | 134.7 MB | |
| 图内存 | 55.3 MB | NPU 的计算图占用 |
| 应用运行时堆 | 20.2 MB | |
| 动态库 | 15.7 MB | |
| 合计 | Pss 518.2 MB / 常驻 580.2 MB |
说话人分离这一项的净增约 145 MB,和它 55 MB 的模型体量对得上,没有重复加载,也没有缓存泄漏。"存疑待查"到这里可以撤销。
这组数字还解释了 16. 模型压缩与体量门槛 里为什么否掉了"切成两段用 FP16"的方案:那会让这部分从 162 MB 涨到 293 MB,多出 131 MB,而图内存本来就已经是一项实打实的开销。
总结
这一篇真正交付的不是数字,是口径:时延要分开测峰值和定速,并记录超时拍数;功耗要先过供电门禁、要有空载基线、两档要处理等量音频、突发式负载必须用均值;温升在当前设计下没有结论,就该写成没有结论;内存要先确认采集手段可信、再按类别归因,而不是拿一个总量去推测去向。
把这些立住之后,剩下的问题只有一个:模型确实上了 NPU,确实更省电,但为什么没有变快。
下一篇:19. 为什么没有变快。
