这些年我一直在捣鼓端侧AI部署从手机上的小模型到笔记本里的大模型绕来绕去总逃不开三个词张量、NPU、底层执行逻辑。很多人一上来就急着调API、跑benchmark结果模型一上NPU就崩要么算子不支持要么内存爆掉要么速度慢得还不如CPU。问题出在哪在我看来就是没搞懂张量是怎么一路流到NPU里的。这篇我就把这套底层链路拆开讲清楚从张量的内存布局到NPU的MAC阵列再到编译器怎么把一个PyTorch模型变成NPU能跑的指令最后落到AMD、Intel、高通这些平台上的实操细节和踩坑记录。适合正在做端侧AI硬件部署、想搞明白NPU推理原理、或者刚接触异构计算的同学参考保证看完能少走几个月弯路。1. 从张量说起端侧AI的数据语言1.1 张量不是玄学是数据排队的规则我第一次接触张量这个概念时也被吓到了以为是什么高深的数学对象。后来给朋友打比方才说明白张量就是一块有形状的数据容器标量是零维、向量是一维、矩阵是二维三维以上就统称张量。一张224x224的RGB图片在PyTorch里就是一个形状为 [1, 3, 224, 224] 的四维张量分别对应batch、通道、高度、宽度。文本经过tokenizer之后是 [1, sequence_len] 的二维张量LLM的KV Cache是一堆形状各异的高维张量。但真正要命的不是张量有几个维度而是这些维度在内存里怎么排。同一个 [1, 3, 224, 224] 的张量按NCHW排和按NHWC排访问顺序完全不同。NCHW意思是通道维在第二个位置、接着是高度和宽度NHWC则是把通道维挪到最末。别小看这个顺序它决定了数据从内存搬运到计算单元时的连续性。NPU这类专用芯片对连续内存访问极其敏感你按错误的布局喂数据哪怕模型是对的性能也能差好几倍。纯推理框架如TensorRT、OpenVINO内部几乎都默认NHWC布局因为它更适配卷积算子的滑动窗口访问模式。而PyTorch训练时默认NCHW。这意味着你在做端侧部署时如果直接把训练权重导出来跑而不做布局转换NPU的访存会被打成碎片MAC阵列空转等待数据NPU利用率能掉到惨不忍睹的程度。1.2 张量在端侧AI中的流转路径从张量的视角看一次端侧推理大致走这么几步第一步数据从摄像头、麦克风、传感器或者文件读进来预处理成模型要求的张量形状和数值范围。比如图像要归一化到 [0, 1] 或 [-1, 1]文本要映射成token id序列。这一步看起来简单但预处理算子到底跑在CPU还是NPU上直接影响整体时延。第二步张量被送进模型编译器编译器做算子融合、内存规划、指令调度把框架无关的计算图转成NPU的指令流。这里有个关键点模型里不是所有算子都能在NPU上跑比如部分动态shape的算子、某些自定义op编译器会把计算图切分成NPU子图和CPU子图中间通过桥接层交换张量数据。第三步NPU从DDR里批量读取输入张量和权重经过MAC阵列完成卷积、矩阵乘等核心运算中间结果缓存在片上SRAM或统一缓冲区里逐层往下推最终把输出张量写回DDR。这套流程里张量就像流水线上的工件框不匹配、布局不对、中间量太大任何一个环节卡住都会导致整体停机。理解这条路径是后面所有调优的前提。1.3 为什么说搞懂张量才能懂NPU很多人觉得我不需要懂张量也能调NPU反正有编译器兜底。但实践告诉我不掌握张量的形状推断、内存布局、对齐要求你连报错都看不懂。比如NPU跑LLM时经常报 shape mismatch深层原因通常是动态序列长度导致KV Cache的形状在运行时变化而NPU编译器在静态编译阶段已经把内存地址规划死了动态shape一进来就崩。又比如NPU的输出张量常要求16字节或64字节对齐你分配缓冲的时候少算了一个paddiding推理结果就会周期性地出现错乱。组里新同学最容易犯的错就是混淆显式张量与隐式张量。端侧推理引擎里很多算子接收的不是CPU指针而是一个带内存描述符的Buffer比如OpenVINO的Tensor、ONNX Runtime的OrtValue。这些对象封装了张量的形状、布局、数据类型和内存所有权。如果你直接去拿原始指针做数据搬运等于跳过了引擎的内存管理后面翻车纯属自找。2. NPU的硬件设计哲学为张量运算而生2.1 CPU、GPU、NPU的分工与边界端侧芯片为什么会同时塞CPU、GPU、NPU这三类计算单元因为它们解决的是完全不同的邻居问题。CPU擅长分支跳转、乱序执行、跑操作系统和业务逻辑但算力密度太低。GPU靠几千个小型核心做并行吞吐适合图形渲染和通用并行计算但功耗开销大而且它对矩阵运算的支持需要通过SIMT方式模拟。NPU则把计算、存储、控制高度绑定只做AI推理里那些高频低复杂度的工作矩阵乘、卷积、激活、池化。你可以这么理解CPU是个全科医生什么病都能看但效率一般GPU是个急诊团队一次性处理大量简单的并行任务资源消耗大NPU是个专科流水线只做某个手术做得又快又省电。端侧推理为什么必须用NPU因为手机、笔记本、车载盒子这些场景里功耗和温控是硬约束。一个Llama-3-8B的模型在纯CPU上跑吞吐可能只有几个token每秒芯片还发烫NPU加持后同样的设备能把功耗压低到一个级别这就是AMD、高通、Intel都拼命在芯片里集成NPU的原因。AMD Ryzen AI系列就是这个思路的典型代表。2.2 NPU的加速核心MAC阵列、片上缓存、数据流NPU内部结构虽然各家有差异但核心骨架基本一致。第一个关键是MAC阵列也就是乘累加单元阵列。卷积也好、矩阵乘也好本质都是乘加运算的堆叠所以NPU会做一个大规模的乘累加阵列宽度从几十到几千不等每个时钟周期就能完成大量乘加操作。第二个关键是片上缓存和统一缓冲区。数据从DDR到MAC阵列的搬运成本远高于计算本身所以NPU设计了很深的存储层次。比如很多NPU会把输入激活activation、中间结果、输出累计值全部放在片上SRAM里让数据尽量在片内流转减少DDR访问。做LLM推理时KV Cache的量非常大能不能命中片上缓存很大程度上决定了你能跑多大的上下文。第三个关键是数据流调度。NPU的MAC阵列不能像GPU那样随意取数它需要编译器提前安排好数据从哪搬、搬到哪、什么时刻开始计算。每个时钟周期都有严格的依赖关系约束编译器不排明白硬件就空转。这也是为什么NPU对静态shape、固定数据流特别友好而动态shape会成为灾难。2.3 异构调度CPU、GPU、NPU怎么配合干活一次完整的端侧推理很少只跑NPU。预处理、后处理、动态逻辑一般放在CPU上做部分算子CPU更稳定。所以现代端侧推理框架都做异构调度。以OpenVINO为例它支持通过配置device参数把不同算子分配到不同设备。比如图像预处理解码放到CPU卷积归卷积网络放到NPU最后的softmax拉回CPU跑保证数值稳定性。AMD的Ryzen AI平台里模型也经常是部分跑到NPU部分留在CPU通过共享内存交换中间张量。异构调度的核心原则是尽量减少设备间张量拷贝。跨设备搬数据一次内存带宽就消失一次。正确的姿势是让中间张量尽量留在同一个设备上或者把设备间通信做成异步流水。实际工程里我更喜欢把IO算子放在CPU计算密集算子全部丢给NPU然后手动设计一段pipeline让CPU在NPU计算的同时做下一帧的预处理。3. 从张量到NPU的完整执行链路3.1 模型编译从框架图到NPU指令拿到一个PyTorch或ONNX模型NPU并不能直接执行。它需要经过编译器做前端解析、图优化、算子选取、内存分配、指令发射这一整套流程有点像把高级语言翻译成机器码。前端解析阶段编译器把模型计算图展开成中间表示IR每个节点都是算子每条边都是张量同时记录每个张量的形状、数据类型、所属设备。图优化阶段做算子融合算子融合和常量折叠比如把ConvBNReLU融合成一个NPU自定义算子把固定形状的TransposeReshape提前算好。算子选取阶段是硬骨头编译器要遍历NPU的算子库逐节点匹配硬件实现。匹配不上的算子会被标记成fallback送到CPU执行。这就会产生子图切分NPU子图和CPU子图之间需要插入数据转换算子包括布局转换、数据类型转换和内存拷贝。内存分配阶段编译器开始规划每个中间张量在DDR和片上缓存里的位置。它要做穿针引线找出生命周期不相交的张量把它们重叠存放在同一块物理内存里降低峰值内存。这部分优化做得好不好看profile里的memory footprint就能看出来有些框架能帮你省掉30%以上的内存。3.2 算子映射与图优化能省则省能融则融算子映射的质量基本决定了NPU跑得快不快。以AMD Ryzen AI背后的Vitis AI EP和Intel的OpenVINO NPU插件为例它们都维护了一个庞大的算子库把ONNX或PyTorch的算子映射到NPU原语上。常见优化有这么几类算子融合把连续的小算子合并成一个大算子减少中间张量的落地。比如ConvBiasReLU融合一次扫过MAC阵列就出结果。权重重排训练好的权重按NPU需要的布局重新排列比如把权重从[O,I,KH,KW]变成[O,KH,KW,I]让MAC阵列取数时能一次拉一整条连续数据。数据布局统一整图统一成NHWC或自定义的NPU最优布局尽量避免每个算子之间反复做layout转换。稀疏化剪枝把权重里接近零的值剪掉用稀疏存储格式节省带宽。这个在端侧模型量化时常见但依赖芯片对稀疏计算的支持。实际工程里最讨厌的算子包括动态Shape的 Reshape/Gather、复杂的注意力掩码计算、控制流如循环和条件分支。这些算子要么让编译器静态规划报废要么必须回退到CPU。设计端侧模型时尽量把所有shape固定下来让编译器能把整张计算图压平成静态数据流。3.3 数据布局与内存管理最容易踩坑的隐形杀手这一节我必须重点展开因为太多人栽在数据布局上。先说一个具体案例。之前在做AMD平台的大模型部署时同样的Qwen2-1.5B模型我一开始直接用PyTorch导出ONNX权重形状是原始的[hidden_dim, vocab_size]FP32。上NPU之后速度稳定在每秒4个token明显不对。后来用profiler一看NPU的MAC阵列利用率只有12%大部分时间都在等数据。原因就是权重布局和NPU期望的[N, K]布局不一致导致每次矩阵乘都要先做一次转置拷贝。解决方案很简单在编译阶段用工具把权重就地重排成NPU友好的布局比如预先完成transpose并落盘。同时把输入从FP32转成FP16这样访存带宽压力直接减半。改完之后同样的模型跑到每秒22个tokenMAC利用率跳到70%以上。内存管理上要注意点包括所有张量缓冲需要按NPU的对齐要求申请内存常见是64字节。不对齐会导致部分NPU报错或者性能降低。避免推理过程中频繁申请释放内存。正确做法是预分配一个足够大的内存池所有中间张量在启动时申请推理时复用。不同子图之间的输出张量如果只被CPU使用一次可以考虑直接在CPU侧复用它减少一次完整拷贝。还有一个我以前忽略的细节端侧NPU常常不支持零拷贝的CPU张量地址引擎会在上下边界做staging copy。如果你在CPU端做后处理需要频繁读取NPU输出就应该把这个输出单独标记为可映射的shared buffer而不要在每次推断时新建OrtValue或者Tensor对象。3.4 一次完整的推理旅程从请求到token把上面所有概念串起来模拟一次LLM端侧推理的完整流程用户在笔记本上输入一句话框架拿到prompt在CPU上做tokenize得到tokened ids。这个张量被送到编译器预处理转换成模型输入的固定形状和数据类型。NPU执行prefill阶段提示语解码输入token全部作为一批和权重矩阵做一次大的矩阵乘生成第一个token输出同时把每层的KV Cache写入预分配的内存区域。进入decode阶段每轮只处理一个token。NPU读取上一步生成的token对应的embedding结合KV Cache做自注意力计算得到下一个token概率分布。这个时候张量形状是不变的但是KV Cache的长度在变所以编译方案上KV Cache内存是静态预留的最多能支持多大的上下文在compile时就确定了。核心运算完成之后CPU参加采样比如top-k、top-p选出实际输出的token id再拼接到输入序列中进入下一轮decode。如此循环直到输出EOS结束。全链路里张量在每个环节都有明确形状、明确内存位置、明确设备归属。这种确定性才是端侧AI能稳定推理的根本原因。4. 端侧AI硬件部署的实操要点4.1 工具链选型对比同一模型不同平台现在主流端侧NPU平台就那几家选型时核心是看算子覆盖、编译成熟度和生态文档。AMD阵营推荐Ryzen AI平台底层是Vitis AI EP支持ONNX Runtime还提供一个Python封装RyzenAI。它承接了PyTorch模型的路径能自动生成NPU版本。AMD NPU跑大模型的优势是内存带宽大、集成度高适合在笔记本上跑7B级别的量化模型。Intel阵营核心是OpenVINO。Intel给NPU做的插件已经非常成熟对常见CV模型、LLM、Diffusion模型都有现成的优化路径。最近很火的ComfyUI调用Intel NPU就是通过OpenVINO后端实现SD系列模型在核显NPU上推理。实际体验下来Intel NPU跑Stable Diffusion的UNet是非常划算的因为卷积结构正是它的强项。高通阵营用Qualcomm AI Engine DirectQNN主流于手机和开发板平台。它有很强的量化工具链端侧LLM场景优化明显但文档相对分散。苹果阵营Core ML ANE闭源但优化到位。如果你是苹果生态直接用coremltools把模型转成.mlpackage部署成本最低。这些工具链选型的共同经验是先跑通一个最简模型工程上叫最小可复现用例再上目标大模型。直接拿一个7B模型怼工具链报错铺天盖地你根本分不清是权重损坏、布局错误还是算子缺失。4.2 量化与精度权衡端侧内存压力的解药端侧设备内存有限7B模型按FP16算光权重就是14GB直接爆掉绝大多数端侧设备。要跑起来必须量化。目前主流的选择是INT8和INT4。INT8基本损失很小部署工具链支持普遍较好能省下一半内存带宽推理速度提升明显。INT4能进一步压到四分之一但存在精度回退风险在数学推理、代码任务上尤其要注意。实际操作中我会建议按这样的优先级来干先用INT8量化跑通得到baseline速度和精度如果内存紧张再单独把注意力层的KV Cache做量化。实在要INT4优先做权重分组量化比如group size128每组权重单独算scale和zero point精度损失会比全局量化小很多。这里有个容易踩的坑量化之后一定要在端侧CPU上跑一次全量精度验证而不是只在量化工具里做simulated quantization。很多时候工具里看起来精度不错一上NPU算子实现差异和中间张量的低比特存储同时作用模型就胡言乱语了。4.3 性能调优的五个实战技巧第一切子图要精准别让NPU跑单算子。NPU最怕的是每两个权重算子之间夹一个CPU算子导致计算来回切换。尽量让NPU子图覆盖连续的大块计算。第二控制batch size。端侧LLM推理时prefill阶段可以适当大batchdecode阶段通常batch1别试图用大batch塞满全部设备否则KV Cache内存会瞬间爆炸。第三留意CPU与NPU的流水重叠。CPU做采样和后处理的时候NPU在算下一层这套流水线如果设计得好整体吞吐能提升百分之几十。我习惯把预处理、采样、tokenize全部封装成独立线程通过队列跟NPU的解码线程解耦。第四尽量缩小中间张量的dtype。比如注意力score矩阵FP32和FP16差异巨大但很多模型推理时精度都够。端侧部署优先FP16能半精度就别全精度。第五善用profiler定位瓶颈。不要瞎猜性能问题在哪直接把OpenVINO的profiling dump拉出来看或者用perfetto看CPU和NPU的流水线事件。我之前遇到过NPU利用率低的问题排查半天发现瓶颈根本不在NPU而在CPU采样线程没跟上属于调度失衡。5. 常见问题与排查技巧实录5.1 算子不支持、循环崩溃换个写法立刻通端侧NPU算子库不可能覆盖所有PyTorch算子这是最常见的翻车点。有一次我把一个包含torch.topk的模型转OpenVINO编译报错算子不支持。后来一查topk要按序列长度做动态选择NPU静态编译根本没法规划内存。解决办法是把静态shape版本的计算逻辑手工改写。比如把topk改成在固定维度上做slicemask或者把采样逻辑挪到CPU上NPU只负责输出最后一个token的logits。改完之后模型转换一次通过。给个小提示遇到算子不支持时第一步不是写复杂逻辑而是看看能不能用几个基础算子组合替代。比如动态MaskedSoftmax可以用两个Slice加一个固定长度的加法和Softmax实现。只要张量形状在编译阶段是确定的NPU基本都能吃下。5.2 内存爆掉PV Cache和内存池是最主要的控制点端侧跑大模型最常见的报错是OOM也就是内存溢出。原因多半集中在两处。第一是KV Cache预留过大你在配置里把max_context_length设得特别高编译时NPU就会为每层预留足够的缓存可能超过设备物理内存。解决办法是把max_context_length设成实际会用到的值比如8K别一上来就32K。如果中途要换更大的上下文重新编译一次就好。第二个原因是中间张量内存池设计太粗糙。编译器默认给每个中间张量分配独立缓冲区内存开销很大。实际框架里往往提供内存复用机制比如静态内存图和动态内存池。在OpenVINO里可以设置缓存目录和使用静态内存块分配减少推理期间的动态内存请求。同名张量内存复用之后峰值内存直接下降。5.3 NPU利用率上不去八成是被数据搬运卡住了很多同学看到NPU利用率30%以下就以为是算子实现不行其实更多是数据搬运瓶颈。有一次我帮人优化SD模型的NPU推理发现UNet的推理时间主要花在等权重加载上。模型太大DDR带宽跟不上MAC阵列吃的速度NPU算一波停一波。后来把权重预加载到片外内存并开启了bloack缓存性能立刻上去。还有一个经典问题CPU和NPU共享同一块DDR带宽CPU侧做图像解码和预处理会占用大量带宽导致NPU取权重时被卡。优化办法是错峰CPU预处理提前做好或者在NPU计算的同时让CPU做下一帧不要把两边带宽需求同时顶到最高。5.4 我的排查顺序建议如果端侧推理出问题我一般按这个顺序排查先看编译日志确认算子落点、子图切分。再看输入张量的形状、dtype、布局是否满足引擎要求。接着看内存分配和模型峰值占用排除OOM隐患。然后用profiler看整条pipeline的时间线定位是NPU计算慢、CPU慢还是设备间拷贝慢。最后才考虑改模型结构或换量化方案。顺序别反过来不然很容易出现改了半天模型最后发现是数据布局错误的情况。6. 一点个人经验折腾端侧NPU这几年我最大的体会是硬件只是载体张量和数据流才是主角。所谓底层执行逻辑本质就是搞清楚数据以什么形状、什么布局、什么精度、什么时间点流经哪一块计算单元。把这些细节都理顺了NPU的潜力自然就释放出来。最后再分享一个小技巧如果刚开始接触某个平台的NPU别一上来就折腾复杂模型先在文档里找它的官方示例用最简单甚至有点傻的模型比如单一卷积网络跑通全链路。然后逐步加结构复杂度每加一层就跑一次profiling。这套办法看着笨却是理解张量到NPU这条路最稳的走法。