19. 为什么没有变快
18. 时延、功耗与内存怎么测 给出了一组口径可靠的数字:编码器在 NPU 上每拍 147 ms,在 CPU 上 69 ms,慢 2.1 倍;但每拍能耗 63 mJ 对 199 mJ,省约 69%。
"更省电"好解释——算得慢但瞬时功率低。真正需要解释的是"更慢"。一颗专门做矩阵运算的加速器,跑一个以矩阵乘为主的模型,比四个通用核心慢一倍,这件事不符合直觉,而不符合直觉的现象如果不查清楚,后面所有优化都会押在错误的方向上。
这一篇讲怎么查清楚的,以及查清楚之后这颗 NPU 该怎么用。
先造几把尺子
直接在真模型上试优化是没有尽头的:图里有 3766 个节点、几十种算子,改哪一处、省多少,全靠猜。有效的做法是先用合成模型在同一颗 NPU 上量出几个基本常数,再拿这些常数去算真模型的时间去向。
合成模型就是手工构造的小图:固定节点数、固定张量形状、计算内容无关紧要,只用来测一个变量。
量出来的六个数:
| 量 | 实测值 | 怎么测的 |
|---|---|---|
| 每节点地板价 | 0.0024 ms | 100、400、800、1600 节点的链,耗时严格线性;模型文件体积也严格是 1:4:8:16,说明编译器没有把链条折叠掉 |
| 真实形状下每节点 | 0.0035 ms | 把链条里的张量换成编码器主流的形状 |
| 每路张量绑定 | 0.003 ms | 117 路输入输出对 8 路、总体积相同,只差 0.32 ms |
| 权重读带宽 | 23.9 GB/s | 单个矩阵乘分别扫 8.39、33.6、81.9 MB 的权重,三点线性 |
| 形状敏感度 | 5.6 倍 | 同样是每张量 64 KB、200 个节点,二维 [1,16384] 要 4.956 ms,四维 [1,64,16,16] 只要 0.890 ms |
| 批次收益 | 16 倍数据只花 2.44 倍时间 | 同一条 200 节点的链,批次从 1 提到 16,单位数据成本降 6.6 倍 |
第一条里那句"模型文件体积严格 1:4:8:16"值得单独说:没有这个交叉校验,就无法排除"编译器把我的测试链优化掉了"这种可能,测出来的线性关系会变成一个假象。造合成模型做基准,一定要有一条独立的证据说明被测对象真的还在。
五个假说,同一批尺子,全部不成立
有了常数,之前那些"听起来合理"的解释就可以逐个算账了。
| 假说 | 算账结果 |
|---|---|
| 时间几乎全花在每节点的固定过路费上,每节点约 0.037 ms | 实测 0.0024 ms,小 15 倍 |
| 把节点数从 3766 砍到 1605 就能追平 CPU | 折掉 600 个节点只值 1.4 ms,在 144 ms 里可以忽略 |
| 把 99 处展开写的激活函数折回成原生算子是必要铺垫 | 数学上等价,同样只值约 1.4 ms,不值得做 |
| 234 路张量绑定每路 0.25 ms,合计 58 ms | 实测合计 2.4 ms,松了 20 倍 |
| 每秒只搬 63 MB,远低于内存带宽,所以带宽不是瓶颈 | 结论侥幸是对的,但依据无效:漏算了 157 MB 权重和 121 MB 激活 |
最后一条是个很典型的坑——结论对、推理错。如果不复核依据,下一次用同样的方法算另一个模型就会出错。
把常数代回真模型,144 ms 的去向是这样的:
| 项 | 时间 | 占比 |
|---|---|---|
| 节点地板价(3766 × 0.0035) | 13.2 ms | 9% |
| 权重读取(157 MB ÷ 23.9 GB/s) | 6.6 ms | 5% |
| 234 路张量绑定 | 2.4 ms | 2% |
| 余下的真实算术 | 约 122 ms | 83% |
可归因的开销合计只有约 22 ms。 也就是说没有"开销"可以削——83% 的时间是这颗 NPU 在老老实实做算术,只是做得不快。
这同时回答了另一个被反复提起的方向:"能不能减少数据搬运"。搬运(权重 6.6 + 绑定 2.4)合计只占 6%,把它清零也只省这么多。方向本身不错,但入口不在这里。
真因:张量填不满计算单元
把节点按形状分类之后,答案就出来了:编码器 81% 的节点(3766 个里的 3063 个)是三维、形状为"时间 8、批次 1、维度 D"。
这类 NPU 的矩阵单元按 16×16×16 的立方块工作,一次吃进一个 16×16 的瓦片。"8 乘 1"的瓦片只填满立方块的三十二分之一,而算一次的代价和填满时相差不大。于是标称上远强于 CPU 的算力,在这张图上只兑现了很小一部分。
这不是算子选得不好,也不是转换做得不对。流式识别的张量本身就是这个样子:一拍 640 ms 音频,经过下采样之后时间维只剩 8,批次是 1 因为只有一路音频在跑。换句话说,慢的原因写在模型的使用形态里,不在模型的实现里。
两个顺带的操作性结论:
- 不要去手动优化那 250 个转置算子。离线编译器自己会消除冗余转置——往返转置的合成模型编译出来反而更小更快,手动改只会更差。
- 不要为了"减少节点"去做等价重写。按地板价算,每砍 1000 个节点值 2.4 ms。
对照:同一颗 NPU 上另一个模型赚了 11 倍
如果结论停在"这颗 NPU 不行",那就是错的。同一台设备、同一颗 NPU 上,还跑着一个说话人编码器:90 个节点,每次处理一个 10 秒窗口,从 CPU 的 307.7 ms 降到 27.7 ms,快了 11 倍。
两个模型的差别不在节点数(90 对 3766,少的那个反而赚了),而在每个节点带多少数据。说话人编码器一次吃 10 秒音频,张量塞得很满;流式识别一次只吃 640 ms,还要拆成 8 帧,塞不满。
所以"这颗 NPU 行不行"这个问题问得不对,应该问的是"这张图的形状行不行"。判断一个模型值不值得上这类加速器,先看它的典型张量能不能填满计算单元,而不是先看它有多少参数或多少算子。
顺着真因往下试,又撞上一堵墙
既然填满批次是唯一实测有效的杠杆,下一步就很明确:把同一张图按 16 路并行重转,让 16 条音频流共享一次权重读取和一次计算。这同时解决了搬运问题——157 MB 权重从每拍读一次变成每 16 拍读一次。
这件事做到了一半,记录如下。
第一次判断是错的。 我一开始说"批次维本来就是符号维,不需要改图"。上游 ONNX 确实是符号维,但固定形状那一步做过常量折叠(10891 节点合并到 3766),把批次 1 烧进了 278 个形状重排的常量、38 个膨胀算子和一份注意力掩码常量,而且批次所在的位置并不总是第一维。手工往回改时连续撞上形状冲突,撞了两处就该停下来重新想——正确做法是回到带符号维的上游图,用重写脚本自带的批次参数重跑整条链。
重跑是通的。 批次 16 的图仍然是 3766 个节点、没有残留的不支持算子,117 路输出在宿主上验证通过,最大偏差 9.59e-4。校准集用 16 段错开起点的真实音频并行递推产出。量化和编译都成功,产出 166,697,763 B 的模型。
设备拒收。 兼容性查询返回 1,构建阶段失败,日志指名注意力里的一批矩阵乘不被算子库支持。
隔离实验把原因缩到了一个点上。先排除"批次大于 1 就不行":批次 4 的普通矩阵乘、时间维 32 的矩阵乘,都通过。再排除形状:按报错里的确切形状造合成模型,包括那个被点名的形状,浮点版本全部通过。最后做决定性验证——同一个算子、同一个形状,浮点版本设备收,量化之后设备不收。
结论是:这颗芯片没有量化版本的批量矩阵乘实现。批次为 1 时,这些节点会降解成普通矩阵乘(有量化实现),所以反而能跑;批次大于 1 就变成批量形态,没有量化实现可用。
这一类"只在某个组合下才出现"的限制,光看文档和算子支持列表是查不出来的。能定位到它,靠的是把变量一个一个拆开做合成模型对照——先分离批次与时间维,再分离形状,最后分离精度。
补救方向和它的状态: 被拒的是 19 个带四维权重的注意力矩阵乘,这些权重合计只有 0.1 MB(而另外 325 个二维权重合计 608.5 MB)。把这 19 个节点排除出量化、保持浮点,体量上几乎不花代价。这一步在记录时点只写好了跳过指定层的量化配置,没有跑完,没有真机结果。 它是一个有依据的方向,不是一个结论。
同样属于推算而非实测的还有一条:按合成模型的缩放关系外推,吞吐档下每拍可能降到 22 ms 量级、能耗降到 10 mJ 量级。这个数字没有在真模型上验证过,而且代价很明确——批次 16 意味着要先缓冲 10.2 秒音频,交互式场景用不了。
两条平台级的边界
有些路不是没调通,是平台没开。这类结论要尽早做实,避免反复投入。
GPU 在这台设备上对通用计算是关闭的。 推理框架在上下文校验阶段就否掉了(1.17 ms 返回,GPU is not supported),系统自带的那份库编译时没有启用 GPU 后端,应用侧没有开关可拨。旁证是系统能力参数里只有图形渲染相关的项,没有通用计算的能力位;GPU 本身在正常工作,但只对渲染开放。两台不同型号的鸿蒙手机结论一致。其余路径也都不通:几种常见的 GPU 执行后端要么不属于这个平台,要么官方已移除;手写计算着色器理论可行,但 3766 个节点的量级工作量按月计,而且低精度支持残缺。
CPU 这一侧还欠着一笔。 对照用的 CPU 档走的是默认计算后端而不是优化后端,原因是产物里压根没编进去(细节在 18. 时延、功耗与内存怎么测)。修好之后 CPU 预计快约 24%,估算仍是 NPU 的约 2.4 倍——所以它不改变结论的方向,但会让差距更大。
采用方案与边界
把所有实测汇到一起,这颗 NPU 在这个模型上的结论是分档的。
| 场景 | 该不该上 NPU | 依据 |
|---|---|---|
| 流式实时转写,目标是续航和温升 | 可以上。时延从 69 ms 变成 147 ms,但仍有 77% 的空闲余量、937 拍零超时,而每拍能耗省 69% | 实测 |
| 流式实时转写,目标是首字时延 | 不要上。慢 2.1 倍是结构性的,不是调优问题 | 实测 |
| 交互式对话,对时延敏感 | 不要上。同上,而且填批次的方案要先缓冲 10 秒音频 | 实测 + 推算 |
| 离线批量转写、多路并发 | 值得试,但目前只有合成模型的外推,真模型上被量化版批量矩阵乘挡住 | 未验证 |
| 窗口较长的辅助模型(说话人编码器一类) | 优先上。同一颗 NPU 上快了 11 倍 | 实测 |
一个更可迁移的判断方法:先看典型张量能不能填满计算单元,再谈要不要上加速器。参数量和算子数都不是好的指标,形状才是。把一个流式模型的时间维和批次维摊开,如果它们都是个位数,那么这颗加速器的算力优势大概率兑现不了,节能可能仍然成立。
还有一条流程上的结论:这次之所以能把"慢"解释清楚,靠的不是更深的硬件知识,而是先用合成模型量出可复用的常数,再用常数去否定假说。五个假说在被量化之前,每一个听起来都成立,其中两个还指导过实际的优化尝试。拿不到硬件内部指标时,这种"自造标尺"的办法通常比继续猜更快。
总结
模型上了 NPU 却变慢,原因不在算子选择、不在数据搬运、也不在节点数量——可归因的开销合计只占 15%,83% 的时间是真实算术在极低的计算单元利用率下跑出来的,而低利用率来自流式识别"时间维 8、批次 1"的张量形态。同一颗 NPU 上窗口更长的模型反而快了 11 倍,说明问题在图的形状而不在硬件。按这个结论取舍:目标是功耗和温升时,流式档上 NPU 成立且已实测;目标是时延时不成立;吞吐档理论上有优势,但被一个"没有量化实现的批量矩阵乘"挡住,补救方案尚未验证完。
这个子系列到此结束。把视角拉回整条语音链路,识别只是其中一环——状态机、打断、端云协同怎样组织,见下一篇。
下一篇:20. 语音交互链路。
