☰
端侧AI部署底层逻辑:从张量布局到NPU性能瓶颈
2026/10/7 5:31:07 网站建设 项目流程

做了快十年的端侧 AI 部署工作,有个问题我几乎每隔几天就会遇到一次:同一个模型在 GPU 云主机上跑得飞快,一到手机、开发板、工业盒子上就卡得怀疑人生。问题很少出在模型本身,大概率是底层的执行链路没理顺——从张量(Tensor)进入算法框架的那一刻起,到 NPU 真正把运算做完,这中间任何一环做得糙,你看到的帧率都会非常诚实。这篇文章不聊算法创新,也不聊怎么训练模型,我想把端侧 AI 的底层执行逻辑这条链路拆开,从张量的内存形态讲到 NPU 的架构取舍,再落到大模型在端侧 NPU 上到底能跑多快、瓶颈卡在哪。无论你是做嵌入式、做上位机,还是准备把 LLM 塞进本地设备,这套逻辑都值得当成基础设施来看待。

1. 先搞清楚张量:AI 世界里没有“二维数组”,只有“字节流 + 元信息”

1.1 张量在内存里的真实样子

大多数开发者对“张量”的第一印象来自 PyTorch 或者 TensorFlow 教程:张量就是带维度的数组,torch.tensor([1, 3, 224, 224])无非是个多层嵌套的数据容器。这么说没毛病,但如果只停留在“带维度的数组”这个抽象层,端侧部署时一定会吃亏。

在 NPU 看来,世界上只有两种东西:连续内存块,以及描述这块内存的元信息。张量就是这两者唯一的组合形态。你心里想着“这是三通道 224x224 的图像”,硬件看到的却是一段连续的、按特定顺序排列的数字序列,元信息里记录了它的 shape、dtype(FP32、INT8 还是 INT16)和 data layout。换句话说,张量不是“数据结构”,而是“内存协议”。同一份模型,换一个设备,张量的字节排列变了,性能就可能差出好几倍。

我在实际项目里见过太多人把模型的输入输出当成理所当然的“数组”来对待,结果一迁移到 NPU 就发现边界条件全得重来。建议所有做端侧部署的团队,把模型输入输出的张量形态当作一份接口契约来维护,shape、dtype、layout、对齐方式都写清楚,这比事后排查快得多。

1.2 NCHW 与 NHWC:布局问题为什么重要

图像张量最典型的 shape 是[N, C, H, W],但在内存里排列方式有两种主流选择。NCHW 把每张图的全部通道连续放好,再放下一张图,通道之间是“大块连续”;NHWC 则把每个像素的多个通道放在一起,走完一行像素就同时处理了这一行所有通道的数据。

两种布局对数学结果没有影响,但硬件访存习惯差别很大。传统 CPU 的 SIMD 指令喜欢按通道分离、整块搬运,所以不少老式 CPU 推理库在 NCHW 上效率更高;而 NPU 的卷积阵列往往希望“一行像素连续读入”,NHWC 能让数据从内存搬到计算单元时减少跨步访问,缓存命中率更高。TensorFlow Lite 在移动端默认用 NHWC,OpenVINO 内部针对不同后端也会自动重排布局。

这里有一个很常见的坑:训练时用 PyTorch 的 NCHW,部署时预处理直接给了 HWC 数据,模型验证时精度看不出大问题,但 NPU 算子被拆成低效路径,性能直接垮掉。我接手过的项目里,因为布局不匹配导致推理时间翻倍的情况,至少遇到过三四次。请在预处理模块里先把布局固定成模型期望的格式,而不是指望推理框架每次都能帮你自动纠正。

1.3 在 C# 里手动创建一个 OpenVINO 输入张量

平时做上位机或者工业软件,经常不走 Python,而是直接在 C# 里调用端侧硬件做推理。OpenVINO 官方没有第一方 C# API,社区里常见的 OpenVinoSharp 等绑定本质上是对 OpenVINO C API 的封装,核心流程不变。概念性写法大致是这样:

// 概念性伪代码,具体方法名以所用绑定版本为准 using var core = new Core(); using var model = core.ReadModel("model.xml"); using var compiled = core.CompileModel(model, "NPU"); using var request = compiled.CreateInferRequest(); Tensor input = request.GetInputTensor(0); // 关键:先告诉运行时输入张量的真实形状 input.SetShape(new Shape(1, 3, 224, 224)); // 把预处理好的 BGR planar 数据原样交给张量 float[] data = PreprocessToPlanarBGR(rawBitmap); input.CopyFrom(data);

