
最近后台经常有人问我手机里跑的大模型和电脑上跑有什么区别NPU 到底是个什么芯片凭什么能让 AI 跑得更快每次回答都要从头讲起干脆写一篇长文从张量这个概念开始一直拆到 NPU 的乘累加阵列把端侧 AI 的底层执行逻辑完整捋一遍。这篇文章不追求让你成为硬件设计专家而是让你在部署模型、调性能、排查问题时脑子里能有一个准确的画面数据是怎么进来的、在芯片里走了什么路、为什么有些算子快有些算子慢、真正卡住性能的到底是算力还是带宽。适合三类人看一是刚接触端侧 AI 硬件部署的开发者二是被各种缩写名词绕晕的算法工程师三是想在手机、PC、嵌入式设备上把模型真正跑起来的落地团队。看完不敢说你能手写一个 NPU 驱动但至少不会再被“45 TOPS”“INT8 算力翻倍”这种宣传词带偏。1. 张量AI 计算的基本单元也是一段内存的视角1.1 张量不是玄学是带形状的内存视图先说一个被讲烂但又必须讲清楚的概念张量。很多教程喜欢从0 阶张量是标量、1 阶张量是向量、2 阶张量是矩阵开始讲说得没错但对你理解部署没什么帮助。我更喜欢用一个工程视角张量本质上就是一段连续内存加一套描述这段内存怎么切分、怎么读取的元信息。举一个最典型的例子一张输入给图像模型的 RGB 图片在 PyTorch 里长这样import numpy as np x np.random.rand(1, 3, 224, 224).astype(np.float32) print(x.shape) # (1, 3, 224, 224) print(x.strides) # (602112, 200704, 896, 4)这里shape的四个数分别表示batch 为 1、通道数为 3、高度 224、宽度 224也就是常说的 NCHW 布局。真正存放在内存里的其实就是 1×3×224×224150528 个 float32 数每个数占 4 字节总共约 600KB 的连续地址空间。strides告诉你的是想跳到下一个通道需要跨过 200704 字节想跳到下一个像素需要跨过 896 字节想跳到下一个列只需要 4 字节。为什么要强调这一点因为后面所有跟性能相关的坑有一半都出在这里。比如 PyTorch 里做一次transpose或view通常只改变元信息不复制底层数据。这很快但 NPU、GPU 这类加速器往往要求张量在内存里按特定方式连续排列一旦布局不对你就得做一次显式拷贝。这一步看似不起眼在端侧可能比 NPU 算那一层还慢。1.2 为什么说 AI 模型就是张量运算的堆叠你去看任何一个神经网络不管它叫 ResNet、Transformer 还是什么展开之后就是一张由算子组成的有向无环图。算子之间的流动对象全部是张量。权重是一个张量输入是一个张量中间每一层的激活值也是一个张量梯度和缓存同样用张量表示。核心算子其实就那几类卷积Conv、矩阵乘GEMM、加偏置BiasAdd、归一化LayerNorm/BatchNorm、激活ReLU/SiLU、Softmax、注意力里的 QKV 投影和缩放点积。这些算子全部是对张量的变换要么改变张量的数值要么改变张量的形状和布局。理解这一点对排错特别重要你遇到的所有shape mismatch、layout not supported、data type not match本质上都是你手里的张量视图和下一个算子期望的张量视图对不上。1.3 矩阵乘法才是算力的心脏为什么各大芯片厂商宣传算力时都拿矩阵乘法说话因为神经网络里最重的计算几乎都被矩阵乘法承包了。全连接层Y XW b本质就是一次矩阵乘加Transformer 里的 Attention 核心是三个矩阵乘卷积在实际推理时也常被转换成矩阵乘法来做常见的有 im2col 展开成显式 GEMM或者在卷积核内部使用 implicit GEMM 计算。具体算一次矩阵乘要多少次运算假设A[M×K]乘以B[K×N]得到C[M×N]需要做M×N×K次乘法和同样次数的加法。工程里常用 FLOPs浮点运算次数和 MACs乘加运算次数两个单位一次乘加算 1 MAC等于 2 FLOPs。举个例子一个规模为M4096, K4096, N4096的矩阵乘总计算量是 4096³ ≈ 687 亿次乘加约 1374 亿 FLOPs。假设你的芯片峰值算力是 10 TOPS理想状态下一次矩阵乘只需要约 0.14 秒。但如果你在手机 CPU 上跑同时还在做大量内存搬运实际耗时可能是这个数字的十倍甚至几十倍。问题很少出在算不动而是出在数据喂不过去。这个现象用一个生活类比来解释矩阵乘法像一条流水线工厂每个工位负责一次乘加。你要让工位不停干活就得不停往工位递材料。如果一个工人干一秒活要等十秒材料工厂再大也没用。芯片设计者早就想到了这一点所以 NPU 的第一个核心设计目标不是算得更快而是少搬数据、重复利用已经搬进来的数据。这也就引出了访存和算力的关系。2. 从算子到计算图推理引擎拿到的到底是什么2.1 计算图是模型的骨架训练框架里你可以写一段 Python 代码把模型定义出来但真正部署时推理引擎拿到的一般不是 Python 对象而是一张静态计算图。无论是 ONNX、TFLite还是各家 NPU 编译器使用的中间表示本质都是同一个东西节点是算子边是张量指向关系描述数据流向。为什么要转成静态图因为静态图给足了优化空间。推理引擎可以在运行前把整张图扫一遍做常量折叠把能提前算的节点算掉、算子融合把多个小算子合成一个大算子、布局转换把图统一成硬件喜欢的 NCHW 或 NHWC、内存规划把所有中间张量的生命周期排好复用同一块内存。这些优化在动态图里做不了因为动态图要保留 Python 层的灵活性跑一步算一步。举个最经典的算子融合例子ConvBN 融合。训练时 BatchNorm 依赖 batch 内的统计量必须逐 batch 计算。但推理时用的是全局统计量均值和方差都是常量于是可以把 BN 的缩放和平移系数直接融进卷积权重里[ W W \times \frac{\gamma}{\sqrt{\mathrm{var}\epsilon}}, \quad b (b - \mathrm{mean}) \times \frac{\gamma}{\sqrt{\mathrm{var}\epsilon}} \beta ]融合之后原来需要跑两个算子的位置变成一个卷积算子中间那个 NCHW 布局的激活张量也不用写回内存再读出来了。别小看这一处省下来的读写在几百层的模型里每个中间张量都写回再读带宽消耗会非常惊人。2.2 算子耗时不公平用 Roofline 模型看瓶颈做端侧部署第一个常见认知误区是算力高的芯片一定快。实际上一个算子的耗时取决于它落在哪个区域是算力受限还是访存受限。这里有一个简化版的 Roofline 模型可以帮你建立直觉。一个算子的计算密度arithmetic intensity定义为单位字节访存承担的浮点运算次数[ \text{intensity} \frac{\text{FLOPs}}{\text{访存字节数}} ]如果计算密度很高说明数据复用充分性能天花板由峰值算力决定如果计算密度很低说明每个字节只被用了一两次性能天花板由内存带宽决定。大矩阵乘属于前者因为一个权重可以被很多输入反复使用而单点激活函数属于后者因为每个元素基本上只读一次、写一次。我自己测试过一个很典型的场景在一个 112×112 的 feature map 上做 64 通道的 3×3 卷积单层计算量算下来不算大但你如果每层之间都做一次激活、写回内存、再读出来做下一层带宽立刻爆掉。所以推理引擎在优化时特别强调一个动作把卷积 激活 残差加捏成一个算子让中间结果留在片上内存里。这也是为什么你在 NPU 上跑同一个模型开启算子融合前后性能可能差 2 到 5 倍。3. NPU为张量而生的专用芯片3.1 CPU、GPU、NPU 的分工先理清楚三者的定位。CPU 是通用计算单元擅长分支判断、乱序执行、处理复杂逻辑但它的并行度有限大批量浮点运算效率不高。GPU 拥有上千个并行核心适合重计算、高吞吐的任务但功耗和面积都很大数据中心可以接受手机和笔记本不行。端侧设备的特点是功耗和散热被严格约束。一块手机 SoC 的整机功耗预算可能只有几瓦你不能在上面塞一块桌面级 GPU。于是芯片厂商做了一个非常直觉的选择既然 AI 模型里绝大多数计算都是低精度矩阵乘法那就专门做一个只干这一件事的电路把面积和功耗都省下来。这就是 NPU 的核心思路用专用化换取能效比。这里需要强调一点厂商宣传的 TOPS 是理论峰值不是实际性能。以一款宣称 45 TOPS 的 NPU 为例它在跑真实模型时的有效算力可能在 5 到 10 TOPS 之间波动具体取决于数据布局、算子是否支持、访存是否充足。所以拿TOPS横向对比芯片可以但别把它当成模型一定能跑多快的保证。3.2 乘累加阵列是 NPU 的心脏NPU 最核心的计算单元是乘累加阵列由大量 Processing ElementPE组成每个 PE 负责一次乘加运算。业界最有名的结构是脉动阵列数据像动脉血液一样按固定节奏在 PE 之间流动每个 PE 只和相邻 PE 通信不需要每条数据都从全局内存里取。一个 128×128 的阵列一个时钟周期可以做 16384 次乘加。如果频率是 1GHz理论上每秒能做约 16.4 万亿次乘加也就是 16.4 TOPS 的 INT8 峰值。实际产品里还会叠加多核、多个阵列并行所以你会看到手机 NPU 动辄几十 TOPS。但注意NPU 不是只算矩阵乘法。卷积、矩阵乘之外还有 Softmax、LayerNorm、量化、池化这些算子它们不一定适合脉动阵列。大多数 NPU 会配备一组 SIMD 向量单元来处理这类计算或者把不支持的算子回退给 CPU。这就是为什么你在 NPU 上经常会遇到算子不支持自动 fallback 到 CPU的情况而一旦回退发生CPU 和 NPU 之间的数据复制成本往往比算子本身还贵。3.3 低精度和量化NPU 的超能力如果只算 FP32NPU 的能效优势还没那么大。端侧 NPU 真正厉害的地方在于低精度计算最常见的是 INT8 和 FP16现在还有 INT4。为什么低精度这么关键两个原因。第一存储和带宽按比例下降。一个 FP32 权重占 4 字节转成 INT8 只占 1 字节同样一片内存能装下 4 倍权重读取同样大小权重只需要 1/4 的带宽。对于 LLM 这种权重动辄几个 GB 的模型带宽往往比算力更快成为瓶颈。第二低精度乘累加电路更小、更快、更省电。INT8 乘累加的面积和功耗远低于 FP32同样的芯片面积可以做更多计算单元。从 FP32 量化到 INT8 的公式很简单[ q \mathrm{round}(\frac{x}{s} z) ]其中s是缩放系数z是零点。反量化则是x ≈ (q - z) × s。看起来容易实际操作全是坑如果校准数据分布不均匀某些通道的数值范围过大per-tensor 量化很容易溢出换成 per-channel 量化可以缓解但不同 NPU 对 per-channel 的支持程度又不一样。这块我后面实操部分会展开讲。另外简单回应一下热搜里提到的 AMD NPU 和 Intel NPU。AMD 的 Ryzen AI 走的是 XDNA 架构提供专门的 AI Engine集成在锐龙处理器里Intel 的 Core Ultra 处理器里的 NPU则通常通过 OpenVINO 工具链来调用。它们都是专用张量加速器但指令集、驱动、编译工具链完全不一样Intel 用 OpenVINO 和 Level ZeroAMD 有 Ryzen AI 软件栈手机平台又有 NNAPI、QNN、Core ML 等各自体系。这种碎片化正是端侧部署最让人头疼的地方。4. 底层执行逻辑从模型文件到 NPU 上的一条指令4.1 运行时和后端的分配逻辑现在可以聊底层了。你手里有一个model.onnx文件把它部署到 NPU 上这个过程大致分四步加载模型解析计算图对图做优化常量折叠、算子融合、布局转换做算子分配决定哪些算子给 NPU哪些留在 CPU为 NPU 后端编译算子、规划内存运行时按顺序提交执行。以 ONNX Runtime 为例它把不同的硬件适配层叫 Execution ProviderEP比如openvinoEP、qnnEP、coremlEP。编译模型时runtime 会逐个节点检查这个算子 NPU 后端支持吗支持就分配给它不支持就标注为 CPU。最终可能形成NPU 算一段CPU 算一段的混合执行图。新手最容易误解的就是这里你问模型跑在 NPU 上吗答案往往是部分跑在 NPU 上。一旦某个关键算子 fallback 到 CPU每次跨设备执行都要把张量从 NPU 内存复制到 CPU 内存再复制回去。这个拷贝开销不写在算子耗时里很难一眼发现但实测性能掉得惊人。4.2 张量布局与切块硬件不喜欢大矩阵NPU 不喜欢把整个大矩阵硬塞进去原因是片上内存有限。拿一个4096×4096的矩阵来说FP32 下就要 64MB大多数端侧 NPU 的片上 SRAM 只有几 MB 到几十 MB。所以硬件在算矩阵乘法时一定会做 tiling也就是切块。一个典型的 tiled GEMM 伪代码长这样BM, BN, BK 64, 64, 32 for i in range(0, M, BM): for j in range(0, N, BN): for k in range(0, K, BK): A_tile A[i:iBM, k:kBK] B_tile B[k:kBK, j:jBN] C[i:iBM, j:jBN] A_tile B_tile为什么把 K 放在最内层循环因为C的累加块可以一直留在片上寄存器里A和B的小块按需加载。如果反过来把i放最内层每次累加都要重新读C访存开销会成倍上升。除了切块还有布局问题。同一个张量NCHW 和 NHWC 在内存里的排列顺序完全不同。很多 NPU 的矩阵乘单元更喜欢 NHWC 或某些特殊格式因为它保证通道维在内存上连续访存更友好。从训练框架导出的模型默认可能是 NCHW部署时就得做一次布局转换。这个转换如果由 NPU 驱动自动插入你感觉不到但它消耗的时间和带宽是真实存在的。还有一个更麻烦的问题动态 shape。训练时你可能习惯让模型支持任意 batch size但 NPU 的静态编译喜欢固定 shape因为所有中间张量的大小和内存偏移都在编译期确定。模型里有一个动态维度内存规划就做不了很多 NPU 编译器直接报错。端侧部署的惯例是固定输入尺寸或者把可变维度通过 padding 对齐到固定值。4.3 指令下发与同步看不见的时间陷阱模型编译完成后运行时向 NPU 提交一个执行任务这个过程大致是驱动程序把输入输出 buffer 的地址、算子描述符打包成一个任务提交到硬件队列然后 NPU 开始执行。硬件执行完会写一个事件/中断来通知主机。这里有一个非常经典的坑异步执行。为了不阻塞 CPU驱动一般不会让infer()调用傻等。你提交了一个任务它立即返回接着你又往同一个输入 buffer 里写了新数据如果上一轮任务还没真正读完数据就被覆盖了。所以很多运行时 API 要求你在复用 buffer 前先同步或者采用双缓冲机制一块 buffer 在推理另一块在写入交替使用。另一个容易忽略的事是低功耗状态。NPU 为了省电空闲时会进入低功耗模式。你的应用第一次调用 NPU 时驱动要把硬件唤醒、加载固件、初始化流水线这个时间可能长达几毫秒到几十毫秒。所以在服务端预热 NPU 是必须的否则你压测看到的第一个请求延迟会异常高。4.4 算子融合为什么能带来数倍收益前面讲过 ConvBN 融合这里说一个更综合的例子卷积 残差加 ReLU三段可以融合成一个 kernel。常规做法是卷积结果写回 DRAM残差加读取两块数据写回ReLU 再读取写回。融合之后卷积结果直接留在片上加法和激活紧跟着完成最终只写一次 DRAM。图优化里还有一类常见的节点叫 QDQ也就是QuantizeLinear / DequantizeLinear成对出现。量化模型里如果硬件本身支持低精度输入输出这些 QDQ 节点可以被吸收进算子内部让上游直接产出 INT8下游直接消费 INT8省掉两次反量化加量化。反之如果硬件对 INT8 输入支持得不好编译器会在每个算子边界插入反量化模型性能立刻崩盘。调优时不要一上来就怪 NPU 慢。先用 profiler 看算子耗时表把耗时排前几名的算子拎出来看是不是布局转换节点是不是某个 fallback 到 CPU 的算子是不是大量小算子没有被融合绝大多数NPU 跑不过 CPU的案例最后查出来都不是算力问题而是数据在来回搬运。5. 实操把一个模型真正跑到端侧 NPU 上5.1 模型转换与静态化流程以最常见的场景为例PyTorch 训练好的模型要部署到一台带 Intel NPU 的设备上。完整流程分几步走。第一步导出 ONNX。注意导出时尽量固定输入 shape或者明确哪些维度是动态的import torch model torch.load(model.pth) model.eval() x torch.randn(1, 3, 224, 224) torch.onnx.export( model, x, model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )如果你确定部署时 batch 固定为 1就不要加dynamic_axes。动态 shape 在 NPU 上是麻烦源头能固定就固定。第二步用onnxsim做一遍图精简去掉冗余节点python -m onnxsim model.onnx model_sim.onnx这一步会把一些常量折叠掉顺带统一图结构。很多算子融合优化在精简图上跑得更彻底。第三步检查目标 NPU 的算子支持列表。以 OpenVINO 为例它会对不支持的算子报错或者自动回退到 CPU。建议你做一个最小化测试先把模型编译到 NPU确认没有红色报错再检查模型包含的算子类型逐个确认哪些是真正跑在 NPU 上的。第四步量化校准。这一步单独说。5.2 以 OpenVINO 调 Intel NPU 为例代码非常简洁OpenVINO 把底层的 Level Zero 驱动完全封装了import cv2 import numpy as np from openvino import Core core Core() model core.read_model(mobilenet.xml) compiled core.compile_model(model, NPU) img cv2.imread(cat.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) resized cv2.resize(img, (224, 224)) input_tensor np.expand_dims(resized.transpose(2, 0, 1), 0).astype(np.float32) request compiled.create_infer_request() # 建议 warmup for _ in range(5): request.infer({input: input_tensor}) result request.infer({input: input_tensor}) output result[compiled.output(0)] print(np.argmax(output[0]))这里有两个实测经验。第一compile_model本身可能包含图编译和驱动初始化耗时不短但如果放在服务启动阶段做不影响在线请求。第二第一次infer往往包含 NPU 唤醒和流水线预热benchmark 前一定要跑几轮 warmup否则你测出来的延迟比真实值高一大截。用官方benchmark_app跑对比更直观benchmark_app -m mobilenet.xml -d NPU -t 10 benchmark_app -m mobilenet.xml -d CPU -t 10在我手头的一台 Core Ultra 设备上MobileNetV3 单张推理 CPU 大约 5 到 8 毫秒NPU 大约 3 到 5 毫秒差值看起来不大。但换成更大一点的模型或者提高 batch 并发NPU 的优势会明显拉大。这是 NPU 的特性单个小模型的启动和调度开销占比高吞吐场景更能发挥它的价值。5.3 量化实践从 FP32 到 INT8 的完整收益量化是端侧部署里收益最高、坑也最多的环节。以 OpenVINO 为例你可以用optimum-cli或者 Neural Network Compression Framework 做量化也可以自己用校准集统计激活的 min/max计算出 scale 和 zero_point 之后手动把 FP32 的权重转成 INT8。我推荐先用官方工具链跑通再手动微调特殊层。校准集的选择非常关键。有次我图省事拿了一套纯白底商品图做校准结果模型上线后遇到自然光线下的图片第一层卷积的输出分布直接漂移精度掉了一大截。校准集必须和真实部署场景的数据分布接近数量不需要太多100 到 500 张代表作足够。量化之后做两件事一是对比 FP32 和 INT8 的输出相似度可以用 cosine similarity 或者绝对误差热力图二是重新跑 benchmark确认延迟和内存是否有改善。以我自己跑过的 MobileNet 为例FP32 模型大小约 17MBINT8 后约 4.3MB内存占用下降明显推理延迟在 NPU 上大概能再降 20% 到 40%。为啥因为权重变小后访存时间成比例下降而 MobileNet 这种小模型恰恰是访存受限型所以量化收益远大于算力收益。有一个经验分享不是所有层都适合量化。第一层卷积直接吃原始像素对量化误差很敏感可以保留 FP16最后一层分类头如果输出要精确对齐也可以留 FP32。很多工具支持按层配置精度虽然配置起来麻烦但精度和性能两者兼得。6. 常见问题与排查技巧实录6.1 端侧 NPU 问题速查表我把这几年踩过的坑整理成一个速查表排查时可以按顺序对照现象可能原因排查方向模型编译失败算子不支持、opset 太新、动态 shape、dtype 不被支持查看算子支持列表固定输入 shape检查 dtype 是否为 fp32/int8尝试回退 CPU性能比 CPU 还慢模型太小、NPU 唤醒/调度开销占比高、大量算子回退用 profiler 看每层耗时检查是否有跨设备数据拷贝增大 batch 或改用吞吐模式精度异常量化校准集不合适、per-tensor 溢出、部分算子回退后数值路径不一致检查输出 diff换校准集对敏感层跳过量化设备找不到驱动未安装、固件过旧、BIOS 里 NPU 被关闭、虚拟化环境确认设备管理器/系统设备列表更新驱动和固件内存爆掉中间 tensor 峰值过高、batch 太大、未做内存复用、多任务并行减小 batch查看工具链显式内存规划关闭多余的后端缓存结果不稳定/偶发错误异步提交 buffer 被覆盖、未同步、驱动版本 bug检查同步机制改用双缓冲更新驱动这个表里的每一项我都实际遇到过。尤其是性能比 CPU 还慢那条很多开发者第一反应是 NPU 没用其实十有八九是模型太小 未做融合 每次调用都有唤醒开销三个因素叠加的结果。判断方法很简单测完延迟后再用同一个模型的融合版本在 NPU 上跑一轮对比算子级耗时分布很快就能定位。6.2 独家避坑经验第一拿到一块新的 NPU先做算子级最小用例测试。别一上来就把大模型塞进去。我团队第一次调 Intel NPU 时就是用单算子模型逐个验证卷积、残差加、Softmax、量化节点确认哪几个算子支持、哪几个会回退效率比对着大模型报错日志猜高太多。具体做法写一个三行代码的小模型只包含一个 Conv2d导出 ONNX编译到 NPU跑一遍再逐步增加算子。每加一个算子都能立刻定位是哪一个出的问题。第二数据布局比你想的更值钱。很多 NPU 要求输入行按 64 字节对齐或者要求权重按特定 format 预打包。OpenVINO 这类工具会在编译时自动做但某些直接调驱动的场景你要自己保证布局正确。排查性能问题时我记得先看 profiler 里有没有layout conversion或copy类型的节点它们的耗时经常排进前三。第三warmup 不是玄学是必须写在代码里的。我见过一个团队压测 NPU 推理第一个请求延迟 200ms后面每个请求 8ms他们以为系统不稳定。其实只是 NPU 从低功耗状态唤醒了一次。把前 5 到 10 次推理作为预热之后的数据才是真实的稳态表现。第四服务端如果同时跑 CPU 和 NPU注意跨界数据拷贝。有时候你想让预处理在 CPU 上做然后传给 NPU看起来很合理但每张图传过去都是一次 PCIe/内存拷贝。更聪明的做法是把预处理算子也融合进模型图里让整张图尽量留在 NPU 端只在最后输出一个结果张量给 CPU。最后再分享一个小技巧这是我自己的习惯也算是给刚接触端侧 AI 的同行一个建议拿到任何一块新 NPU我都先写一个最小闭环——一个 3 层卷积的小模型从转换、量化、编译、推理到性能对比全走通再上真实模型。这个最小闭环能帮你快速理解这个硬件平台的性格它是偏好 NHWC 还是特殊布局INT8 支持得干不干净算子融合到底做没做warmup 要几轮待这些脾气摸清了大模型部署其实就是流水线活。端侧 AI 的底层执行逻辑说穿了并不复杂张量是数据在内存里的形状算子和计算图是程序的骨架NPU 是专门为低精度矩阵乘和访存优化设计的专用执行器。真正决定部署质量的往往不是某个硬件有多先进而是你对数据布局、算子融合、量化、同步这几件小事的把控。希望这篇长文能帮你把脑子里那幅数据怎么在芯片里流动的画面补完整少走几步弯路。