ARTICLE DETAIL

资讯详情

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

从张量到NPU:端侧AI部署的底层执行逻辑全解析

从张量到NPU:端侧AI部署的底层执行逻辑全解析 我干端侧 AI 部署也有几年了从早期在手机上调 DSP到后来在 NPU 上跑检测模型再到这两年把大模型塞进 AI PC一个感受特别强烈很多人对 NPU 的理解停留在“TOPS 越大越快”或者“NPU 就是硬件加速器”这种层面真到上手的时候发现算子不支持、量化掉点、张量布局不对换一个平台又得从头再来。这篇文章我想以“张量”为起点从数据形态、硬件结构、算子映射、量化部署到踩坑实录把端侧 AI 的底层执行逻辑完整捋一遍。适合刚接触 NPU 的入门者也适合已经在做迁移部署、想系统补齐原理的同学。1. 张量是 NPU 的“语言”从数据形态到硬件访存的底层约定1.1 把张量讲透维度、形状和内存顺序很多人以为张量就是“数值的多维数组”这个理解没错但放到 NPU 上就太粗糙了。NPU 不像 CPU 那样对内存里的数据“想怎么读就怎么读”它对数据在内存里的排列方式极其敏感同样的形状、同样的数值摆放顺序不同性能可能差出好几倍。先回顾基础。标量是 0 维向量是 1 维矩阵是 2 维三维以上的统一叫张量。深度学习中一个典型张量长这样shape [1, 3, 224, 224]含义是 batch1、通道数3、高224、宽224。这个顺序本身就有讲究它对应的就是 NCHW 布局也就是“样本-通道-高-宽”。但在实际硬件上数据不是按我们理解的多维抽象存的而是拉成一维字节序列。于是关键问题来了访问某个坐标 (n, c, h, w) 时它在内存里的偏移量是多少这就引入了 strides步长的概念。不同步长方案决定了你的数据是“通道连续”还是“像素连续”。NCHW通道是外层遍历同一像素点的不同通道在地址上离得远NHWC宽度是最内层连续同一位置的不同通道在地址上紧紧挨着通道交错 / 分块布局把通道按 4、8、16 分成小块再按块排布CPU 上跑很多算子时 NHWC 往往更快因为 SIMD 向量化通常按最低维度连续访问一次加载能取到多个通道的同一位置像素。但到了 NPU 上情况又不一样NPU 普遍采用“通道分块”的布局常见的是把 C 轴拆成 16 或 32 的倍数配合硬件 MAC乘加器阵列的宽度。比如昇腾的 5HD 格式、瑞芯微 RKNN 内部布局、寒武纪的 NCHW 变形本质都是把通道维度重新排布让 MAC 阵列能“一口吃到一批数据”。所以我特别强调一句话在 NPU 上张量的形状只是抽象层面的契约真正决定性能的是张量落到物理内存后的排列方式。调布局往往比调算法更直接。1.2 NPU 的执行模型张量指令和数据流CPU 是冯·诺依曼结构一条条指令取进来操作数搬运到寄存器运算完写回内存。GPU 是 SIMT 模式很多线程并行执行同一个指令。NPU 则走的是另一条路张量指令 数据流。什么叫张量指令就是一条指令里带上完整的数据形状、起始地址、步长、位宽信息硬件一次性处理一大片数据。举个接地气的例子CPU 做一次 3×3 卷积内层循环可能要跑几百次乘法NPU 是直接把“输入特征图矩阵 × 卷积权重矩阵”映射到 MAC 阵列上一个时钟周期出几百甚至几千个乘加结果。数据流执行则意味着卷积算子的输出不是先写进全局内存再被下一个算子读而是直接通过片上互联传到下一级 Buffer甚至直接作为池化或激活的输入。这就是算子融合的硬件基础。框架层面的融合可能只是一层“图优化”但 NPU 上的融合是物理上的连续数据流动没有多次访存的开销。这就带来一个部署上的核心原则计算量不是瓶颈访存才是。NPU 的小算力在 TOPS 上看着不大但它的设计目标就是“用尽量少的功耗访存尽量多次的数据复用”。所以我们做端侧模型时最先管的不是算子实现而是每一层按什么方式读写内存。1.3 计算强度为什么“访存优化”永远排在算法优化前面业内衡量一个算子受限于计算还是受限于访存用的是算术强度这个概念就是“每读取一个字节数据对应做多少次浮点运算”。单位是 FLOPs/byte。粗略估算一下 3×3 卷积。假设输入是 224×224×64输出通道也是 64核是 3×3。对每个输出像素要做 3×3×64 次乘加也就是 576 次乘加、约 1152 FLOPs。但对应的输入数据量呢如果按 NHWC 布局卷积窗口内是 3×3 个像素每个像素 64 个通道共 576 个数值每个值假设是 FP32 就是 4 字节那么权重对应的就是 64 个输出通道 × 576 个值 × 4 字节 ≈ 147KB。实际计算量除以实际访存量比值可能只有几十。这个算术强度其实不高意味着整个算子更多是在等数据搬运。NPU 的应对办法是两层缓存结构L1 Buffer 放当前卷积层的输入块L2 或 SRAM 放权重。设计时就要让权重常驻在片内输入数据按块滑窗复用。真正会优化的 NPU 部署工程师会把卷积按行块切分保证滑动窗口覆盖范围内的数据已经进了 Buffer而不是靠硬件 cache 去猜。明白了这一点你再看 NPU 的 TOPS 和带宽规格就能判断一个模型跑得快不快了。TOPS 再高如果内存带宽只有几十 GB/s那峰值算力就是纸面数字。很多手机 NPU 标称十几 TOPS但跑大模型时首 token 速度很一般问题基本都出在权重加载带宽上。实操建议做端侧部署之前先用模型各层的权重总字节数除以可用带宽估算纯权重搬运时间。如果这个时间已经超过你的延迟预算那再怎么优化算子实现都救不回来必须换更小的模型或更强量化。2. NPU 的硬件结构MAC 阵列、SRAM 与数据复用的真实玩法2.1 MAC 阵列一台流水线工厂NPU 最核心的运算单元是 MAC 阵列。一个 MAC 单元做一次乘加a × b c深度学习里的卷积、全连接、矩阵乘法本质上都可以拆成海量 MAC 操作。把 MAC 阵列理解成一条汽车装配线特别形象。一个工人MAC 单元只做“把一个零件装上去”的动作流水线上素材输入特征图传过来每个工人从手边寄存器或片内缓存取对应的标准件权重做完传给下一条线。整条线同时处理很多辆车效率就是单人的几十上百倍。数据怎么喂给流水线决定了效率上限。NPU 通常支持两种数据流方向权重固定权重先加载到阵列旁的缓存里激活数据沿流水线流动每流过一个位置就和所有权重做一次乘加。适合卷积层因为同一条卷积核要被不同空间位置的数据共享。激活固定激活值留在阵列里权重数据从外面不断推入。适合全连接层和矩阵乘法因为右侧矩阵要反复参与所有输出位置的计算。做算子映射时搞清楚硬件是哪种数据流偏好再决定把哪个矩阵塞进“固定端”这是能直接影响性能的选择。跑大模型时通常是权重做固定端每次推理把输入的 token 或 hidden state 沿阵列推过去这么做的好处是权重只加载一次能在片上复用很久。2.2 片上存储NPU 的“工作台”决定了算力能不能兑现片上存储SRAM / Buffer是 NPU 第二个必须关注的结构。MAC 阵列算得再快数据要从 DDR 或 LPDDR 内存搬运那是典型的“远水救不了近火”。片上 SRAM 的带宽可以做到几百 GB/s 甚至上 TB/s代价是容量小一颗 NPU 通常只有几百 KB 到几 MB。这就产生了一个经典的工程矛盾容量和带宽不可兼得。模型权重超过片上容量就必须分块加载吞吐自然掉下来。跑大模型时这个矛盾尤其明显。拿最近讨论度很高的 AI PC NPU 来说没有谁真的把几十亿参数的权重全部塞进片内。大家普遍做两件事权重 4bit / 8bit 量化压缩把加载字节数降到原来的四分之一甚至八分之一算子拆成“分块滑动”模式每次只让一部分权重常驻 SRAM我见过不少团队模型量化到 INT8 之后速度反而没有跟着翻四倍原因就是算子没有按片内容量做分块导致权重被反复从外层内存加载量化省下来的字节又被低效读取代回去了。2.3 为什么 Winograd 和低精度是 NPU 的常客NPU 跑卷积时最常见的一种加速手段是 Winograd 算法。它的核心思想很反直觉不直接做卷积乘加而是通过矩阵变换把乘法的次数降下来用更多加法去换。以 F(2,3) 为例2×2 输出窗口配 3×3 卷积核直接卷积需要 36 次乘法Winograd 变换后只需要 16 次乘法数量减少了近 55%。MAC 阵列的成本主要来自乘法器减少乘法就是在减少硬件面积和功耗。所以很多 NPU 内部集成了 Winograd 引擎部署工具链也往往默认开启。但 Winograd 有两个副作用一是变换矩阵会引入额外数值误差FP32 下还好INT8 下精度掉得更明显二是它要求数据有特定分块大小如果特征图尺寸或卷积参数不匹配硬件会退化到直接卷积模式。这时候你可能会发现部署工具里某层没有跑 Winograd不要慌去查它是不是自动回退了。低精度就更不用说了。INT8 乘法器和 FP16 乘法器在面积上能差好几倍INT4 又能再省一半。NPU 本质上是用更小的数值位宽换更多的并行算力。Tensor 从 FP16 变成 INT8MAC 阵列的吞吐密度理论上可以翻倍但前提是数据访存也同步减半才不会出现计算宽松但访存拥堵的倒挂。补充一个关键细节低精度不是简单截断NPU 硬件一般要求数据以特定格式喂入比如 INT8 要带零点和比例因子。工具链负责把浮点模型转成定点的 Scale 和 Zero Point这一步做不好整个模型的精度就崩了和第 3 节要讲的量化密切相关。3. 端侧大模型的执行链路从 PyTorch 到 NPU 上的 INT4/INT83.1 模型转换ONNX 到厂商格式之间的翻译官端侧 AI 部署第一步是把训练好的 PyTorch 或 TensorFlow 模型转换成厂商 NPU 工具链能识别的格式。流程大概是这样的PyTorch 模型 → ONNX → 厂商 IR比如 OpenVINO IR、RKNN、OM 等→ NPU 可执行文件为什么拿 ONNX 作为中间格式因为它定义了标准计算图各大厂商的工具链都实现了 ONNX 解析器。自己做模型导出时最常踩的坑有两个动态 shape 问题PyTorch 里很多模型默认允许动态 batch 或动态分辨率。NPU 工具链需要静态 shape 来精确分配 Buffer、做内存规划。导出 ONNX 时最好显式固定输入尺寸比如 1×3×640×640并用 dynamic_axes 谨慎处理。算子拆分过大PyTorch 里一个模块在 trace 后可能拆成很多细粒度算子比如把 LayerNorm 拆成多个 ReduceMean、Sub、Pow、Add。NPU 上这些细算子逐一执行会比较浪费工具链的图优化阶段会尝试把它们重新融合融合失败就会退化成 CPU 执行或直接报不支持。转换之后一定要检查每个算子的“支持状态”。常见的有三类NPU 原生实现、反解到 CPU 执行、直接不支持报错。CPU 执行会破坏整体流水线的数据同步虽然结果对但性能往往很惨。3.2 量化为什么权重从 FP16 到 INT4 不是“砍掉一半”那么简单量化是把连续性数值映射到离散整数值的过程。NPU 上最常用的是均匀对称量化公式大概是这样q round(clamp(x / scale, -128, 127))对应 INT8 的话scale max|x| / 127。如果没有加零点的对称量化INT8 的动态范围就是 [-128, 127]一个 scale 管全张量。听起来简单实际工程里有几个很容易让人忽略的点按层量化 vs 按张量量化按张量量化就是整个权重矩阵一个 scale简单但精度差按层或按通道量化是每个输出通道一个 scale精度显著提升现在几乎所有工具链默认都这么做。代价是反量化过程更复杂硬件如果支持“每通道 scale 随权重一起加载”就没有额外开销。激活的量化更难权重是静态的统计数据一次就能算准激活是推理时动态的只能拿校准集去统计分布。校准集选不好激活量化出来的 scale 就会偏离真实分布典型表现是模型整体掉点但权重量化误差明明很小。INT4 的精度陷阱INT4 只有 16 个离散级别如果权重分布集中在一个小范围量化后大量数值会挤进同一个等级信息严重丢失。所以现在大模型部署基本都是分组量化比如 GPTQ 里的 group_size128每 128 个权重一组算一个 scale把精度损失摊开。NPU 工具链是否支持这种分组量化格式是选型时一个很关键的问题。量化后还有一步常被忽略算子里卷积或矩阵乘法的输入是量化后的定点数中间累加结果必须用 INT32 甚至更高位宽保存否则多个大数乘加后溢出模型输出的概率全是垃圾值。端侧工具链一般会自动处理但如果你手写算子或做算子融合必须保留足够的累加位宽。3.3 图优化和算子融合NPU 性能的隐藏变量工具链拿到计算图后会做一系列图级别优化。在真正的 AI 框架里这种优化叫“图优化 pass”。NPU 工具链最常见的几类优化Conv ReLU / Add 融合把激活函数并入卷积算子因为 MAC 阵列输出后直接过一个激活单元不需要把中间结果写回内存再读出来。很多 NPU 硬件原生支持这个模式。Transpose / 布局转换消除上面第 1 节讲过不同硬件的布局偏好工具链会在图上插入布局转换算子NCHW→NHWC 之类的。太多布局转换反而会拖慢速度所以优化器会尝试合并或者尽量提前在模型导出时就统一布局。常量折叠直接把权重相关的常量计算在编译期完成不放到运行时去执行。算子间 Buffer 复用分析生命周期让先后两个算子在片上复用同一块 Buffer最大化 SRAM 利用率。这几个 pass 看起来比算子本身更“看不见摸不着”但对最终性能影响巨大。一个比较典型的经验同一个模型在同一个 NPU 上工具链版本不同性能可能差 20% 以上很多差距就是图优化质量造成的。所以端侧部署绝不是“导出一次一劳永逸”工具链更新版本后重新转换、重新比对精度和延迟是常态。这里必须强调一下部署文档的作用把模型转换时用的工具链版本、量化参数、校准集信息、图优化开关都记录下来。不然出了精度问题你根本没法定位是量化配置变了还是算子融合策略变了。4. 不同 NPU 平台的“脾气”AI PC 与端侧硬件的实际体验4.1 为什么大家都在讨论“CPU NPU GPU”三件套最近 AMD NPU 跑大模型、ComfyUI 调用 Intel NPU 这类话题热度很高很多人会问NPU 到底能干什么和 CPU、GPU 有什么区别我基于自己的使用体验给个判断。传统 CPU 擅长逻辑控制分支预测和乱序执行非常强但做密集型并行计算效率不高GPU 强在 SIMT 并行大量核心齐上阵适合图形渲染和通用计算NPU 强在低功耗、高吞吐的定点矩阵运算专为神经网络设计但它不擅长复杂分支逻辑和控制流。端侧跑大模型时单靠 CPU 会慢得离谱单靠 GPU 又功耗偏大NPU 则能在很低的功耗下持续吞吐 token。像 AMD 的 Ryzen AI 系列平台标称 NPU 算力从 16 TOPS 到 50 TOPS 不等跑基于 Transformer 的模型时NPU 负责 decode 阶段的矩阵乘CPU 负责采样、KV cache 管理和一些并行分支GPU 则可能被调用起来做图像相关的数据集这种分工就是现在“AI PC”的标准玩法。装配的时候经验也很直接先用厂商工具链查看 NPU 支持哪些算子再把模型里真正“吃算力”的部分交给 NPU不要让 NPU 去执行它不擅长的算子。强行把所有算子都压给 NPU最后 Perf 出来可能比 CPU 还慢这个坑我踩过不止一次。CPU 降级执行的原因一般是硬件没有对应的算子实现工具链会把该节点标记为“fallback to CPU”但同步开销和内存拷贝足以把 NPU 加速吃掉。4.2 ComfyUI 调用 Intel NPU一次 AI 绘图侧的实践观察拿实际例子说ComfyUI 调用 Intel NPU 走的一般是 OpenVINO 这套工具链。你先把 ComfyUI 的 PyTorch 模型导出为 OpenVINO IR再用 NPU 插件加载和推理。实际操作中有几个最容易卡住的地方模型精度格式NPU 插件对 IR 的精度有要求比如只接受 FP16/INT8。导出时如果不注意它会强制转换转换质量未必理想。动态形状ComfyUI 里输出分辨率可以动态变化NPU 插件对动态 shape 支持不算特别完善最好固定一批常用的分辨率并预先编译。多阶段流水线ComfyUI 生成图像要走“文本编码 → 采样 → 解码”多个阶段。采样阶段有大量 step 循环上图里每一步都会有连续计算这种情况下 NPU 的持续吞吐优势才能完全释放。如果你只是把某个一步算子放到 NPU 上其他都留在 CPU综合延迟大概率是倒退的。操作路径大致是用 openvino.convert_model 把 UNet、文本编码器、VAE 分别转成 IR也可以用 OpenVINO 官方工具链。为特定分辨率准备固定 shape用模型编译接口指定 device_nameNPU。把 IR 替换进工作流跑通后再逐步调 batch 和压缩选项。这种过程最能检验你对底层执行逻辑的理解你会发现同一份 IR 在不同设备上能不能跑、跑得快不快很大程度上取决于“量化 shape 固定 算子支持”这三个变量而不是模型的架构多先进。4.3 NPU 算子开发什么时候需要自己写算子热门词里有“NPU 算子开发”这个方向主要服务于两类场景模型里出现了厂商工具链不支持的算子比如某个新激活函数、自定义 Attention 变体工具链自动生成代码性能差需要手写底层实现去压榨硬件算子开发的门槛不低因为你得同时理解硬件指令集、数据布局、内存管理和数值精度。很多厂商提供的开发方式有两种一种是提供一套高级语言 API底层帮你映射另一种是暴露底层指令接口你直接写汇编级的计算流程。如果团队刚起步我的建议是优先走高级 API 路线。先实现一个功能正确但性能中等的算子在 NPU 上跑通然后分析耗时分布看瓶颈是在访存、还是在计算阵列利用率不够再针对性优化。千万不要一步到位追求极致性能因为调试成本和硬件知识积累往往被低估。调试算子时有个常用技巧写一个对应算子的 CPU 参考实现再和 NPU 输出逐项比对。比对时注意数值误差范围定点运算的结果和浮点不会完全一致允许误差一般看工具链给出的量化误差参考标准不要拿 exact match 去卡。5. 端侧大模型部署的一线踩坑清单5.1 首 token 延迟别被“吞吐数字”骗了端侧模型部署最常见的认知偏差是看厂商宣传的“每秒生成 XX token”以为整体体验一定好。实际使用中还有一个更需要关注指标首 token 延迟就是从你输入 prompt 到模型吐出第一个字的时间。首 token 延迟的组成大致是输入 prompt 的 prefill预填充计算时间权重加载 / 常驻准备时间推理管线初始化时间NPU 的 prefill 阶段往往是计算密集的尤其输入很长时所有 token 并行计算MAC 阵列满载运行这时 NPU 能发挥真本事。但权重加载如果每次都要从 DDR 读一遍几十亿参数那首 token 延迟就是巨大的。我实际的优化顺序一般是先量化权重让每次加载字节数下降再尽量让权重常驻避免每次推理都动态加载最后才优化计算侧实测中这三步按顺序做完首 token 延迟经常能降一半以上而且不需要改模型结构。5.2 KV cache 与 KV Cache Offload显存不够时的“作弊”LLM 自回归生成时要缓存每一层的 KVKey-Value向量方便后续 token 去 Attention 里查询。这个缓存随着序列长度线性增长也是端侧常被忽略的内存杀手。举个例子一个 7B 模型hidden_size4096层数 32每层 KV 维度假设 2×4096如果缓存一个 2048 token 的上下文需要的数据量大概在数百 MB 到 1GB 级别。这对端侧设备内存来说并不小。所以端侧部署的工具链开始引入“KV Cache Offload”机制把不活跃的 KV 块临时放到系统内存或更慢的存储通过调度策略把当前需要的块搬到片上。问题来了如果你的工作负载是长文本对话KV 块要频繁换入换出带宽消耗会很可观最终拖慢生成速度。一个朴素的优化方向是调整缓存策略按 token 的激活活跃度做分段管理而不是简单按顺序置换。5.3 数据搬移和同步算子跑对了整体还是慢多半是搬移惹的祸端侧推理跑慢了经常会有人怀疑算子实现有问题但我在实际项目里遇到的最大瓶颈往往是一头一尾数据搬移。现代端侧 SoC 上NPU 是独立 IP它和 CPU 之间、和内存之间走互联总线。任何数据都要从 CPU 端存比如从摄像头采集、从文件系统读入拷到 NPU 能访问的地址空间推理完再从 NPU 侧拷回。这一来一回如果走的是通用 DMA 路径单次拷贝耗时就可能占到整个推理时间的 30%~50%。几个降低搬移成本的方向尽量让前后算子都在 NPU 上完成少和 CPU 交换中间结果如果多个算子都要用同一份输入尝试 Buffer 复用避免重复拷贝查看工具链是否支持“零拷贝”输入有的平台可以让你直接申请共享内存NPU 和 CPU 都能访问省掉一次 memcpy这些看起来像系统层面的事情反而比调算子更常见地决定端侧 AI 的最终体验。你要记住端侧 AI 的优化本质上是“带宽 缓存 量化 运算符支持”四个变量的综合平衡算得多快反而是相对次要的。5.4 工具链版本和算子退化同一个模型换个版本就变慢最后再说一个非常容易被忽略的点工具链版本更新带来的“稳定性问题”。很多厂商工具链的算子实现图优化策略一直在变同一个模型的 IR用旧版本编译可能正常走 NPU用新版本编译反而会有更多算子退化成 CPU 执行。原因可能是新版本识别出了某些算子之间的依赖关系然后调整了融合策略——但结果是性能回退而不是提升。我在实际项目中已经养成一个习惯同一份模型同时保存 FP16 原始权重、INT8/INT4 量化后的权重、以及对应工具链版本的编译产物这样每次版本升级后可以用同一套测试基准快速回归一旦性能异常能立刻定位到底是模型变了还是工具链变了。顺带分享一个比较务实的观点跑端侧大模型的收益有时候不在“性能数字更漂亮”而在“功耗表现更合理”。NPU 的优势不是让模型跑得比 GPU 快而是让同样的模型在更低的功耗下持续运行这对移动设备、嵌入式终端这类场景才是真正的价值。6. 最后分享一点普通的经验从张量到 NPU整个链路实际走一遍你会发现它并不神秘。张量描述数据硬件提供计算工具链完成映射部署工程师把这些环节串起来。真正难的不是某一个点而是当算子不支持、量化掉点、带宽打满、缓存不够这些问题一起出现时你能不能从底层逻辑出发判断到底该往哪优化。个人来说每次接手一个端侧模型我第一件事不是看模型结构不是调参数而是先查这三个东西工具链算子支持表、模型每层权重的字节数、NPU 片内缓存大小。这三份资料基本能把绝大多数性能问题的答案提前暴露出来。如果这篇文章能让你少走一点弯路那我就很满足了。端侧 AI 这条路上欢迎随时交流。
返回列表