
张量这个词这几年被大模型热潮带出了圈但真正跟它朝夕相处的是端侧AI开发的同行们。做应用的人问得最多的一句话是“我把模型丢到手机或者开发板上输入一张图片它到底是怎么跑起来的”想回答这个问题就得把从张量到NPU这段路从头到尾走一遍。先搞懂数据在内存里长什么样再理解CPU、GPU、NPU之间到底谁更适合干这活然后看清推理引擎怎么把模型变成NPU能执行的指令最后落到部署时的工程细节上。这篇文章就是围绕这条链路写的适合准备做端侧模型部署的工程师、硬件加速爱好者以及想弄明白“端侧AI硬件部署”到底在做什么的人。1. 张量先把“数据长什么样”这件事说透1.1 张量不过是一个“带形状的盒子”在数学定义里张量是标量、向量、矩阵的推广但在端侧AI的世界里我更愿意把它理解成一个“带形状、带类型、带排列规则的内存盒子”。比如一张224x224的RGB图片在PyTorch里经常被表示成shape为[1, 3, 224, 224]的浮点张量1是batch size3是通道数224是宽和高。这个四维数组在内存里其实是一段连续排列的字节GPU或NPU之所以能“看到”这张图靠的是编译器把shape、strides每个维度跳多少字节、dtype每个元素占几个字节这三样东西解释清楚而不是像我们在脑海里想象一个三维立方体。这里提到的strides可能是很多人第一次接触端侧优化时栽跟头的地方。同一个张量按行优先还是按列优先存储访问效率完全不同。更麻烦的是不同框架对同一批数据的排列方式经常不一致。做端侧AI时你从ONNX里导出的是一个NCHW的模型推理引擎底层驱动的输入可能要求NHWC排布中间就多了一次转置操作。在PC上这次转置可能只损失几毫秒但在内存带宽紧巴巴的手机NPU上一次无谓的转置就是几十上百MB的流水线搬运直接吃掉整个推理预算。所以处理端侧数据的第一步不是调模型而是在把数据喂进去之前就保证它的layout是硬件喜欢的那一种。1.2 布局转换是看不见的“搬运工”NHWC与NCHWNCHW和NHWC的争论不只是学术问题它会真实影响NPU的利用率。NCHW把同一通道的像素连续存放对卷积运算来说访存友好NHWC把同一位置的多个通道挨在一起对某些向量化指令和DMA搬运友好。不少移动端推理引擎默认输入就是NHWC因为摄像头和图片解码出来的数据往往就是这个顺序而很多训练框架导出的模型是NCHW一进一出之间就引入了一次代价不小的内存重排。我见过不少新人在集成SDK的时候卡在“为什么模型跑出来的结果全是噪点”这个问题上整整一天最后发现是输入张量排布错了——不是精度问题也不是模型问题而是数据被设备解释错了。更微妙的是batch这一维。端侧推理大部分时间都是batch1但NPU为了榨干算力往往要把几个小请求拼成一个batch一起送进去。这个“batch维度要不要可变”的问题听着简单真正落到工程上却会牵扯出一整套动态shape的麻烦模型转换的时候你必须告诉编译器这个维度是固定的还是可变的。固定了会损失业务灵活性可变了则可能让部分NPU放弃静态编译直接走性能较差的解释执行路径。所以我的经验是如果能接受尽量把batch、分辨率这些动态维度在业务层写死换取NPU最高效的静态执行路径如果业务确实需要动态输入再考虑用padding把尺寸对齐到固定值。这个选择很多时候比后面调算子的收益来得更直接。2. CPU为什么“算得动却扛不起”端侧AI需要专属硬件的理由2.1 CPU的伤疤每个人都必须走完一条流水线CPU是一台非常全能的机器但全能是有代价的。每一次计算CPU都要取指令、译码、访问寄存器、执行运算、把结果写回为了应对分支跳转还要做预测和流水线冲刷为了把多条指令的依赖关系理顺还要做乱序执行的调度。这套通用设计换来的是“什么程序都能跑”但也意味着每一块晶体管和每一瓦功耗里真正用于“乘加运算”的比例其实不高。让CPU去跑一个大矩阵乘法效率很难比得上用一块专用电路连续吃进成百上千次乘加。类比来说CPU像一位什么都会的全能手艺人而AI推理是一道每天要重复几万次的流水线工序。手艺人在单件精度上绝对厉害但论“每分钟稳定生产相同零件”专用产线可以把成本压低一个量级。端侧AI恰恰需要的是后者模型固定、算子固定、输入尺寸可以变得更固定这时候把整个计算图压到一条为矩阵乘而生的硬件流水线上才是真正的正解。2.2 AI的低层算力单元是乘加MAC不是通用算术深度学习里的绝大部分运算最终都可以归约成乘加MACMultiply-Accumulate。卷积是对一个小窗口做加权求和全连接是向量点积Transformer里的QK^T和PV也都是矩阵乘。一个MAC在芯片里做的事情不过是“把两个数乘起来再加到累加器里”。听起来简单但一个大模型动辄几十亿参数每一层都在疯狂重复这个动作于是芯片的关键指标就变成了“每秒能做多少次乘加”也就是TOPS。GPU走的是“大量并行ALU加高速显存”的路线适合通用并行计算但功耗和体积都偏大NPU则干脆把一整块硅片留给乘加阵列让权重尽量常驻在片上SRAM让激活按固定节奏流过。用行话叫数据流/脉动阵列用大白话讲就是“人不动料在流水线上动”。这一点决定了NPU在端侧真正有价值的优势不是绝对算力而是把数据搬运次数减到最少之后换来的能效比。你去看各家端侧NPU的宣传很少只吹TOPS都是拿“每瓦TOPS”说事原因就在这里。2.3 功耗墙手机电池撑不起一台缩小的服务器为什么不直接在手机上放一颗降频GPU做推理或者干脆把所有计算都发包到云端除了时延和网络连接的问题最关键的限制是功耗和散热。手机整机连续高负载的功率预算通常在个位数瓦特能分给NPU的只有零点几瓦到两三瓦一颗云端训练卡的峰值功耗能上几百瓦等比缩小到手机里热设计和电池会先崩溃。NPU的存在价值就是用尽可能少的焦耳完成一次推理。举个常见的例子。我拿一个移动端分类模型做过对比CPU线程全开跑一次延迟四十毫秒左右但整机功耗能冲到四瓦以上连续跑两三分钟机身就开始发热同样的输入切到一块能效不错的NPU上延迟压到八毫秒左右功耗还不到一瓦。这个差距在云上无所谓在端侧就是“能用一天”和“撑不过半天”的区别。理解了这条功耗墙你才会明白为什么“端侧AI硬件部署”相关的话题里永远绕不开“能效比”而不是单纯比拼峰值算力。3. 中间1000米的“惊险一跃”推理引擎如何把张量变成NPU指令3.1 第一步图优化与算子融合先把模型“瘦身”模型训练时留下的计算图直接搬上NPU往往效率很差因为训练图和推理图关心的根本不是一回事训练要保留梯度推理不需要训练允许逐算子动态调度推理希望整张图尽量静态化。所以任何端侧推理引擎不管是ONNX Runtime、TFLite、NCNN还是各家自研运行时拿到模型之后的第一件事都是先做一次图优化。图优化里最经典的技法是算子融合。以ConvBNReLU为例BN在推理阶段就是一组线性的scale和shift运算完全可以把scale乘进卷积权重、把shift加进卷积偏置然后把ReLU原样贴在卷积输出端。融合前是三次内存读写的串行过程融合后变成“读一次输入、算一次、写一次输出”。这一步在CPU和GPU上都有收益在NPU上的收益尤其明显因为NPU最怕的就是在片上SRAM和系统内存之间反复倒腾数据。类似的融合还有多头注意力里的QKV合并计算、残差加法和LayerNorm的合并等等思路都是同一个减少中间张量的落盘。3.2 第二步算子择路——哪些上NPU哪些回CPU融合之后引擎拿到了一张相对干净的静态计算图。接下来它要做一件很现实的事把图里的每一个算子跟当前NPU的算子支持清单逐一比对。每个NPU都有自己最擅长的那几张“牌”有的对卷积和池化优化得极好有的对矩阵乘和Attention特别上心但总有不支持的算子比如某些动态shape的Gather、非标准激活函数或者还没来得及适配的新算子。遇到这种情况引擎不会直接报错而是做分区partition支持的算子走NPU不支持的算子留CPU中间自动插入数据往返的桥接。很多人第一次看引擎日志发现NPU利用率只有百分之三五十就以为是芯片性能不行。实际上多数情况是算子覆盖度不够大量子图回退到了CPU兜底。由于NPU和CPU之间的数据复制要走系统内存每兜底一个算子往往要付出“结果搬出去、再搬回来”的来回成本整体延迟可能不降反升。所以真正决定端侧推理快慢的不是纸面TOPS而是“算子覆盖率乘每个算子的执行效率”这两个数的乘积。选模型、改模型结构时优先避开那些不支持的热点算子比什么都重要。3.3 第三步DMA搬运、片上计算、写回——流水线里没有闲置就算所有算子都被NPU接受了也还差最后一步怎么把计算真正安排到硬件上。端侧NPU的内部结构通常是“SRAM乘加阵列加DMA引擎加小控制器”。一次推理的微观流程大致是CPU侧或NPU控制器发起指令DMA把输入张量从系统内存搬到片上SRAM计算阵列批量做乘加结果留在SRAMDMA再把结果搬回系统内存供下一个算子读取。这里最考验工程能力的是“重叠”计算和搬运能不能同时进行。理想状态是计算阵列在算第一批数据的同时DMA已经把第二批数据搬到片上了这样计算单元永远不会空等行话叫双缓冲。NPU利用率低十次里有七八次的问题不在计算阵列本身而在数据没喂饱。这一点在做性能分析时体现得特别明显打开profiler你会看到大量时间花在等待数据和DMA传输上而不是真正在算。端侧性能优化里所谓“把张量通道理顺”本质上就是在跟这些看不见的搬运过程打交道。4. 端侧的真正敌人不是算力而是内存带宽与功耗4.1 一张带宽账本7B模型挂在手机上到底吃掉多少先算一笔账。一个大语言模型跑在端侧最占内存的是权重7B参数的FP16版本大概14GBINT8量化后约7GBINT4量化后不到4GB。手机或PC要同时跑系统、应用和推理程序内存总量是固定的能分给模型的就那么几GB。所以端侧部署LLM第一件事不是调算力而是决定“把权重压到什么精度才能塞进设备”。这也是为什么论到“端侧AI硬件部署”“量化”这个词出现的频率远高于“加速”。把权重塞下之后真正的瓶颈才会浮现出来带宽。推理时每解码一个token都需要把权重从头到尾扫一遍这本质上是一次重量级的内存读取。以7B INT4模型为例生成一个token需要读大约4GB的权重数据。就算NPU完全不需要算光是“读一遍”就会受限于内存带宽。LPDDR5内存的理论带宽上百GB/s实际可用再打个折这意味着单token的耗时被钉死在几十毫秒量级——这不是算力不够是内存搬运的上限。理解了这条约束你就明白为什么端侧大模型普遍追求“每个token少读一点数据”和“多算少搬”的设计哲学。4.2 量化不是玄学从FP32到INT8再到INT4量化的数学本质很简单把浮点数的取值范围映射到几个有限的整数步长上公式就是r scale * (q - zero_point)。但工程层面的选择非常多。权重分布比较均匀适合用对称量化不需要维护zero point硬件实现也更省激活张量的动态范围大且不稳定通常用非对称量化或者干脆把per-tensor改成per-channel、per-token让每一行、每个通道有自己的scale减少个别异常值造成的破坏。我个人的经验是量化对视觉模型往往比较宽容对LLM的累积误差要敏感得多。跑分看着都还行但换到长文本、代码生成这类任务输出质量可能悄悄劣化。稳妥的做法是拿一批校准数据做离线统计选择让分布差异最小的scale再对每个算子做在线验证。不要只盯着单个算子的精度一定要看整条链路跑完之后的输出分布变化。至于INT4本质是在INT8基础上再砍一半比特牺牲一些精度换“能不能塞进内存”这张门票。很多端侧场景里这个trade-off是值得的。4.3 大模型端侧的额外功夫KV Cache与投机采样权重只是一部分。Transformer的自回归机制还会产生KV Cache生成过程中每一步都要缓存历史token的Key和Value上下文越长这坨缓存越大到后期可能比权重还占内存。更麻烦的是KV Cache长度是动态的每生成一个token就长一点这对NPU的静态shape编译非常不友好。常见的处理思路有把KV Cache预分配成一段固定长度的连续内存超长再分片或者对KV做量化把FP16压到INT8甚至更低省出的内存换更长的上下文。有了KV Cache这把尺子你会发现端侧LLM部署里还有另一个精巧技巧投机采样。既然大模型解码的瓶颈是“每个token都要读一遍权重”那就让一个小草稿模型先在CPU或低功耗单元上快速试出几个候选token大模型只需要做一次批量验证对了就一次性接受多个token。这个技术在端侧格外有用草稿模型小、跑得快验证阶段的批量推理又正好符合NPU喜欢“大批量矩阵乘”的脾气一石二鸟。我自己在小型模型上试过解码延迟确实能明显往下掉——不是算力变强了而是把“读权重”的次数减少了。5. 各家主流NPU的架构与脾气从Hexagon到XDNA5.1 高通HexagonDSP基因的高能效路线高通Hexagon从早期的DSP一路演化而来这决定了它对低功耗、低延迟的执着。Hexagon里有一个非常有特色的“块浮点”设计与其给每个元素单独配一个指数不如把一组数据共用一个指数尾数以整数存储。这样既保留了比较大的动态范围又不用为每个数都维护一套scale和偏移折算下来就是更省带宽、更高的有效计算密度。Hexagon在高通骁龙平台和Windows on ARM上都是能力很强的硬件但工程复杂度不低。它的QNN工具链对算子覆盖有自己的一套脾气很多在GPU上能跑的模型转过去会遇到分区的坑。如果想在Hexagon上吃满性能要么用官方SDK预置的模型库要么仔细过一遍算子支持矩阵。不要指望一个通用ONNX文件直接起飞那只是少数幸运儿的剧本。5.2 Intel NPU与Apple ANEPC与手机两个生态的代表Intel NPU的技术积累源自Movidius时期那波视觉处理单元到今天酷睿Ultra内部的NPU模块主要落地场景是PC端侧AI。它最大的特点是软件栈相对集中OpenVINO把模型转换、量化、部署的链路串了起来对开发者来说基本是“我出模型框架出适配”。这也是为什么很多端侧AI硬件部署教程里Intel NPU的起步门槛看起来最低——你不需要跟寄存器打交道所有接口都封装在更高层。Apple ANE则是另一个路线的代表深度绑定Core ML生态开发者看到的不是“NPU调度”而是系统自动决定哪一部分上ANE、哪一部分留在CPU或GPU。上手体验最平滑但也意味着你能做的精细控制最少。它的优势在于生态一致模型从PyTorch或ONNX转过来大部分算子自动映射研究和调查DMA的机会都省了。这两家放在一起正好展示了端侧NPU的两种路线给开发者更多控制的“开放路线”和让开发者少操心的“封闭路线”。5.3 AMD XDNA一张不太一样的牌AMD XDNA继承了FPGA时代的思路强调的是“流”而非“阵列”。传统NPU更看重把一大块矩阵乘尽量一次性算完XDNA的设计理念则是让数据和计算像流水一样在不同单元之间移动把数据搬回内存的次数压到最低。这种架构理论上很优雅真正落到工程上的关键是AMD给开发者的编译器能不能把一个普通ONNX模型自动映射成这种流水布局。目前来看XDNA在大模型推理场景的讨论热度明显在涨尤其是和Llama.cpp这类社区工具的结合。这背后有一个很实际的原因对“AMD NPU跑大模型”这件事来说硬件算力不是第一位的社区软件的适配速度才是。只要工具链能顺利把一个热门开源模型跑通后续的调优就只是个时间问题。这也是为什么“amd npu 大模型”最近会成为一个热搜词——它代表的不是一张芯片的胜利而是一套生态开始被认真使用的信号。5.4 “ComfyUI调用Intel NPU”为什么突然有热度“ComfyUI调用Intel NPU”这个热搜词背后其实是一个很现实的PC用户需求Stable Diffusion这类生成模型跑在GPU上既占显存、发热也大如果能把文本编码器、部分采样环节这些相对轻量的子任务挪到NPU上GPU只专注最重的UNet主循环整机功耗会好看很多GPU也能腾出来干别的活。ComfyUI因为节点式结构足够灵活社区才能通过OpenVINO这类后端把部分节点挂到NPU上。这事的本质不是“NPU取代GPU”而是“异构分工”谁擅长矩阵乘就让谁多算一点是否划算要看数据量、时序和带宽是否匹配。分辨率低、步数少的轻量任务NPU的能效比很亮眼一旦分辨率上去、批量变大GPU依然不可替代。这个判断标准我觉得同样适用于所有端侧NPU的选型——别问“谁算得快”要问“我的负载模型里数据搬运和计算的比例处在哪条线上”。6. 从跑通到跑快端侧AI部署的工程经验清单6.1 先跑CPU再上NPU建立精度和延迟基线做端侧AI部署我见过最多的错误是“一步登天”模型直接从仓库里拿出来量化、转换、丢给NPU然后对着莫名其妙的输出开始怀疑人生。正确的姿势是先花半小时把同一模型在CPU上完整跑通记录精度和延迟作为基线。这个基线能帮你把问题隔离成两类是模型或数据本身的问题还是NPU适配的问题。之后再切到NPU对比端到端精度、算子级耗时和内存峰值问题出在哪一层就一目了然。我自己做过一次印象很深的部署转换后输出和CPU基线差距大得离谱。第一反应是量化误差排查到半夜才发现输入像素忘了做数值归一化数据范围整体偏移——跟NPU一点关系都没有。这种坑虽然蠢但在端侧开发里极其常见。所以“先建基线”这一步绝对不能跳。6.2 用Profiler找出真正的瓶颈搬运时间比计算更显眼端侧推理的性能分析建议把时间分成三段来测模型加载时间、第一次推理的冷启动时间、稳态推理时间。很多人在稳态推理的算子耗时里抠了半天却没意识到冷启动可能才是用户感知的大头。模型加载涉及权重反序列化、布局转换甚至一次完整的编译器准备动作动辄几百毫秒到几秒如果应用每次启动都来一遍体验一定很糟。解决办法是让推理线程常驻、模型常驻用一次预热请求把编译和加载动作提前消耗掉。到了稳态阶段打开profiler先看“读写数据”的耗时再看“计算”的耗时。很多NPU性能报告会分别列出compute time和memory time我经常发现memory time比compute time长得多。这种情况下再怎么优化算子实现都没用正确的做法是调整数据布局、减少中间张量、把多个小算子融合成一个。还有一点特别容易漏NPU请求发起时如果驱动在CPU上做了同步等待CPU线程就会被卡住连带整个应用掉帧。所以异步推理接口能开就开别让控制流的等待白白浪费掉NPU省下来的时间。6.3 量化输出劣化后的排查顺序从输入范围到混合精度量化模型跑起来发现输出质量劣化了先别急着把模型退回FP32。按下面这个顺序排查通常能快速定位问题。第一步检查输入和预处理是否一致确认不是数据问题第二步比较量化模型和原始模型的逐层输出找到第一个误差明显被放大的算子第三步观察这个算子的输入张量分布如果存在明显的离群值说明量化参数校准得不好换一组更贴近真实场景的校准数据重新统计第四步如果仍然劣化把这个算子单独保留FP16或FP32其它继续量化。这个“混合精度”的思路比一刀切回到全精度实用得多。我遇到过最经典的翻车案例出在LayerNorm和Softmax这两个算子上。它们的输入动态范围很大直接量化容易崩但计算量占比相对小保留FP16几乎不影响整体加速收益。所以定制端侧量化策略时我不会把所有算子都压到一个精度而是先跑一轮逐算子精度对比哪些对量化敏感就放行到高精度哪些不敏感就继续低比特。效果往往比盲目追求“全INT8”好上一截。6.4 那些容易被忽略的“小事”决定体验上限最后说几件容易忽略的小事。第一件推理引擎输出的张量布局和模型输入一样有讲究模型输出经常是[N, C, H, W]应用层要的往往是[H, W, C]如果直接在应用里逐像素访问性能会很难看正确做法是在引擎里配置输出布局或者在一次内存拷贝中完成转换。第二件端侧设备的电源状态会直接改变NPU频率。低电量模式、过热降频都会让标称性能报表变得虚假。做性能对比时务必统一设备电源策略和温度状态否则你测出来的差异可能根本不是软件优化带来的。第三件驱动和推理引擎的版本决定算子覆盖率的上限。同一块硬件驱动版本不同、引擎版本不同性能和可用算子集合可能天差地别。升级前一定看release note升完马上重跑一遍“小模型冒烟测试”。这三件事没有一件需要高深理论但每一个都实打实影响过我项目的结果。踩过之后你会发现端侧AI优化的前百分之九十是具体而琐碎的工程活乐趣都在最后那百分之十的瓶颈定位和一击命中里。写到这儿我特别想分享一个自己踩过的大坑。有一回我拿着芯片厂商的demo模型在NPU上跑性能报表漂亮得像广告一样后来换成我业务里的真实模型数字立刻缩水一半以上。查了一整天才明白demo模型被厂商削掉了所有不支持的算子而我的模型里有几个动态shape操作让引擎把大半个计算图都回退到了CPU。那种“租了一条高速路、出口却被封掉”的感觉做过端侧AI的人应该都能共鸣。所以现在我的习惯非常简单任何模型上NPU之前先看一眼编译器日志里哪些算子真正分配到了NPU确认了执行明细再谈优化。最后再分享一个保命技巧——永远在项目里保留一个极小的基准模型比如一个两三层的小卷积网络专门用来在引擎或驱动升级之后做冒烟验证。它帮我抓出过至少三次驱动升级引入的回归问题。成本几乎为零回报却非常稳定。