ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从张量到NPU:端侧AI推理的底层执行逻辑与工程实践

从张量到NPU:端侧AI推理的底层执行逻辑与工程实践 我最早意识到“同一段AI代码换了个硬件就面目全非”这件事是在一台带NPU的轻薄本上跑ComfyUI。CPU模式下一张512x512的图十分钟还没出切换到NPU我以为会起飞结果直接报算子不支持软件秒退。那时候我连NPU到底在干什么都说不清楚只知道网上都在喊“端侧AI”“张量”“NPU”但没人把这三件事真正串起来讲明白。这篇文章就想把这条链路从头梳理一遍数据进了AI模型之后怎么一步步变成张量张量又怎么落到NPU这样的专用硬件上执行。适合正在做端侧AI推理部署的人、被厂商SDK折磨的算法工程师以及那些好奇“为什么手机跑AI这么快、PC跑AI反而不争气”的开发者。搞懂底层执行逻辑之后你再去看厂商的文档和算子库思路会完全不一样。1. 张量AI计算的最小商品单元很多人听到“张量”这个词就头大觉得是数学里多深奥的东西。实际上你可以把它理解成一种装了数字的、有结构的多维箱子。标量是一个数向量是一排数矩阵是一张表张量就是这些概念的推广——几排几列只是它的某种特殊情况。1.1 为什么AI里的所有数据最终都变成张量AI模型本身不关心你输入的是图片、音频还是文字因为它只认数字而且必须是排好队的数字。图片在进入神经网络之前会被拆成“高x宽x通道”的三维张量比如一张RGB图就是 [224, 224, 3]一句话会被分词后映射成token id再转成 [序列长度, 隐藏维度] 的二维张量在Transformer里不同头的Q、K、V运算本质上是把三维张量做拆分和重组。所以整条推理链路无论模型多花哨最终都能收敛成一句话一堆张量经过一个又一个算子变成另一堆张量。这个视角非常重要。它意味着AI计算的“最小商品单元”不是某个函数而是张量。卷积、注意力、归一化、激活全部是作用在张量上的变换。不同AI框架之间的差异说到底就是张量怎么定义、怎么存储、怎么调度执行。1.2 张量形状为什么直接影响性能张量的“形状”(shape)不仅是语义层面的信息对硬件来说它直接决定内存布局和访问模式。同样是十万个浮点数排成 [1000, 100] 和排成 [10, 10000] 的存储是连续的但切片、转置、广播的代价完全不一样。很多推理引擎里有一个专门的优化叫“布局转换”就是把张量在内存里重新排列成硬件更喜欢的样子。举一个最直观的例子卷积里有一个操作叫im2col把输入张量按卷积窗口展开成一个大矩阵看起来额外占内存但它能让后续计算彻底变成矩阵乘法正好喂给为矩阵乘设计的硬件单元。这就是为什么同一模型用框架默认布局和用硬件友好的布局性能能差好几倍。端侧NPU尤其敏感因为它的缓存很小、内存带宽也有限张量排得越整齐硬件搬运数据的次数就越少。2. CPU跑AI算力的瓶颈在哪通用性的代价我们在PC上开发AI应用时默认的第一执行设备往往是CPU。CPU是“什么都能干”的通用处理器但这恰恰是它跑密集张量计算慢的根源。想理解NPU的必要性得先看通用计算到底贵在哪。2.1 指令集和流水线对密集计算并不友好CPU的设计目标是在乱序、分支、中断、虚拟内存这些复杂场景下保持正确和高吞吐晶体管预算大量花在控制逻辑、分支预测器和缓存一致性上。它也有SIMD指令集比如AVX512能做向量运算但一条SIMD指令可以处理8个或16个数据而张量计算经常是成百上千个数据同时参与乘和累加。拿矩阵乘法来说C A x B 这种操作即使SIMD已经拉满CPU也只能按“指令-执行-写回”的节奏一步步推进。更麻烦的是AI模型里大量算子对小矩阵特别频繁矩阵尺寸不定CPU的SIMD没法总是保持满负荷。你可能遇到过这种情况某个模型在CPU上跑CPU占用率只有30%但速度还是慢。因为瓶颈根本不在算力而在指令解码、数据加载和在执行单元之间来回分配这些“额外成本”。2.2 隐藏的瓶颈不是算力而是带宽和缓存我测过一个粗糙的规律跑FP32的矩阵乘CPU的理论算力往往能到几十甚至上百GFLOPS但实际能跑出来的可能只有十分之一。差距大量来自内存层次结构——CPU先从L1缓存取数L1没有去L2L2没有去L3最后才到内存。每次数据搬运的延迟比一次浮点乘法高两三个数量级而张量计算的特性恰恰是“一个数会被反复用很多次”。举一个实际的数字假设一个矩阵乘计算量是 1 GFLOP10亿次浮点运算数据总量只有几十MB理论上是完全能被缓存装下的。但如果代码写得不好每次乘的时候都把数据从头读一遍那么实际访存量会暴涨到几十GB带宽瞬间被压垮。CPU的缓存和调度策略把太多精力花在“猜测”上而张量计算的访存模式是高度规整、完全可以预判的通用CPU却没法利用这一点等于拿大炮打蚊子还经常打不准。2.3 NPU并不是CPU的替代品而是“专门裁缝”这不代表CPU要被淘汰。CPU负责的是真正的“通用大脑”系统调度、IO、AI之外的各种业务逻辑这些短时间内没人敢交给NPU。NPU的意义是当你知道计算模式是“大批量乘累加、数据可以按固定模式复用”时就不再需要那种通用而昂贵的“猜测式”执行单元而是可以做一个非常专精的、为矩阵乘法和卷积量身定制的计算引擎。打个比方CPU像一个十八般武艺样样都会的杂技演员让他做Three-Phase的复杂操作没人比得上NPU则像一个专切土豆丝的厨子你给他一批土豆他手起刀落效率高出杂技演员几个量级。麻烦在于这个厨子只会切土豆丝你让他切葱他是不干的。所以NPU的软件栈本质就是一套把“任意菜品”都尽量翻译成“切土豆丝”流程的工具链。3. NPU架构拆解为张量计算反着设计的芯片厂商宣传NPU的时候都喜欢报TOPS好像算力数字越大就越猛。但真正决定NPU能不能跑出账面性能的是两个更底层的东西数据怎么流动以及计算单元的密度。3.1 近存计算与数据复用少搬数据多算数据传统芯片的通用结构里计算单元是一块存储是另一块两者之间通过总线搬数据。这种结构对AI推理是灾难因为矩阵乘里的数据复用度极高——一个输入元素会被多个输出元素共用。NPU内部会设计专门的片上存储同时把计算单元和存储排布得非常近让数据从片内SRAM直接进计算单元避免反复访问外部内存。更关键的设计思想叫“数据复用”。以卷积为例一个输入窗口在一个输出通道上移动时输入数值本身没有变只是和不同的权重做了乘累加。NPU的精巧之处就是让这些权重被“固定”在计算单元旁边输入数据在片上“流动”流转而不是每做一个输出就重新取一次数。业内常说的Dataflow优化就是针对不同算子选择是固定权重还是固定输入以最大化片上数据复用、最小化访存次数。3.2 脉动阵列和大规模乘累加单元NPU最标志性的结构是脉动阵列。它和CPU里“一条指令控制一次乘加”的思路完全不同阵列里的每个处理单元PE只做非常简单的乘累加数据像水流在管道里一样从一个PE流向下一个PE每经过一个PE就累加一次。同一组数据在阵列里跑一遍就能输出一整块结果矩阵相当于把一个二维矩阵乘拆成了大量并行的流水线计算。这就是为什么TOPS不是唯一指标。一个NPU上报100 TOPS可能是在极端低精度、极高阵列利用率下测出来的但真实模型里如果数据形状和阵列维度不匹配部分PE会空转有效算力可能只有三成。另一个常被忽略的点是“乘累加单元密度”。同样面积下NPU能把上百万个乘法器集成进去而CPU同一块面积还要留出巨大的控制逻辑和缓存。所以即便是主频很低、算力数字不大的NPU在特定体型和低精度下也能把CPU按在地上摩擦。3.3 低精度计算的取舍逻辑端侧做推理FP32基本是奢侈品。NPU上主流是FP16、BF16、INT8甚至INT4。这不只是省显存这么简单更关键的是低精度数据类型的单位面积能塞更多计算单元——同样一块硅片跑INT8的乘法器数量可以是FP32的四倍。所以相同的物理面积下INT8的TOPS数字会大幅提升。代价是精度。模型在FP32下正常切到INT8就可能出现个别层输出偏移生成图片有噪点文本概率分布变歪。实际部署时不会一刀切全部量化而是做混合精度敏感层比如注意力里带SoftMax的地方保留FP16或FP32卷积类算子用INT8。这个取舍逻辑拿到NPU上尤其重要因为各家NPU对低精度的处理方式差异极大有的支持INT8校准得很好有的则只是“能跑但边界值很抖”。4. 从模型到NPU的执行链路编译器才是真正的主角硬件架构再精巧如果没有软件把它们“翻译”成可执行计划它就是一坨高性能摆设。实际上整个NPU生态里最复杂、最让开发者头疼的不是硬件而是软件链路。4.1 模型到图到算子第一步是先脱离Python模型在PyTorch/TensorFlow里是Python对象但NPU不认识Python。训练好的模型需要首先被导出成一种中间表示最常见的包括ONNX、TFLite等这些格式把模型拆成一个计算图——节点是算子Conv、MatMul、Softmax等边是张量的流动方向。这个图描述了计算顺序和数据依赖不包含任何具体设备相关的信息。这一步充满了坑。做过导出的同学都知道PyTorch里一个动态shape的模型转到ONNX时如果不显式指定shape就会变成动态图很多NPU编译器直接不支持。还有像Python的for循环、if分支、字典操作如果没被trace进去导出的图可能静默地少了一部分计算。我之前处理过一个问题模型转ONNX后推理结果全错排查半天发现是PyTorch的某个in-place操作没被正确记录。导图这件事本质上是把Python的动态世界“固化”成一个静态图越早意识到模型必须可静态化后面越省心。4.2 图优化与算子融合计算图只是第一步接下来编译器要对图做优化。最常见的是算子融合相邻的卷积、批归一化、ReLU在数学上可以合并成一次卷积运算只不过权重里事先包含了BN的参数、偏置里混进了ReLU的阈值。NPU上这样的融合非常关键因为每一次算子执行都意味着一次“从片外读数据-计算-写回片外”的完整往返融合一次就能省下一整轮内存搬运。再往下编译器会把大图切分成子图把能在NPU上跑的部分切出来剩下不支持的算子回落给CPU执行。这个切分点决定了你模型最终是“大部分跑NPU”还是“大部分跑CPU”。你看到的很多“xxx设备适配成功率”本质上就是这个切分逻辑的覆盖率。我自己常见的一个情况是模型整体结构没问题但某个小众算子比如某个奇怪的采样方式没有NPU实现结果整个子图碎片化决策引擎频繁在NPU和CPU之间来回切换速度甚至比全CPU还慢。4.3 厂商SDK和IR同样的模型不同的方言如果把ONNX/TFLite比作国际通用语言那各家NPU的SDK就是方言。高通有QNN联发科有NeuroPilot英特尔有OpenVINOAMD有Ryzen AI以及基于ONNXRuntime的Vitis EP苹果有CoreML华为有CANN。这些SDK接收模型后会做自家的算子映射、内存规划和指令生成最终下发给NPU固件执行。所以同一个ONNX模型在不同厂商的设备上会得到完全不同的性能这很正常。因为各家从算子实现精度、排列布局到调度策略都不一样。我在Intel Meteor Lake的NPU上跑过Stable Diffusion相关测试同一个图用OpenVINO的FP16路径能跑切到INT8路径某些Adapter直接不支持而AMD的Ryzen AI走的是另一套定制流程只支持自己验证过的模型列表自定义结构基本要自己改图。这就是“方言”的现实——不是说某家技术更好而是你想用好它就得接受它的语言习惯。5. 异构调度与端侧落地CPU、GPU、NPU到底听谁的端侧AI真正落地时很少只靠一个处理器。手机上拍照是ISP预处理NPU推理CPU后处理的流水线笔记本里跑大模型也可能同时用到NPU和核显。硬件之间怎么分工由推理框架和应用侧代码决定。5.1 推理两阶段调度和计算分开一个成熟的端侧推理框架会把你模型的整个执行拆成两层高层调度负责图遍历、算子选择、Buffer规划低层执行则把单个算子下发到具体硬件。调度层不关心硬件细节只关心“哪个设备能跑这个算子、用哪个精度、需要多大内存”到了执行层才会真正调用硬件驱动。这里有个关键概念叫“设备选举”。框架会评估每个算子在不同设备上的预估耗时然后给每个设备分配一个算子子集。试想同一个矩阵乘CPU耗时10msNPU估算耗时3ms但NPU往往需要一次大Buffer拷贝实际换算下来可能5ms。所以调度器并不是盲目地把所有算子都扔给NPU而是做成本建模。这也是为什么“NPU能不能加速”不能只看某个单算子的速度要看整体调度开销。5.2 真机实例把ComfyUI跑在Intel NPU上经历了什么我知道这个标题的热搜里有“ComfyUI调用Intel NPU”因为这个需求太真实了。同行们拿轻薄本跑ComfyUI看到NPU就以为能硬解SD结果多半会踩坑。Intel的方案是OpenVINO。ComfyUI要跑在NPU上通常得走ComfyUI的OpenVINO后端而且要注意NPU代号VPU和核显iGPU是两条完全不同的执行路径。我有一次在Meteor Lake上开OpenVINO的NPU插件SD的UNet部分确实能跑起来但速度没有想象中快原因是elnap图的某些算子被编译成了低效率的格式反而把整机功耗拉高。后来我把采样器改成FP16、关闭部分INT8量化路径再调整子图切分策略才勉强达到可用的水平。这事给我最大的教训是搞清楚“哪一部分算子真的跑到了NPU上”比直接按下运行键更重要。ComfyUI节点界面里并没有直接显示算子分配但你可以通过OpenVINO的debug信息看到每个subgraph的device分配或者在任务管理器里观察NPU占用率。如果跑起来NPU占用一直是0或忽高忽低说明子图切分根本没起作用。5.3 AMD Ryzen AI与Llama.cppNPU加量化跑大模型的操作AMD这边的生态是Ryzen AI。它的落地方式和Intel不太一样走的是ONNX Runtime Vitis EP以及一套基于MLIR的定制编译器。AMD的NPUXDNA有一个特点对大模型支持的模型列表比较固定自己随便拼的模型很可能编译失败。我之前试着在AMD平台的Llama.cpp里开启NPU后端。Llama.cpp本身支持多种后端NPU后端目前处于非常早期的阶段默认还是走CPU和CUDA路径要自己编译时开启特定标志并且把量化格式转成NF4等NPU能接受的形式。跑通了之后生成速度确实比纯CPU快但最主要的问题还是生态成熟度——不是每次构建都能成功一旦某次升级改了模型结构又得重新折腾。AMD的平台定位很明确集成显卡本来就强NPU更像“低频高能”的补充计算单元适合固定场景的持续推理而不是什么都往里塞。我自己更倾向于把NPU当作“批处理加速器”而非“全能加速器”因为当你需要最低功耗、最稳定吞吐的固定任务时NPU的价值反而最能体现出来。6. 部署选型与排坑清单上层模型要真正跑在硬件上关键看这几件事最后聊点实际的。不管你是做APP端的物体识别、PC端大模型助手还是轻量级Stable Diffusion工具想把模型真正从“开发机跑得好”变成“端侧设备跑得也还行”都必须面对下面几类现实问题。6.1 算子覆盖率决定你能不能跑起来第一个要问的问题永远是这个模型用到的算子目标NPU的算子库支持吗不看这个其他都白谈。Coverage的问题很少是“某算子完全不存在”更多是“某算子只在特定输入维度、特定精度下才支持”。常见的坑是UpSample、Gather、Split这些看似基础的算子在NPU上可能是分档支持的。排查方法不复杂先把模型导出成ONNX/TFLite然后跑到厂商SDK提供的模型转译工具里看转换报告。如果某个算子报“not supported”优先尝试把它替换成等价结构。比如把Dynamic Range的Resize换成静态shape的Resize把NMS换成基于TopK的自己实现很多时候就能把覆盖串起来。有一点要提醒厂商的报告说“支持”不代表“高效”要结合后面几项一起看。6.2 动态shape、内存带宽和时延抖动这三个坑是端侧部署的三个黑洞。动态shape最要命。NPU的编译过程往往要锁定具体shape才能做内存规划和指令流水编排。模型输入尺寸一变整个编译计划失效运行时就必须重新编译慢到离谱。解决办法是在模型入口固定分辨率目标检测就统一Resize到448x448能忍就忍不能忍就做多分辨率切换的多份编译缓存。内存带宽决定了上限。很多端侧SoC内存带宽只有个位数到十几GB/s哪怕NPU的算力数字听起来很猛带宽不够一样会把模型卡在“等待数据”上。算一个简单的账跑一次1GFLOP的INT8矩阵乘需要读入大约数百MB的数据如果带宽只有12GB/s光搬运数据就要几十毫秒NPU真正计算反而用不了那么久。所以看到TOPS数字猛先查平台的内存规格判断到底是不是纸面性能。时延抖动是NPU的隐形问题。NPU共享内存带宽、共享SoC散热、甚至和ISP抢资源导致同一段代码跑十次每次时延差出一倍。你要是做实时音频或者实时视频流应用必须做推理管线预占或者超时保护否则用户体验会忽好忽坏。6.3 我给端侧AI落地列的几个检查项我把这些年踩过的坑浓缩成一套检查单你先对着过一次再动手部署能少走不少弯路先把模型的动态维度全部静态化能避免80%莫名其妙的部署失败明确目标精度在开发阶段就确认是FP16还是INT8别最后才切精度导致调参重来把算子替换的工作往前放如果模型里有小众算子尽早找等价替代而不是拖到集成阶段去问厂商客服检查一遍厂商SDK的模型列表或者在编译报告里搜一遍“warnning”很多低级错误比你想的多得多做一次纯CPU基线测试再对比NPU加速后的实际端到端耗时和功耗别被单算子加速冲昏头脑。整链路如果没优势就别为了“用NPU”而用NPU最后选型时优先挑已经有生态落地案例的推理框架而不是自己从零写一套NPU调用代码。你调通的不是NPU本身而是整个上层框架与模型图的匹配度。从张量到NPU说到底就是一句话先把问题固化成硬件看得懂的规整结构再让硬件用最擅长的方式把它算完。端侧AI的优化从来不神秘神秘的是你不肯去拆那层被厂商包装起来的执行细节。真拆开了芯片还是那个芯片编译器还是那个编译器一切都能算明白。
返回列表