这段代码里有三个容易踩的细节。其一,SetShape必须在CopyFrom之前调用,否则张量内部缓冲区地址已经按旧 shape 分配,拷进去的数据量对不上。其二,CopyFrom的数据必须是一段真正连续的内存,绝对不能是 C# 里常见的二维数组,或者被字节对齐打断的托管对象。其三,如果输入张量的 dtype 是 INT8,预处理时要提前把 float 像素量化成 INT8,并且把 scale 和 zero_point 信息留给模型,这一步漏了,NPU 上跑出来的结果看着“差不多”,但数值稳定性很差。我第一次在 C# 里对接 NPU 时,就是直接从二维数组遍历拷贝,结果推理时间几乎全是拷贝时间,改成按内存块一次拷入后才把搬运开销压到毫秒级。

2. NPU 为什么能快:一次架构视角的对比

2.1 CPU 为什么跑矩阵乘法“吃力”

要理解 NPU 的价值,得先看懂 CPU 的“吃力”在哪。CPU 的核心设计目标,是处理不可预期的控制流。分支预测、乱序执行、多级缓存,都是为了在“下一条指令是什么还不确定”的情况下尽可能提升吞吐。可当你把 AI 推理变成一个个固定形状的矩阵乘法时,CPU 的通用性反而成了负担——每次乘加运算都要经过取指、译码、执行、写回的完整流水线,哪怕有 AVX-512 这类 SIMD 扩展,一次也最多处理 16 个 float 的乘加操作,相比 NPU 里成百上千个乘加单元并行工作,真不是一个量级。

有个类比我一直觉得挺贴切:CPU 是咖啡店里那个全能店员,什么订单都能接,但一次只能处理一杯;NPU 是一条只做“牛奶加咖啡”的流水线,永远在重复同一个动作,却能同时处理一百杯。通用性是用面积和能耗换来的,当任务高度确定时,专用硬件当然更有优势。所以“NPU 跑得快”并不是玄学,而是它在设计之初就把“做矩阵运算”这件事固化成了物理结构。

2.2 NPU 的乘加阵列与脉动式数据流

NPU 的核心是乘加阵列,简称 MAC Array,一块被组织成若干行乘列的乘法器和累加器阵列,通常一次性完成大规模矩阵乘加。为了喂饱这些 MAC,数据不能像 CPU 那样每次去缓存里取,而是普遍采用“脉动式”的数据流:权重驻留在片上,激活从一侧流进去,部分和在阵列内部流动,相邻单元直接用寄存器传递数据,不反复访问内存。

这种设计对卷积这类权重复用极高的算子非常友好。一个权重块被加载后,可以被同一层所有乘加单元反复使用,数据搬运量被压到最低。这正是 NPU 和 GPU 最大的不同:GPU 靠大量线程和通用寄存器文件堆带宽,NPU 靠更小的片上缓存和固定的数据流向换能效。所以评判一块端侧 NPU,不能只看峰值 TOPS,更要看定点运算的实际效果、片上内存容量、以及某类算子能否完整映射到阵列结构。市面上很多标称 45 TOPS 的 NPU,真正跑一个带大量动态 shape 和长尾算子的模型,实际能落地的算力可能只有宣传值的三四成。

2.3 不只是“小 GPU”:NPU 与 GPU 的协处理差异

很多人把 NPU 想象成一块功率更低、频率更小的 GPU,这个直觉多半来自“数据都是张量、都在做矩阵运算”,但两者的工作模式差别很大。GPU 依赖高度并行的 SIMT 模式,要先把写好的 kernel 按 warp 形式调度下去;NPU 更多是一个由编译器生成专用数据流的协处理器,算子的执行路径在编译期就基本确定,运行时调度的弹性远小于 GPU。

这对部署策略有两个直接影响。第一,NPU 对“算子集合固定、shape 静态”的工作负载非常舒服,一旦模型里有频繁变化的动态分支、字典查找、自定义字符串处理,NPU 的阵列结构往往无能为力,只能把这些算子塞回 CPU。第二,端侧 AI 项目里 NPU 很少独立扛起整个模型,绝大多数情况是 CPU 加 NPU 异构协作。哪一层放 NPU、哪一层留 CPU,不能只看“硬件快不快”,要看这一层算子能不能被完整映射成数据流。

3. 张量从相机到 NPU 的完整链路

3.1 像素不是模型的输入,张量才是

