☰
从张量到NPU:端侧AI推理的底层执行逻辑与工程实践
2026/10/5 5:34:48 网站建设 项目流程

我最早意识到“同一段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没有去L2,L2没有去L3,最后才到内存。每次数据搬运的延迟比一次浮点乘法高两三个数量级,而张量计算的特性恰恰是“一个数会被反复用很多次”。

举一个实际的数字:假设一个矩阵乘计算量是 1 GFLOP(10亿次浮点运算),数据总量只有几十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,英特尔有OpenVINO,AMD有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耗时10ms,NPU估算耗时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.cpp:NPU加量化跑大模型的操作

AMD这边的生态是Ryzen AI。它的落地方式和Intel不太一样:走的是ONNX Runtime + Vitis EP,以及一套基于MLIR的定制编译器。AMD的NPU(XDNA)有一个特点:对大模型支持的模型列表比较固定,自己随便拼的模型很可能编译失败。

我之前试着在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的优化从来不神秘,神秘的是你不肯去拆那层被厂商包装起来的执行细节。真拆开了,芯片还是那个芯片,编译器还是那个编译器,一切都能算明白。

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

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

立即咨询