☰
从张量到乘累加阵列:端侧AI芯片NPU底层执行逻辑全拆解
2026/10/5 5:46:43 网站建设 项目流程

最近后台经常有人问我:手机里跑的大模型,和电脑上跑有什么区别?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×224=150528 个 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。

举个例子:一个规模为M=4096, K=4096, N=4096的矩阵乘,总计算量是 4096³ ≈ 687 亿次乘加,约 1374 亿 FLOPs。假设你的芯片峰值算力是 10 TOPS,理想状态下一次矩阵乘只需要约 0.14 秒。但如果你在手机 CPU 上跑,同时还在做大量内存搬运,实际耗时可能是这个数字的十倍甚至几十倍。问题很少出在"算不动",而是出在"数据喂不过去"。

这个现象用一个生活类比来解释:矩阵乘法像一条流水线工厂,每个工位负责一次乘加。你要让工位不停干活,就得不停往工位递材料。如果一个工人干一秒活要等十秒材料,工厂再大也没用。芯片设计者早就想到了这一点,所以 NPU 的第一个核心设计目标不是"算得更快",而是"少搬数据、重复利用已经搬进来的数据"。这也就引出了访存和算力的关系。

2. 从算子到计算图:推理引擎拿到的到底是什么

2.1 计算图是模型的骨架

训练框架里你可以写一段 Python 代码把模型定义出来,但真正部署时,推理引擎拿到的一般不是 Python 对象,而是一张静态计算图。无论是 ONNX、TFLite,还是各家 NPU 编译器使用的中间表示,本质都是同一个东西:节点是算子,边是张量,指向关系描述数据流向。

为什么要转成静态图?因为静态图给足了优化空间。推理引擎可以在运行前把整张图扫一遍,做常量折叠(把能提前算的节点算掉)、算子融合(把多个小算子合成一个大算子)、布局转换(把图统一成硬件喜欢的 NCHW 或 NHWC)、内存规划(把所有中间张量的生命周期排好,复用同一块内存)。这些优化在动态图里做不了,因为动态图要保留 Python 层的灵活性,跑一步算一步。

举个最经典的算子融合例子,Conv+BN 融合。训练时 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 Element(PE)组成,每个 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 的"超能力"

如果只算 FP32,NPU 的能效优势还没那么大。端侧 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 Zero,AMD 有 Ryzen AI 软件栈,手机平台又有 NNAPI、QNN、Core ML 等各自体系。这种碎片化正是端侧部署最让人头疼的地方。

4. 底层执行逻辑:从模型文件到 NPU 上的一条指令

4.1 运行时和后端的分配逻辑

现在可以聊"底层"了。你手里有一个model.onnx文件,把它部署到 NPU 上,这个过程大致分四步:

  1. 加载模型,解析计算图;
  2. 对图做优化:常量折叠、算子融合、布局转换;
  3. 做算子分配:决定哪些算子给 NPU,哪些留在 CPU;
  4. 为 NPU 后端编译算子、规划内存,运行时按顺序提交执行。

以 ONNX Runtime 为例,它把不同的硬件适配层叫 Execution Provider(EP),比如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:i+BM, k:k+BK] B_tile = B[k:k+BK, j:j+BN] C[i:i+BM, j:j+BN] += 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 算子融合:为什么能带来数倍收益

前面讲过 Conv+BN 融合,这里说一个更综合的例子:卷积 + 残差加 + 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 模型大小约 17MB,INT8 后约 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 是专门为低精度矩阵乘和访存优化设计的专用执行器。真正决定部署质量的,往往不是某个硬件有多先进,而是你对数据布局、算子融合、量化、同步这几件小事的把控。希望这篇长文能帮你把脑子里那幅"数据怎么在芯片里流动"的画面补完整,少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询