真实场景里,一个端侧检测模型的输入,通常不是算法工程师手里的 numpy 数组,而是摄像头传感器输出的 YUV 或 RAW 数据。这条链路比想象中长:传感器把光子变成电荷并转成像素,ISP 把它处理成 RGB 或 BGR,编码器压缩、解码器还原,然后才轮到推理框架把像素数据按模型要求重排成张量。

如果中间任何一步直接在内存里来回搬动而不是按需处理,比如先用 Bitmap 把图像完整拷一遍,再做色彩空间转换、缩放、转 float,那么即便 NPU 推理只需要 3ms,整条流水线的延迟也轻松超过 30ms。优化的思路不是提高某一步的速度,而是减少从像素到张量的数据改道。用零拷贝的方式让解码器输出直接落进可被 NPU 访问的缓冲区;色彩空间转换和缩放放进预处理算子,和模型推理一起编译进同一张计算图;归一化操作尽量并入模型第一层卷积或者量化 scale 里。这些手法听着基础,但“NPU 跑模型这么快,为什么实际应用还是卡”的根因,十有八九都在这段链路上。

3.2 量化:把 FP32 的舒适区搬进 INT8 的地盘

NPU 也有 FP16 或 FP32 的计算路径,但真正让它省电又省带宽的是定点计算。以 INT8 为例,一个 FP32 数从四个字节压成一个字节,模型权重体积直接变成四分之一,对应层的访存带宽需求也压缩到四分之一,这对端侧硬件是决定性的。

量化的核心是用一个 scale 加一个 zero_point 把 FP32 的数值范围映射到 INT8:q = round(r / scale) + zero_point。看起来简单,实际部署里最大的坑是量化参数怎么来的。训练后量化(PTQ)拿一批校准数据统计激活的 min/max,速度快但容易在长尾数据上掉精度;量化感知训练(QAT)把伪量化误差揉进训练过程,效果好但要改动训练管线。端侧跑大模型时还会遇到按通道量化和分组量化的问题,权重每行或者每组合一个 scale,比全局一个 scale 在低比特场景下重要得多。

实操层面,我建议所有做端侧 NPU 部署的团队,把量化误差评估固定成常规流程:准备一组真实输入,比较量化模型与 FP32 模型在相同输入上的输出偏差,而不是只看某个公开基准集。因为 NPU 的定点运算和 GPU 上的 INT8 模拟并不完全一致,有些 NPU 是真正的整数计算,有些则是 FP16 加缩放,数值行为差异会直接影响后续的误差排查方向。

3.3 为什么说“NPU 驱动”其实是一整套软件栈

很多人以为装上 NPU 驱动,硬件就算准备好了。实际上,NPU 的驱动远不是普通外设驱动那么简单,它至少包含三部分:编译器,负责把 ONNX、PyTorch 格式的计算图翻译成 NPU 可执行的指令流;运行时,负责内存分配、任务排队和同步;算子库以及内部 IR。缺任何一块,硬件都转不起来。

这也是为什么 OpenVINO 这类工具链在端侧部署里特别重要。它的价值不在于把性能优化到极致,而在于把算子选择、布局转换、量化、编译调度这些琐碎工作统一起来,让你用一套 API 在不同硬件之间切换对比。什么场景下值得绕过它直接调用厂商 SDK?当你的模型完全固定、算子全部落在厂商自研高性能内核上,并且需要做极细粒度的数据流控制时。除此之外,先用 OpenVINO 或 ONNX Runtime 这类跨平台推理框架把基准跑出来,永远是最稳妥的第一版方案。

4. 从计算图到 NPU 指令:编译与调度的底层流程

4.1 计算图优化与算子融合

模型在框架里是一张算子图,但真正喂给 NPU 的不是“按顺序执行算子”,而是一份经过编译优化的二进制数据流。这个编译过程里最重要的动作是算子融合。举个例子,一个卷积后面接 BiasAdd 再接 ReLU,在朴素执行里要三次访问内存;融合之后变成一个“卷积加偏置加激活”的大算子,中间结果可以不落回主存,直接在 NPU 片上寄存器里完成交接。

判断一个推理框架底子好不好,有个粗暴的指标:看能不能把常见的 CBR 结构,也就是卷积、偏置、ReLU 或者卷积、批归一化、激活这样的组合,融合成一个 NPU kernel。不能融合的框架,多半是靠“每个算子都调用一个通用 Kernel”硬跑,这类实现即使在高性能 CPU 上表现还行,一迁移到 NPU 就露馅。因为每个 Kernel 的启动和内存搬运动辄几十微秒,几十个算子串起来,延迟立刻恶化到不可用。

4.2 静态 Shape 与动态 Shape 的场地战

