
最近在评估一台带 NPU 的端侧设备时我遇到一个很典型的问题模型在 PC 上跑得好端端的一搬到 NPU 上延迟反而比 CPU 还高。翻日志才发现模型里某个算子不被 NPU 支持整张计算图被回退到了 CPU 执行。这件事让我意识到很多人对端侧 AI 的理解还停留在“框架能跑通就行”的层面但实际上从张量到 NPU 的底层执行链路才是决定端侧推理性能的真正战场。这篇文章我就把这个链路彻底拆开从张量谈起讲清楚 NPU 为什么快、什么时候快、什么时候不快以及你在部署端侧 AI 时会踩到的那些底层坑。1. 从数据结构说起为什么 AI 的眼里只有张量1.1 张量不是玄学就是有形状的数字盒子很多第一次接触端侧 AI 的开发者一看到“张量”两个字就先退三步总觉得这是什么高深数学概念。其实完全不用怕。张量说白了就是带形状的数组一个数字是零维张量一条数组是一维张量一张表格是二维张量而一张彩色图片在计算机里就是一个三维张量宽度、高度、通道各占一个维度。到了深度学习里因为要一次处理一批图片还得加一个批次维度于是就成了四维张量。你可以把张量想成是一个多层货架。货架本身有长、宽、高的尺寸每个格子里放一个数字。这个“尺寸”就是张量的 shape。PyTorch 里那种torch.randn(1, 3, 224, 224)意思就是“1 张图、3 个通道、高度 224、宽度 224”的货架格子里装的是随机数。那为什么 AI 计算非要统一用张量核心原因在于神经网络本质上就是一堆矩阵运算的复合。卷积是矩阵乘法加滑动窗口全连接是矩阵乘法Transformer 里的 Attention 还是矩阵乘法。既然底层的数学运算高度统一数据结构自然也要统一。框架层面把所有输入、中间特征图、输出都抽象成 Tensor好处非常明显编译器可以统一做内存分配、算子调度和形状推导不需要为每个具体的变量单独写一套逻辑。这一层抽象是所有后续优化的地基。1.2 数据布局与内存连续性性能差距的源头同样是形状为[1, 3, 224, 224]的张量存到内存里却是不同顺序的。常见的布局有两种NCHW 和 NHWC。NCHW 意味着先把第一个通道的整个 224x224 存完再存第二个通道NHWC 则是在每个像素位置上把 RGB 三个通道的值连续存放。单看代码只是permute一下的事但到了物理内存层面这个选择直接决定 NPU 能不能高效取数。这里有一个初学者最容易忽略的概念计算机内存是一维的线性空间高维张量只是逻辑结构。物理上它就是把所有数字按顺序铺开。NPU 的加速器硬件在设计时对数据怎么堆放有强烈的偏好。很多 NPU 天然喜欢 NHWC 布局因为它的卷积运算在遍历一个像素窗口时可以一次性连续读到多个通道的数据cache 命中率高数据复用也好。而 CPU 端常用的 PyTorch 默认是 NCHW模型里如果频繁做维度转置数据搬运和 padding 的开销会在端侧被放大得很明显。我实际测过一个简单的 3x3 卷积模型在 NPU 上如果布局不匹配编译器为了转换数据布局可能要多花 20%-30% 的额外推理时间。这还不是最严重的假如模型里有个自定义算子强制张量从 NPU 计算空间搬回 CPU 内存空间再搬回去那这个来回拷贝的耗时可能比算子本身的计算耗时还高一个数量级。所以在做端侧部署时第一件事不是看模型结构而是确认框架导出的 IR 中张量的物理布局和你目标 NPU 的偏好是否一致。2. NPU 的设计哲学专门为乘加运算盖的工厂2.1 CPU、GPU、NPU 的分工逻辑要理解 NPU 为什么快先得搞清楚它和 CPU、GPU 的分工差异。CPU 是一把瑞士军刀什么都能干分支预测、乱序执行、缓存调度全都为“逻辑复杂、分支多”的场景优化。GPU 是一条大型流水线成百上千个线程同时跑同样的指令适合图形渲染这种数据并行但指令流简单的场景。NPU 呢它连大型流水线都不算更像是一条专线只做乘加运算、激活、池化这类固定模式的操作。拿工厂类比是这样的CPU 是老师傅单间多复杂的手工活儿都能接但产量有限。GPU 是大型装配车间单位时间能处理很多并行任务但建造成本高、功耗大。NPU 是专用自动化产线只做某一种型号的零件一旦干起这个活儿效率和功耗都远超前两者但要让它干别的它立马抓瞎。端侧 AI 的特点是功耗预算极其有限但推理任务高度重复——几十次的卷积、矩阵乘、激活跑在同一个模型上千万遍。这种场景恰好是 NPU 最强的舒适区。它不需要解决通用计算问题只需要把“乘加”这件事做到极致再加上尽可能轻的数据搬运开销。2.2 算力、片上内存、数据流NPU 的三大核心一块 NPU 的硬件设计说白了就围绕三个部分。第一是 MAC 阵列也就是乘加单元阵列这是真正干活的算力核心INT8 算力指标就是它算出来的。第二是片上 SRAM通常几百 KB 到几 MB 不等用来临时存放输入、权重和中间结果。第三是 DMA 和数据流调度逻辑负责在外部 DRAM 和片上 SRAM 之间搬运数据。现实中制约 NPU 性能的往往不是算力而是“内存墙”。算法复杂度在那里放着CPU 里的冯诺依曼瓶颈也一样存在算力再高数据喂不进来一切都是空谈。所以 NPU 设计的核心不是“算得快”而是“喂得饱”——让 MAC 阵列的每个周期的输入数据都不中断。具体做法是数据重用和 tiling 分块。拿卷积举例一个 3x3 卷积核在滑动窗口时相邻窗口之间有很多重叠区域。如果每个窗口都从外部 DRAM 重新读一遍数据访存带宽会爆炸。NPU 的策略是把输入特征图分块放到片上 SRAM让卷积核在每个块内反复复用这些数据把外部访存次数压到最低。Transformer 的矩阵乘也是同理通过 tiling 把大矩阵切成小块在片上完成计算减少片外来回读取。2.3 为什么 NPU 跑 CNN 和 Transformer 特别快CNN 和 Transformer 这两大类主流模型本质都在做同一件事矩阵乘法加非线性激活。卷积在 im2col 之后也能变成大矩阵乘。所以 NPU 只要把 GEMM通用矩阵乘做扎实就已经覆盖了绝大部分端侧 AI 场景。NPU 里对 GEMM 的加速最常见的是脉动阵列Systolic Array或类脉动数据流设计。简单讲就是把乘加单元排成网格数据像水流一样在网格里流动每个单元只跟相邻单元通信避免每算一次乘法都要从寄存器堆重新取数。这种设计极大地减少了寄存器和内存之间的读写次数让计算单元能持续吃满。此外现代 NPU 普遍支持混合精度计算INT8、INT16、FP16 混着来。权重精度高一点的网络层用 FP16对精度不敏感的大矩阵用 INT8算得快还能减小内存带宽压力。端侧 AI 硬件部署时你经常听到某 NPU 能跑到多少 TOPS那个数值多数是 INT8 算力看着很唬人但实际要考虑带宽、算子匹配度和数据布局之后能打几折另说。3. 从框架到硬件的执行链路模型是怎么变成 NPU 指令的3.1 计算图与中间表示翻译的总站你在 PyTorch 里搭好模型不等于能直接把它丢给 NPU。框架里的模型是一个动态计算图Python 对象、动态 shape、各种控制流都还带着这些对硬件来说全是噪音。部署时第一件事是把它导出成一份固定的中间表示IR比如 ONNX、TFLite、OpenVINO IR 等等。一份合格的 IR 应该具备三个特点算子是确定且闭合的——所有算子都在目标工具链的支持列表里张量形状是静态可推导的——shape 都定死编译器才好规划内存量化信息已经固化——每层的 scale、zero_point 都写成常量推理时不再重新计算。说白了IR 就是把“动态的、灵活的 Python 代码”翻译成“静态的、死板的硬件指令蓝图”。这个环节经常出问题。我之前遇到一个模型里面有个torch.where的根据条件选数的操作导出到 ONNX 后又变成好几个小算子组合。NPU 编译器本来不支持其中某个组合模式于是回退到底层 CPU 去执行。这就是为什么导出以后最好做一次 IR 算子检查用工具链自带的模型体检工具跑一遍看看哪些算子在目标设备上会被拒绝。宁可早发现早处理也不要等部署上了再翻日志。3.2 算子的图优化与融合性能提升的大头IR 拿到手之后NPU 工具链还会做一堆图优化。其中收益最大、最常见的是算子融合。什么是融合就是把好几个连续算子合并成一个减少中间张量的写回和重读。典型的是 Conv BatchNorm ReLU。推理阶段的 BatchNorm 其实是纯线性变换它的参数可以和卷积层的权重做一步数学合并而 ReLU 又是一个逐元素算子完全可以在卷积输出的时候顺手做了。三步融合成一个算子中间那张特征图根本不需要一次性写回 DRAM直接在片上 SRAM 里就完成全部流程。这个优化对 INT8 模型尤其重要因为每次往返 DRAM 都是时间和带宽的双重消耗。我表示过这样的比喻算子融合就像工厂里把三道工序合并成一条连续产线一道工序做完直接滑到下一道根本不需要把半成品运到仓库再取出来。NPU 编译器里还有常量折叠、共同的子表达式消除这些基础优化但最关键的还在于融合。做端侧部署时如果发现某个模型在 NPU 上性能不达标先打开工具链的优化报告看看算子融合率是不是偏低这往往能直接定位问题。3.3 算子映射与异构调度一台设备上的多方调度优化完了就轮到真正的算子映射。NPU 不是每个算子都能直接支持它的编译器会把 IR 里的算子拆解成更底层的硬件原语看看能不能拼出对应的硬件指令。能映射的就跑 NPU不能映射的只能走回退路线CPU、GPU或者被拆成几个支持的模式重新组合。这里涉及端侧最常见的“异构调度”问题。以 Android 的 NNAPI 为例系统底层有多个可用的计算设备每个设备支持不同的算子集合优先级也不一样。运行时调度器会枚举模型里的算子决定哪一部分放哪个设备。TFLite 的 Delegate、CoreML、OpenVINO 的异构执行插件也是干同样的事。调度器听着很智能实际坑很多。最大的坑是“层间切换开销”。如果 NPU 算一个算子CPU 算另一个两个设备的计算结果要互相拷贝共享内存这种切换的耗时很可能大于算子本身的计算时间。所以好的调度器不会严格按算子粒度切开而是尽量把连续的 NPU 可执行子图合并成一个大的执行单元减少设备切换次数。这也是为什么一个模型里只要有一个关键算子不支持整张图就可能全部回退到 CPU因为硬拆成两个子图的收益可能反而是负的。4. 量化端侧 AI 的关键手术4.1 为什么端侧一定要量化FP32 模型在 PC 上跑没什么问题但放到端侧就变得很尴尬。一是体积大一个 100MB 的模型光闪存和内存占用就让人肉疼二是带宽需求高每次推理都要把这些参数从内存喂给计算单元FP32 是 INT8 的四倍直接导致功耗和延迟翻着倍地涨。量化要做的事就是把 FP32 的权重和激活值映射到更低比特位宽的整数空间。本质是做一个带缩放和平移的映射公式也很朴实对称量化是q round(x / scale)非对称量化是q round(x / scale) zero_point。选择哪种取决于数据分布如果数据分布在零附近大致对称用对称量化如果分布明显偏移比如 ReLU 输出恒为非负那非对称量化能保留更细的精度。还有一个维度是 per-tensor 和 per-channel。per-tensor 为整个张量算一个 scaleper-channel 则为权重矩阵的每一行算一个自己的 scale。一般来说权重因为分布相对固定适合用 per-channel激活值因为每次推理都在变只能用 per-tensor否则硬件实现成本太高。具体到端侧 NPU 的工具链里怎么组合这些模式往往直接决定最终精度的好坏。4.2 校准与精度调优实战量化之后模型精度掉多少不是拍脑袋定的要先做“校准”。校准的意思是用一小批有代表性的真实数据通常是几百张训练集之外的图片跑一遍 FP32 模型统计每个激活层的数值分布然后根据这个分布来确定 scale 和 zero_point。选什么统计方法非常关键。最简单的是 MinMax直接拿最大值和最小值当边界。但如果激活值里有几个极端离群点整个量化范围会被拉得特别宽大多数数据在量化后全挤在很窄的区间里精度直接崩掉。更稳妥的是 Percentile比如取 99.99% 分位数主动舍弃极小比例的极端值换取整体更高的量化分辨率。再复杂一点的还有基于 KL 散度去度量量化前后信息损失的方案不少成熟工具链默认就在用。我自己实操时的经验是权重量化一般比较安全真正敏感的是激活。尤其像 Sigmoid、Softmax 这类非线性输出数值范围天然受限但分布很极端——大量值集中在 0 或 1 附近量化步长稍微选错整个后续层就全部失真。遇到这种敏感算子最省事的解决方式是保留 FP16 混合精度或者把模型拆开只对敏感的子图用高精度其余部分用低精度。端侧 NPU 的收益曲线不是线性的百分之五的算子用高精度可能需要多花 20% 的延迟但换回的精度提升可能是决定性的。5. 端侧大模型与 NPU 的碰撞5.1 大模型在端侧跑什么内存带宽才是天花板这两年 AMD NPU 跑大模型、Intel NPU 跑端侧大模型甚至手机端跑 7B 模型都成了非常热门的话题。但大模型和传统 CNN 在端侧的执行特征有很大区别。CNN 的单次推理是计算密集型而 LLM 的逐 token 生成是访存密集型每生成一个 token都需要把所有权重参数从 DRAM 里读一遍。参数量越大这个访存开销越高。这时候你会发现NPU 算力再高如果内存带宽跟不上整个延迟还是会卡在“喂数据”这一环。这也是为什么端侧 LLM 都讲究权重量化到 4bit 或 8bit。4bit 不光是为了省存储更直接的意义是减少一次推理需要搬移的字节数。同一个 7B 模型FP16 跑一次生成的参数读取量约 14GB4bit 直接降到 3.5GB对带宽的压力成倍下降。除了权重还有 KV Cache 这个内存大户序列长度一上来缓存大小跟着线性增长。NPU 上做 Attention 计算时不光算得快还要考虑 KV Cache 怎么放、分块多大才不浪费片上 SRAM。5.2 ComfyUI 这类工具怎么用上 NPU最近有个热门方向是把 ComfyUI 这种基于节点式的图像生成工具接到 Intel 或 AMD 的 NPU 上跑 Stable Diffusion。原理上讲SD 的 UNet 部分全是卷积和 Attention理论上 NPU 干这种活应该是本职内的事。实际你去看社区的方案多半是借助 OpenVINO 这类工具链把模型先转换成 IR再挂到 NPU 后端去推理。但真实体验并不那么丝滑。ComfyUI 本身的生态是围绕 PyTorch 和 CUDA 搭的节点和自定义逻辑相当复杂NPU 工具链要完整支持动态形状、各种自定义节点工作量巨大。实际部署时经常出现部分算子回退、动态 shape 导致某些节点的图优化失效等状况。我之前试过一次UNet 的一个关键块在 NPU 上出图另外几个块跑 CPU整体提升有限最后只能把部分变换操作交给 GPUNPU 只处理卷积主体。所以我的判断是这类方案目前更适合做功耗优化或者说个人实验离生产级稳定还有距离。如果你身边也有人想试我建议先跑最原始的官方模型和官方工作流别一上来就用一堆社区自定义节点否则光排查兼容性问题就能耗掉一个下午。6. 实战排查我踩过的那些 NPU 推理坑6.1 算子不支持导致的整图回退这是端侧部署最常见也最隐蔽的问题。症状是模型部署到 NPU 后推理延迟不但没降低反而比纯 CPU 还高而且功率表现也不对。排查思路很简单先打开工具链的日志搜索 fallback、cpu 这些关键词。如果发现某个算子在 NPU 设备上不被支持并被调度器自动回退那就看这张图回退的粒度。很多时候回退的不是单个算子而是整个子图——因为调度器发现一个关键算子不支持干脆把一大段连续子图都丢给 CPU避免来回切换。解决手段集中在模型层面替换算子、调整小结构或直接改模型本身把不支持的算子绕过去。例如之前遇到过一个FloorDiv算子NPU 编译器不支持我直接在导出前用torch.div加取整的方式改写恢复了对 NPU 的映射支持。6.2 算力高但速度上不去内存带宽瓶颈还有一类情况是NPU 的 TOPs 看起来很漂亮但实际跑起来速度很一般。这时要检查的往往不是算力吃没吃满而是内存带宽。尤其是大模型场景权重读取的带宽消耗远大于计算单元的消化能力。遇到这类问题处理手段也很明确量化位宽再降一档尝试算子融合来减少中间结果搬运或者把模型做工程化裁剪。一个容易忽略的细节模型里如果有一个很大的中间特征图比如 [1, 512, 64, 64] 的 FP32 张量它在 NPU 上每计算一次就要被写回 DRAM 一次。如果图优化没把它和前后算子融合这个读写开销会非常可观远大于计算本身。实测下来有些模型光把中间张量改成功 INT8 输出推理速度就有显著提升代价只是略微精度变化。6.3 精度漂移和热降频两个稳定性的坑NPU 上跑 INT8 模型和你在 GPU 上跑的 FP32 结果数值有差异是正常的。这种差异在分类任务里可能不影响 top1但在目标检测的边界框回归这类连续性输出上可能造成准确率指标的轻微波动。排查精度问题时最重要的是先确认对比条件一致同样的输入数据、同样的预处理流程、同样的数据归一化方式。我见过不少人对齐了半天最后发现是两边读图时的缩放插值算法不一样。另外就是热降频问题。端侧设备散热条件差NPU 连续高负载跑几分钟后芯片温度上来频率会明显下降导致推理延迟从 20ms 飘到 40ms。这一点在做性能基准测试时经常被忽略。测试时不能只看第一次推理的峰值性能一定要跑长循环、取稳态性能比如连续推理 100 次以上再看平均延迟。这样才能拿到真实可用的数据。最后分享一个小技巧做端侧 NPU 部署时我习惯先跑通 CPU 推理再切 NPU 后端并且每一步都分开测延迟对比“哪个算子快了、哪个算子反而慢了”。这样定位问题的颗粒度会非常细。很多时候所谓 NPU 部署失败不是算力不行而是数据布局不对、算子覆盖不完整、图优化没生效。把执行链路一条条拆开看问题基本藏不住。