1. 从CPU到NPU:为什么通用算力突然不够用了
如果你最近两年在折腾端侧AI部署,大概率会遇到一个很尴尬的局面:模型在服务器上跑得好好的,一搬到手机、开发板或者边缘盒子上,帧率直接掉到个位数,功耗还高得吓人。我最早接触这类问题是在一个图像分类的小项目上,当时用一块主流的ARM开发板跑一个轻量级卷积网络,CPU单核推理一张224×224的图要接近200毫秒,四核并行也就勉强压到60毫秒左右,而且板子烫得能煎鸡蛋。后来换了一块带NPU的芯片,同样的模型、同样的输入,单帧推理直接降到8毫秒以内,功耗还降了一大截。这个差距不是靠优化代码能追回来的,本质上是计算架构的代际差异。
要理解NPU和TPU为什么快,得先看清楚CPU在AI推理这件事上到底“亏”在哪里。CPU的设计哲学是通用性,它要处理分支跳转、中断响应、操作系统调度、各种类型的数据读写,所以它的核心面积里有大量资源花在了缓存层级、乱序执行、分支预测这些机制上。真正用来做乘加运算的ALU单元占比其实很小。而神经网络推理的计算模式极其规整——绝大部分是矩阵乘法和卷积,数据流是可预测的,没有复杂分支。用CPU去跑这种负载,就像开着越野车在高速公路上送快递,能到,但效率极低。
NPU(Neural Processing Unit)和TPU(Tensor Processing Unit)的思路就完全不同了。它们把芯片面积几乎全部砸在乘加阵列(MAC Array)上,配合专门的数据搬运通路和片上缓存,让数据像流水线一样灌进去、算完、流出来。一个典型的NPU可能包含几千甚至上万个MAC单元,在同一个时钟周期内完成海量的乘加运算。这种设计叫领域专用架构(DSA),牺牲了通用性,换来了在特定任务上几十倍甚至上百倍的能效比提升。
这里有个数字可以帮你建立直观感受。假设一个NPU有4096个MAC单元,运行频率1GHz,理论上每秒能完成约8万亿次运算(4096×2×1G)。而一个主流CPU的SIMD单元可能只有16到32个通道,频率3GHz左右,峰值算力差了两个数量级。当然实际推理受限于内存带宽、数据复用率等因素,跑不满理论峰值,但即便打个三折,差距依然悬殊。
注意:NPU的算力标称值(TOPS)通常指的是INT8精度下的峰值,和FP32的算力不是一回事。看规格书的时候一定要确认精度前提,否则很容易被数字误导。
对于做端侧部署的人来说,理解NPU的价值不只是“它快”,而是要搞清楚什么样的模型能吃到NPU的红利,什么样的模型在NPU上反而会翻车。这涉及到算子支持度、量化兼容性、内存布局要求等一系列工程细节,后面会逐一展开。
2. NPU和TPU的内部构造:乘加阵列到底怎么工作的
2.1 从单个MAC到脉动阵列
要搞懂NPU为什么快,得从最基本的计算单元说起。一个MAC单元做的事情很简单:接收两个数,相乘,然后累加到寄存器里。公式就是acc += a * b。CPU做这件事需要从寄存器取数、执行乘法、执行加法、写回结果,一条指令流水线下来至少几个周期。而NPU里的MAC单元是硬连线的,一个周期就能完成一次乘加。
单个MAC当然不够看,关键在于并行。NPU把成百上千个MAC排成二维阵列,比如256×256的网格。每一行、每一列同时工作,一个时钟周期就能完成65536次乘加。这就是所谓的空间并行——不是让一个单元跑得更快,而是让几万个单元同时干活。
但光有阵列还不够,数据怎么喂进去是个大问题。如果每个MAC都要从外部内存单独取数,那内存带宽立刻成为瓶颈,阵列再大也白搭。Google在TPU里用的方案是脉动阵列(Systolic Array),这个设计非常巧妙:数据像心跳一样在阵列中逐拍流动,每个MAC算完的结果直接传给相邻的MAC,不需要反复访问内存。权重数据预先加载到阵列中保持不动,激活值从左侧流入、从下方流出,整个数据流高度规整,几乎没有控制开销。
2.2 权重 stationary 与数据复用
脉动阵列的核心思想是权重驻留(Weight Stationary)。在推理阶段,模型的权重是固定的,所以可以把权重预先加载到MAC阵列的寄存器里,然后让输入数据一波一波地流过。每一波数据经过阵列时,会和驻留的权重完成乘加,输出部分和。这样权重的读取次数从“每个输入都要读一次”降到了“整个推理过程只读一次”,内存带宽压力大幅降低。
我用一个具体例子来说明数据复用的价值。假设做一个3×3卷积,输入通道64,输出通道128,特征图大小56×56。如果不用任何复用策略,每个输出像素需要读取3×3×64=576个输入值和576×128个权重值,计算量是576×128次乘加。但实际上,相邻输出像素共享大量输入数据,权重更是所有像素共享。NPU通过片上缓存和阵列结构把这些复用做到极致,实际的内存访问量可能只有理论值的几十分之一。
2.3 片上缓存与数据搬运的分层设计
NPU内部通常有三级存储结构:**寄存器文件(Register File)**离MAC最近,速度最快但容量最小;片上SRAM容量大一些,用来缓存特征图和权重块;外部DRAM容量最大但带宽有限、延迟高。推理过程中的数据搬运策略直接决定了实际性能。
一个常见的优化手段是分块(Tiling)。把大的特征图和权重矩阵切成小块,让每一块都能塞进片上SRAM,算完一块再换下一块。分块大小需要根据SRAM容量、阵列尺寸、数据复用模式来综合决定。分得太小,数据搬运次数多;分得太大,SRAM装不下,只能频繁访问DRAM。这个平衡点找得好不好,往往就是同一颗NPU在不同模型上表现差异巨大的原因。
提示:很多NPU厂商提供的编译器会自动做分块和调度,但编译器不是万能的。如果你发现某个模型在NPU上跑得异常慢,大概率是编译器选择了糟糕的分块策略,或者某个算子根本不支持NPU加速,被回退到了CPU。
3. 什么样的模型能真正吃到NPU的红利
3.1 算子支持度:不是所有层都能加速
NPU不是万能的,它只对特定类型的算子有加速效果。常见的支持列表包括:标准卷积(1×1、3×3、5×5等)、深度可分离卷积、全连接层、池化层、逐元素加法/乘法、ReLU类激活函数、批归一化(通常融合进卷积)。而像动态形状的算子、自定义的复杂激活函数、非标准的注意力机制、循环神经网络里的某些门控操作,NPU往往不支持或者支持得很差。
我踩过的一个典型坑是:某个模型里用了一个自定义的Swish激活函数实现,NPU编译器不认识这个算子,直接把它丢回CPU执行。结果整个网络被切成了好几段,NPU算一段、CPU算一段、再回NPU算一段,中间还有数据在NPU和CPU之间来回拷贝的开销。最终推理速度比纯CPU还慢。后来把Swish换成了NPU原生支持的HardSwish,整个网络重新变成一个完整的NPU子图,速度立刻上来了。
所以在模型设计阶段就要考虑NPU友好性。如果你是从头训练模型,尽量用NPU厂商验证过的算子组合;如果是移植已有模型,先用工具链跑一遍算子支持度分析,看看有多少层能落到NPU上。一般来说,NPU覆盖率低于70%的模型,加速效果会大打折扣,因为CPU和NPU之间的切换开销会吃掉大部分收益。
3.2 量化:INT8是NPU的主场
绝大多数NPU的峰值算力都是在INT8精度下测得的,FP16通常只有一半甚至更低的吞吐。原因很简单:INT8的乘法器面积和功耗远小于FP16,同样的芯片面积能塞下更多的INT8 MAC单元。所以想让NPU跑出标称性能,量化到INT8几乎是必经之路。
量化的基本思路是把FP32的权重和激活值映射到INT8的整数范围。最常见的是对称量化:q = round(x / scale),其中scale是根据张量的最大绝对值算出来的。推理时用INT8做乘加,累加器用INT32防止溢出,最后再乘回scale还原到浮点。
但量化不是没有代价的。有些层对精度特别敏感,比如第一层卷积(直接处理输入图像)和最后一层全连接(直接输出分类logits),量化误差可能导致明显的精度下降。常见的做法是这几层保持FP16或FP32,中间层用INT8。这种混合精度策略在精度和速度之间取得平衡。
| 精度模式 | 典型算力比 | 精度损失 | 适用场景 |
|---|---|---|---|
| FP32 | 1× | 无 | 训练、精度敏感层 |
| FP16 | 2-4× | 极小 | 对精度要求高的推理 |
| INT8 | 4-16× | 可控(通常<1%) | 大多数推理场景 |
| INT4 | 8-32× | 较大 | 极致压缩、容忍精度损失 |
量化过程中还有一个容易被忽略的细节:校准集的选择。scale的计算依赖于激活值的分布范围,而这个范围是通过在校准集上跑前向传播统计出来的。如果校准集和实际推理数据的分布差异大,量化后的精度就会崩。我一般会从实际业务数据里抽几百到几千张有代表性的样本做校准,而不是随便拿训练集的一个子集凑数。
3.3 内存布局与数据排布的影响
NPU对输入数据的排布格式往往有特定要求。CPU上常见的NCHW格式(批次、通道、高、宽)在NPU上可能不是最优的,很多NPU更偏好NHWC或者专用的分块格式。如果输入格式不匹配,NPU要么需要额外的转换步骤,要么直接拒绝加速。
这个问题在部署时经常被忽视。比如你用某个推理框架加载了一个ONNX模型,输入是NCHW,但NPU编译器期望NHWC,它可能会在模型前面插入一个转置操作。这个转置如果落在CPU上执行,每帧都要多做一次全图数据重排,开销不小。解决办法是在模型导出阶段就把格式转好,或者在预处理流水线里直接输出NPU期望的格式。
4. 把模型搬上NPU的完整实操链路
4.1 工具链选型与模型转换
不同厂商的NPU配套工具链差异很大,但大体流程是相似的:训练框架导出 → 中间格式转换 → NPU编译器优化 → 生成可执行文件。以常见的流程为例,你可能有PyTorch或TensorFlow训练好的模型,先导出成ONNX,然后用NPU厂商提供的编译器把ONNX转成NPU能执行的格式。
这个转换过程是整个链路里最容易出问题的环节。常见的问题包括:算子版本不匹配(ONNX的某个算子版本NPU编译器不支持)、动态维度(NPU通常要求静态shape)、自定义算子丢失等。我的经验是尽量用ONNX的opset版本和NPU编译器文档里明确列出的版本对齐,不要盲目追新。如果模型里有自定义算子,提前写好对应的NPU实现或者找厂商要支持。
转换完成后,一定要用工具链自带的逐层对比功能验证精度。把原始模型和转换后模型在同一批输入上跑一遍,逐层对比输出差异。如果某一层误差突然变大,说明那一层的转换出了问题,需要单独排查。
4.2 量化校准的实操细节
量化校准这一步值得单独拿出来讲,因为它对最终精度的影响太大了。工具链通常提供两种模式:训练后量化(PTQ)和量化感知训练(QAT)。PTQ不需要重新训练,速度快,适合快速验证;QAT在训练阶段就模拟量化误差,精度更好,但需要重新训练模型。
对于大多数场景,我建议先用PTQ跑一版看看精度。如果精度掉得不多(比如Top-1准确率下降不到1%),直接用PTQ就行。如果掉得厉害,再考虑QAT。QAT的训练成本不低,而且需要修改训练代码插入伪量化节点,不是所有团队都有条件做。
校准集的准备有几个要点:样本数量不用太多,几百到一千张通常够了,但分布要覆盖实际场景。比如做安防监控的模型,校准集里就要包含白天、夜晚、逆光、遮挡等各种情况。另外校准集不需要标签,只需要输入数据,所以准备起来相对容易。
注意:有些工具链的量化校准是逐层独立进行的,不考虑层与层之间的误差累积。这种情况下,即使每层误差都很小,累积到最后一层也可能变得不可接受。如果遇到这种情况,可以尝试逐层量化敏感度分析,把敏感层排除在量化之外。
4.3 推理引擎的集成与调度
模型转换和量化完成后,最后一步是把NPU推理集成到实际应用里。这一步的核心工作是任务调度和内存管理。NPU通常是一个独立的硬件单元,CPU需要通过驱动和运行时库来提交推理任务、等待完成、取回结果。如果调度不当,CPU和NPU之间的同步开销可能吃掉不少性能。
一个常见的优化手段是流水线并行。当NPU在处理第N帧的时候,CPU可以同时做第N+1帧的预处理和第N-1帧的后处理。这样CPU和NPU各干各的,互不等待。实现上可以用双缓冲或者多缓冲队列,配合异步提交接口。很多NPU运行时都提供了异步API,但默认可能是同步模式,需要手动开启。
另一个容易忽略的点是内存对齐。NPU的DMA引擎通常要求输入输出缓冲区按特定字节对齐(比如64字节或128字节),不对齐会导致性能下降甚至报错。在分配缓冲区时要用对齐分配函数,不要直接用malloc。
5. 实测中遇到的典型问题与排查思路
5.1 推理结果不对:从量化误差到布局错位
NPU推理结果和CPU不一致是最常见的问题,排查起来需要耐心。我的排查顺序通常是这样的:
第一步,确认输入数据一致。把同一份输入分别喂给CPU模型和NPU模型,在模型入口处打印或保存输入张量的数值,逐元素对比。如果输入就不一样,那问题出在预处理阶段,可能是归一化参数不同、颜色通道顺序不同、或者数据类型转换有误。
第二步,逐层对比中间输出。如果输入一致但最终输出不同,就需要定位是哪一层开始出现偏差。工具链通常支持导出每层输出,把CPU和NPU的中间结果逐层对比。第一层出现明显偏差的地方就是问题源头。
第三步,判断偏差类型。如果偏差很小(比如1e-3量级),大概率是量化误差或浮点累加顺序不同导致的,属于正常范围。如果偏差很大(比如输出完全乱了),那可能是布局错位、算子实现错误、或者量化scale计算错误。
我遇到过一次典型问题:NPU上的卷积结果整体偏移了一个像素。排查后发现是padding模式不一致——CPU框架用的是SAME padding,而NPU编译器默认用了VALID padding。这种问题在逐层对比时很容易看出来,但如果不做逐层对比,只看最终输出,就会一头雾水。
5.2 性能不达预期:从算子回退到带宽瓶颈
NPU跑得慢的原因通常有几类。算子回退是最常见的:某些层不被NPU支持,被丢回CPU执行,导致NPU和CPU之间频繁切换。用工具链的profiling功能可以看到每层实际在哪个硬件上执行,如果发现大量层落在CPU上,就需要考虑替换算子或调整模型结构。
带宽瓶颈是第二常见的原因。NPU算力再高,如果数据喂不进去也是白搭。特别是当模型很大、片上SRAM装不下所有中间结果时,特征图需要反复在DRAM和SRAM之间搬运。这时候可以尝试减小批次大小、降低输入分辨率、或者用更激进的量化来减少数据量。
阵列利用率低是更深层的问题。如果模型的通道数太小(比如只有8或16个通道),而NPU的MAC阵列是256×256的,那大部分MAC单元都在空转。这种情况下,NPU的实际算力可能只有峰值的百分之几。解决办法是尽量用通道数较多的模型结构,或者在部署时把多个小模型合并成一个大批次一起推理。
5.3 功耗与散热的平衡
NPU满负荷运行时的功耗不容忽视。特别是在手机或嵌入式设备上,持续高负载推理会导致芯片温度快速上升,触发降频保护,实际性能反而下降。我实测过一块开发板,NPU连续跑满5分钟后,频率从1GHz降到了600MHz,推理延迟直接翻倍。
应对策略有几个方向:控制推理频率,不要每帧都跑,根据业务需求适当跳帧;动态调频,在负载低的时候降频省电,负载高的时候再升频;散热设计,加散热片或风扇,保持芯片在合理温度范围内。如果是电池供电的设备,还需要在性能和续航之间做权衡,有时候用CPU跑一个轻量模型反而比NPU跑大模型更省电。
6. 从项目经验里攒下来的几条实用建议
6.1 模型设计阶段就要考虑NPU约束
很多团队的做法是先训练一个精度最高的模型,然后再想办法往NPU上搬。这个顺序往往导致大量返工。更合理的做法是在模型设计阶段就把NPU的约束纳入考量:优先选择NPU原生支持的算子,控制通道数为8的倍数(对齐SIMD宽度),避免动态shape,尽量用标准卷积而不是各种变体。
如果精度不够,优先通过数据增强、更长的训练时间、知识蒸馏等手段来提升,而不是引入NPU不友好的复杂结构。一个NPU覆盖率95%的中等模型,实际推理速度往往比覆盖率60%的大模型快得多。
6.2 建立自动化精度回归测试
模型转换和量化过程中,每次调整都可能影响精度。手动验证既费时又容易遗漏。建议搭建一套自动化回归测试:准备一批固定输入和对应的CPU参考输出,每次转换后自动跑一遍,计算输出差异,超过阈值就报警。这套机制能帮你在早期发现大部分转换问题,避免把问题带到集成阶段。
6.3 保留CPU回退路径
无论NPU支持得多好,总会有一些边缘情况需要CPU兜底。比如输入尺寸异常、模型加载失败、NPU驱动异常等。在架构设计时保留一条CPU推理路径,作为降级方案。虽然CPU慢,但至少能保证功能可用。我见过不少项目因为NPU初始化失败直接崩溃,如果有CPU回退,至少能优雅降级而不是直接挂掉。
6.4 关注工具链版本更新
NPU厂商的工具链迭代很快,新版本往往修复了大量算子支持问题和性能bug。但升级工具链也可能引入新的兼容性问题。我的做法是锁定一个稳定版本用于生产,同时用另一个环境跟踪最新版本,定期评估是否值得升级。升级前一定要跑完整的精度和性能回归测试,不要只看release note就贸然切换。
6.5 实测数据比规格书靠谱
规格书上的TOPS数字是在理想条件下测出来的,实际性能受模型结构、内存带宽、编译器优化程度影响很大。选型阶段最好拿自己的实际模型去跑benchmark,而不是只看纸面参数。同一颗NPU在不同模型上的表现可能差好几倍,只有实测才能知道它适不适合你的场景。
我在一个视频分析项目里对比过两颗标称算力相近的NPU,一颗在卷积网络上表现很好,另一颗在Transformer结构上明显更优。如果只看规格书,根本看不出这个差异。后来了解到,前者对3×3卷积有专门的硬件加速通路,后者则对大矩阵乘法做了优化。所以匹配模型结构和NPU的硬件特性,比单纯比较算力数字重要得多。