端侧 NPU 非常喜欢静态 Shape。编译期就能确定每层分配多少缓冲区、数据按什么顺序流动,编译器甚至能提前安排好整个推理期间的片上内存生命周期。只要输入尺寸固定,比如固定 224x224 检测,NPU 就能把内存规划做到很精准。

动态 Shape 则是另一回事。输入 batch 可变、序列长度可变,编译器无法在编译期一次性定死所有中间 buffer。运行时需要维护一套内存池,动态分配、复用、释放;某些 NPU 还会出现二次编译或者保守的内存预留,性能损失非常明显。端侧大模型的自回归生成就是动态 Shape 的重灾区,因为每生成一个 token,输入序列长度都在增长。

对这类场景,我见过比较实用的做法是给输入张量设置起始形状和上限形状,让编译器预分配最大缓冲,并尽量让迭代之间的数据复用。很多端侧 LLM 项目能把 prefill 速度从“不可用”拉到“能看”,关键就是这一步。

4.3 异构调度:CPU、GPU、NPU 如何分工

在端侧,AI 推理很少是单一硬件的任务。以一台带 NPU 的日常设备为例,语音命令词识别可能跑在 NPU 上,界面渲染用的 GPU 正忙于图形处理,CPU 还在处理各种系统任务。推理框架必须做异构调度:哪些算子放 NPU 数据流,哪些算子留在 CPU,哪些中间张量需要在不同处理器之间同步。

这里最容易犯的错误是过度切换。有些实现把每个算子单独指派给“最快的那块硬件”,结果每层之间都要做一次硬件间内存同步,同步开销远大于省下的计算时间。正确做法是让连续的一整块子图尽量跑在同一个硬件上,只在低频边界处做异构切换。我之前实测过一个检测模型,算子级异构调度延迟 41ms,改成以两个大子图为单位调度后延迟降到 17ms,差距全在同步和拷贝上。所以做 NPU 调优时,第一刀永远先砍跨硬件同步次数。

5. 大模型跑在端侧 NPU 上:算一笔实在的账

5.1 AMD NPU 大模型场景的现实约束

这两年身边有项目在 Ryzen AI 这类 x86 平台加 NPU 的硬件上跑大模型,先说结论:跑起来不难,跑得好很难,瓶颈大多不是 TOPS。

NPU 的算力峰值再高,它处理的是大模型里的巨型矩阵乘。一个 7B 模型在 FP16 下权重就有约 14GB,而端侧 NPU 的片上内存往往只有几十 MB,系统可用内存通常也就 16GB 到 32GB。无论 NPU 的 INT8、INT4 算力多惊人,数据都必须从 DRAM 反复搬进片上,搬运速度和位宽决定了 token 生成速度的天花板。这也是为什么你会看到一些标称 45 TOPS 的 NPU 平台跑 LLM 时只有个位数 tokens/s,而某些算力数字更低但内存带宽更大的平台,体验反而更好。

所以我做端侧大模型选型时,会把内存带宽放在比峰值 TOPS 更高的优先级,其次是低比特量化的支持程度。AMD 的 Ryzen AI 属于 x86 系统加 NPU 的方案,优势是内存和 CPU 资源充足、工具链与 OpenVINO 生态接近,劣势则是 NPU 与 CPU 共享内存带宽,大模型推理时整个系统的卡顿风险会被放大。

5.2 带宽模型:tokens/s 是怎么被算出来的

这里给一个可以直接套用的计算框架。自回归生成每输出一个 token,在理想情况下需要把模型全部权重读一遍,所以每生成一个 token 的最短时间约等于权重体积除以可用带宽,tokens/s 就等于可用带宽除以权重体积。

举个例子:部署一个 3B 模型,INT4 量化后权重约 1.5GB,NPU 推理时实际可持续带宽按 50GB/s 算,注意这是实际带宽而不是标称值,那么上限约为 33 tokens/s。如果换成 7B 模型,INT4 后约 3.5GB,同样带宽下就只有约 14 tokens/s。这两组数字基本符合我在真实设备上测到的量级。

这说明一个很多人忽略的问题:在 NPU 上提升大模型速度,真正见效的手段是减小权重体积、提高有效带宽,以及减少每步推理的数据读取量,比如 KV Cache 复用、连续批处理、投机解码。把目标全押在“优化 kernel 让它跑更快”上,往往事倍功半,因为带宽模型摆在那里,纯计算优化只能救回一小部分。

5.3 一个可复用的端侧大模型部署技术栈模板

基于我近一年的端侧 LLM 部署经验,这套组合比较稳:推理框架优先选 OpenVINO,x86 加 NPU 都能覆盖,或者用 ONNX Runtime 配合 NPU 后端。模型格式统一转成 IR 或 ONNX,权重做 INT4 或 INT8 混合量化。预处理和后处理,也就是 tokenizer 和 sampler,留在 CPU 上。

执行层面,把 prefill 和 decode 分开调度。prefill 阶段是典型的大矩阵乘,尽量压进 NPU;decode 阶段每步只算一个 token,矩阵规模小,NPU 的大并行优势发挥不出来,根据量级可以决定留 NPU 还是切回 CPU。应用侧一定要做异步推理队列,大模型推理延迟不稳定,decode 时还会周期性停顿,如果 UI 线程直接等推理结果,用户感知到的卡顿会非常明显。把推理放后台线程,用 token stream 回调的方式把增量文本逐步推给界面,这是端侧 LLM 应用体验能不能打的关键,而不是单纯看跑分。

6. 实操经验:几个调试 NPU 推理时真正踩过的坑

6.1 Shape 不匹配,算子悄悄回退 CPU

最典型的“假 NPU”现象,就是模型在 CPU 上跑没问题,换成指明 NPU 设备后发现整体速度反而更慢,日志里没有报错,只有一条 INFO 说某个子图被映射回 CPU 执行。原因通常有两个:某一层算子不被 NPU 支持,或者输入 shape 变化幅度超出硬件预编译范围。

排查方法很简单,找到运行时打印的每层实际执行设备,凡是标记为 CPU 的算子逐个确认原因。如果是模型里用了 NPU 不支持的算子,比如某些动态循环、字符串查找,就得改写模型,把这一部分拆到 CPU 处理;如果是动态 Shape 问题,就给输入张量设置上下限,触发 NPU 的预编译路径。很多框架支持打印每层的执行设备和耗时,把这组数据拉出来扫一遍,比凭感觉猜快得多。

6.2 数据布局错了,性能直接腰斩

我接手过一个摄像头检测项目,同样的模型、同样的 NPU,改动后延迟从 22ms 降到 11ms,仅仅是把输入数据从通道交错的存储格式改成模型要求的 planar 排列,并且保证首地址 512 字节对齐。NPU 的数据搬运单元通常要求源地址和每个通道的首地址对齐,不满足时驱动会退化成逐元素拷贝,系统走的是“字节搬运”的低效路径,这部分开销远大于计算本身。

现场检查建议做这样几项:图像是否已经是模型要求的通道顺序;像素排列是交错还是平面;每一行的字节数有没有补齐;张量首地址是否满足硬件对齐要求,常见的是 32 字节、64 字节或者 512 字节。如果确认输入布局对了,再看中间 Feature Map 是否存在类似问题。这类布局问题用硬件厂商的性能剖析工具一眼就能定位,但很多团队从头到尾没拉过一遍这类报告,导致问题在项目里潜伏了很久。

6.3 性能数据该信谁:TOPS、FLOPS 还是真实帧率

最后聊一个最容易让人误判的问题:NPU 规格书上的 TOPS 到底意味着什么。TOPS 通常指理论整数乘加峰值,它成立的假设是每个乘法器每周期都在跑,同时数据永远喂得上。实际端侧 NPU 受片外带宽、片上内存容量、算子映射效率和固定流水线利用率的限制,真实吞吐往往只有峰值的 20% 到 50%。

我的建议很朴素:无论厂商标多高,都拿你待部署的真实模型实际跑一遍,测端到端延迟、吞吐、功耗和内存占用,再决定方案。某些标称 45 TOPS 的 NPU 跑当前模型只能说凑合,反而是标称 20 TOPS 但工具链成熟、算子高度匹配的硬件跑出了更好看的结果。如果一定要看指标,优先看三样:实际内存带宽,注意不是规格书理论值;NPU 支持的最低量化精度;工具链对动态 Shape 的支持程度。

做端侧 AI 这些年,我最大的体会是:再漂亮的算法、再新的模型,最后都要落到“张量在内存里怎么摆、在 NPU 里怎么流”这些非常朴素的工程问题上。很多东西不是难,而是细节太容易被忽略。如果刚开始接触 NPU 部署,建议别急着追新框架,把上面这条链路在自己手头的硬件上完整走一遍,哪怕只是跑通一个 224x224 的分类模型,也要动手看一眼每层的执行设备和耗时分布。踩过这些坑之后,你会发现端侧 AI 的“快”是有迹可循的,大部分性能问题也都能从底层逻辑里找到答案。

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

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

立即咨询