尧图精选

从NPU算子出发,拆解推理Infra的底层架构与性能优化路径

🕒 发布时间:2026/9/9 2:12:23 📁 来源:尧图网络
平时大家聊推理 Infra聊得最多的是 CUDA、TensorRT、vLLM、显存、吞吐。但真到了性能压不动、算子落不了地、精度对不齐的时候你会发现所有问题最后都会钻到“算子”这一层。反过来从 NPU 算子往下摸推理 Infra 的骨架反而是一条特别顺的路——因为算子就是推理引擎执行的最小单位调度、内存、并发、量化全都要围绕它展开。这篇文章我打算从“算子是什么、NPU 怎么执行算子”讲起一路串联到推理引擎怎么编排算子、算子怎么影响整条推理链路的性能上限最后用实际部署中会踩的坑收尾。无论你现在是在做模型部署、推理优化还是写算法想搞懂底层这篇内容都能帮你把“模型到硬件之间那段黑盒”补全。1. NPU 算子的底层逻辑从“执行单元”理解推理的微观世界1.1 算子到底是什么先不说抽象定义直接看一个特别经典的例子Sobel 算子。做图像处理的人都知道Sobel 是用来做边缘检测的原理就是用一个 3x3 的卷积核去扫图像算出每个像素点在水平和垂直方向上的梯度值。这个“3x3 窗口滑过整张图、做乘加求和”的运算放到神经网络里就是一次标准的卷积算子。所以算子这个概念并不神秘它就是对张量做某种变换的数学操作。卷积、矩阵乘、ReLU、Softmax、LayerNorm、GELU全是算子。一个深度学习模型本质上是一张计算图图里的每个节点就是一个算子。模型的推理过程就是按顺序把这些算子逐个在硬件上跑一遍。为什么要单独强调这一点因为实际调优的时候模型框架层面的优化空间其实很有限真正能抠性能的地方全在算子层。比如同一个模型用 PyTorch 直接跑和用编译器优化后的引擎跑速度可能差 3 到 5 倍差别主要就是把一整个子图融合成了更少、更高效的算子序列。1.2 NPU 是怎么执行算子的常规的 CPU 是通用计算要啥都能算靠的是复杂的指令集和乱序执行。GPU 则是大规模并行靠成千上万个 CUDA core 把同样的指令灌进不同数据里SIMT 模式。而 NPU也就是神经网络处理单元走的是另一条路为神经网络算子做专用加速。这里要破除一个刻板印象——NPU 不是“弱化版 GPU”它的架构设计出发点完全不同。典型的 NPU 内部有几大块是绕不开的计算阵列也就是 MAC乘累加阵列一般排成脉动阵列systolic array或者二维矩阵形式专门吃矩阵乘和卷积这类算子的核心循环。片上存储SRAM 或者叫 scratchpad memory用来放输入特征图、权重和中间结果。NPU 对 SRAM 的大小异常敏感因为数据能不能留在片上直接决定要跑多少趟片外内存。DMA 引擎负责把数据从 DRAM 搬到 SRAM或者搬回去。很多 NPU 的性能瓶颈根本不在于算得快不快而在于数据“喂”得够不够快。指令解码/控制单元NPU 自己的指令集通常非常精简一条指令往往对应一大块张量运算比如“把 A 矩阵和 B 矩阵做矩阵乘结果存到 C”而不是一条一条做标量计算。所以回到“执行算子”这件事NPU 执行一个算子核心是“把算子的计算循环映射到 MAC 阵列上”然后用数据搬运隐藏计算间隙。一个算子能不能在 NPU 上跑得快看的不是算子本身的数学形式好不好看而是它在硬件上的映射效率高不高。1.3 算子和模型的区别很多刚入行的人会混淆“算子”和“模型”这两个词逻辑其实很清晰模型是完整的推理任务算子是这个任务里的一次具体运算。用做饭打比方模型是一份完整的菜谱算子是里面每个具体的烹饪动作——切菜是预处理算子下锅翻炒是卷积算子调味是激活函数算子出锅摆盘是输出层算子。这个区分在实际工作里非常有用。当遇到 NPU 算子兼容性问题时比如某款 NPU 的指令集不支持某个自定义算子你只需要改写或替换这一个算子的实现不用动整个模型。反过来模型层面的精度问题也常常要下钻到算子层面去逐个验证看是哪个算子在当前精度设置下引入了偏差。2. 推理 Infra 的骨架算子调度如何串起整条推理链路2.1 推理 Infra 的分层结构推理 Infra 这个词听起来很大实际拆开其实就四层搞清楚每一层在哪做什么后面聊啥都能对号入座硬件层NPU、GPU、CPU以及它们各自的内存、总线、缓存。编译层把模型的计算图翻译成硬件能执行的指令比如 ONNX 转成一个 NPU 专用的 engine 文件。引擎层运行时在编译产物基础上做调度、内存分配、并发执行比如 TensorRT、OpenVINO、ONNX Runtime或者各家 NPU 自带的推理框架。服务层对外提供推理接口处理批请求、超时、排队比如 Triton、vLLM、FastAPI 包一层模型服务。一个推理请求进来先在服务层被接住然后请求数据被预处理成张量张量进入引擎层引擎按编译好的算子序列在硬件上一一执行执行完的中间结果在内存里进进出出最后把输出张量送回去。从这个链路看“调度器”可以说就是引擎层的中枢它决定哪块数据应该放在哪、哪些算子可以和哪些算子合并、哪一簇计算单元负责哪一段子图。2.2 算子调度的核心逻辑调度器要干的活一句话总结把计算图切块、排序并在硬件资源上合理分配。这里面有几个关键动作图优化把结构等价但更高效的算子组合替换掉。比如多个连续的 1x1 卷积和激活可以融合成一个算子Conv 加 BN 在推理阶段可以折叠成一个 Conv因为 BN 的参数在推理时是固定的。内存规划提前算好每个算子的输入输出张量需要多大空间尽量复用同一块内存减少分配和释放的开销。这一步做得好的引擎峰值内存可以用得非常省。算子映射与并发切分把一个较大的算子拆到多块计算单元上或者把相互独立的算子放到不同簇上并行执行。NPU 不同于 GPU 动辄几千核它的并发度往往来自多计算阵列多簇/多核调度器能不能把算子拆得足够并行对利用率影响很大。在推理 Infra 里调度器的优化目标不是“只求结果对”而是“在满足时延目标的前提下让硬件尽量不闲着”。你可以把它理解为餐厅的传菜员菜做出来不是目的能不能稳定地在客人可接受时间内端上去才是目的。2.3 训练和推理的本质差异为什么推理 Infra 更在意算子时延训练和推理虽然都在跑算子但两者的“KPI”完全不同这个差异直接影响推理 Infra 的设计训练求的是吞吐推理求的是时延。训练阶段不在乎单个 batch 多慢只要整体梯度更新速度快就行推理阶段则看单个请求能不能按时返回。训练需要反向传播所以每个算子在执行时不仅要有前向结果还要缓存中间激活值供反向使用推理只需要前向内存和带宽都省下一大截。训练通常用动态 shape每批数据长度可变推理为了稳定性和性能会尽量静态化把输入尺寸固定从而让编译器和调度器做更极致的优化。明白这几点之后很多设计就能理解了为什么推理引擎会把一个多层 Transformer 子图融合成一个大的执行 kernel为什么部署阶段要把模型导出为 ONNX 再转 engine而不是直接拿训练框架跑为什么 NPU 厂商的推理工具链对输入尺寸限制特别严格。这些动作的动机全部指向同一个目标减少算子执行开销、降低端到端时延。3. 算子对硬件性能的挑战内存墙、碎片化与融合优化3.1 算子的两类性能特征计算密集与访存密集一个算子跑得快不快不能只看它的数学公式。工程上习惯于把所有算子按性能瓶颈分成两大类计算密集型compute-bound算子的算术强度高也就是每单位数据搬运背后有大量的乘加运算。典型代表是大矩阵乘GEMM、卷积层。这类算子只要把 MAC 阵列喂满利用率就上去了。访存密集型memory-bound算子的算术强度低时间几乎都花在等数据从内存搬进搬出。典型代表是 Softmax、LayerNorm、Reshape、Concat以及元素级激活函数如 GELU、ReLU。在 NPU 上区分这两类算子非常关键。因为 MAC 阵列吃掉的是片上运算能力而 DMA 搬运吃的是内存带宽。如果一段计算图里密集夹杂着访存算子哪怕是再快的 NPU也会被内存带宽卡死这就是常说的“内存墙”。举例来说在 Transformer 推理里Self-Attention 后面几乎总会跟一个 LayerNorm还要配合残差连接做相加。如果调度器老老实实按计算图逐算子执行数据会反复地“片上计算-写回 DRAM-再读回片上”白白浪费大量带宽。这也是为什么现在主流推理框架都会做算子融合把 elementwise 类算子合并到相邻主算子里。3.2 为什么大量使用算子的模型在 NPU 上特别容易性能拉胯很多从 GPU 迁移到 NPU 的项目第一个坑就是模型结构在 GPU 上飞快在 NPU 上却稀碎。这里表面上是“NPU 算力不够”实际上往往是因为模型里用了大量“非友好算子”。NPU 最擅长的是规则的、层级的、张量化的运算比如卷积、矩阵乘、池化、常用激活。但对不规则运算类似动态更新 dictionary、Python 层面的控制流、需要大量 gather/scatter 的操作NPU 的指令集和片上架构本来就不是为它们设计的。如果模型里混了这些算子编译器无法映射就只能 fallback 到 CPU 去跑一来一回性能直接被拖垮还会带来 CPU 和 NPU 之间的同步开销。这个问题的核心其实是“算子碎片化”——模型结构越来越花哨每两三层就插一个自定义算子每个算子都很小比如一个简单的 scale 操作但它们割裂了整个计算图导致调度器几乎没办法做有效的融合和流水。这也是为什么工业界反复强调“NPU friendly”的模型结构设计尽量用标准算子减少零星的自定义操作。3.3 算子融合、量化与其他优化手段面对碎片化问题优化手段主要有三条路第一条是算子融合。把多个独立的小算子合成一个大 kernel减少核函数启动次数和内存中间结果搬运。常见的有 ConvBNReLU 融合、QKV 投影的 GEMM 融合、Attention 里的 SoftmaxScaleMask 融合。融合之后访存密集型算子的开销会被“吸收”进计算密集型的算子内部隐藏到计算时间里。第二条是量化。NPU 很多主打指标都是基于 INT8 算的因为 INT8 的 MAC 阵列吞吐可以做到 FP16 的两倍甚至更高同时内存占用和带宽需求减半。具体来说FP16 权重单个元素占 2 字节INT8 只占 1 字节同样 8GB 内存FP16 只能塞 40 亿参数INT8 能塞 80 亿。不过量化不是在所有算子上都白捡性能像 GELU 这类非线性算子量化后容易引入精度偏大需要专门处理。第三条是深度流水线化。把整条推理链路拆分成多个阶段比如 prefill、decode、后处理每个阶段用独立的硬件资源并行处理从而隐藏阶段间的等待时间。在大模型推理里prefill 和 decode 就是典型的两种算子特征完全不同的阶段prefill 是计算密集decode 是访存密集分开调度能明显提升整体吞吐。拿卷积BN 折叠来举个例子。训练阶段 BN 有可变参数但推理时均值、方差、缩放、平移都是固定的于是 BN 可以完全吸收进前面卷积算子的权重和偏置里。原来要跑两个算子、读两遍数据现在一个算子搞定这既减少了计算量也压缩了中间张量的访存开销。4. 实操过程从模型导出到 NPU 部署的完整闭环4.1 工具链准备与部署流程换个视角从“跑通一个部署任务”的角度看算子如何参与全过程。我以把一个视觉检测模型部署到 NPU 上为例完整流程大概是这样的从训练框架导出模型。PyTorch 里用 torch.onnx.export记录下动态轴和 opset 版本。这里如果模型里有不受 ONNX 支持的算子需要先用 torch.onnx.export 的 custom ops 机制注册或者在导出前把模型里的算子替换成等效标准算子。用 NPU 厂商自带的编译工具加载 ONNX做精度设置FP16 还是 INT8、输入尺寸设置、量化配置。编译工具会输出一个 engine 文件。编写推理代码读入 engine 文件创建执行上下文分配输入输出 buffer调用执行接口。预处理和后处理流程读图、缩放、归一化、NMS放在 CPU 还是在 NPU取决于芯片上 CPU 能力和内存搬运成本。大多数轻量 NPU 上预处理建议在 CPU 做但尽量用向量指令批量优化。性能测试跑 1000 次推理统计平均时延、P99 时延、CPU 占用率以及 NPU 利用率。这个流程里最容易出问题的一步就是算子兼容性检查。建议在导出 ONNX 后先用 netron 打开计算图逐个确认不认识的算子类型。凡是名字带“Custom”、“Loop”、“If”的节点基本都是危险的信号。4.2 显存需求测算训练和推理的差异关于显存测算热词里总有人问“GPU 显存容量是测算推理还是训练用的”。准确答案是训练要算的参数更多推理需要的显存则按“权重 中间激活值 KV Cache如果是大模型 输出 buffer”来估。推理阶段显存需求的一个简化估算公式是权重显存 参数量 × 每参数字节数激活显存 batch size × 序列长度 × 每层激活张量 × 层数KV Cache batch size × 序列长度 × 层数 × 2 × 头维度 × 每参数字节数比如一个 7B 模型用 FP16 部署每参数 2 字节光权重就是 14GB。如果只给单卡 12GB 显存就必须量化到 INT87GB或者 INT43.5GB。卷积模型则相对轻量但带 Transformer 分支的视觉模型如 EfficientNetV2 加注意力模块也得算 KV Cache 之外激活值的峰值。实操中我习惯先跑一次 batch size 为 1 的推理用工具记录内存峰值再推 batch size 变大后的内存增长曲线。不要只看理论值因为调度器内存复用做得好不好可以差出两到三倍。4.3 算子替换和降级CPU fallback 的取舍NPU 部署里另一个常见操作是“回退”。当某个算子实在无法映射到 NPU 时推理框架会默认把它放到 CPU 去执行这就是 CPU fallback。CPU fallback 不是不能用但它有代价数据要在 NPU 内存和 CPU 内存之间搬一次还要等 CPU 算完整个过程比纯 NPU 执行慢一个量级以上。所以回退是“能不用就不用”但极端情况下是救命稻草。实操上遇到不支持的算子我一般按这个优先级处理找替代算子把自定义实现换成数学上等价的 ONNX Std 算子这是最优解。把算子往相邻主算子合并如果这个算子是对上一层输出做线性变换试试能否融合到上一层算子里。实在不行再 fallback尽量让 fallback 发生在计算图的边缘比如后处理部分而不是图的正中间。一个经验值如果整张计算图里 fallback 算子的数量超过总节点数的 5%部署性能基本就不用期待了。这时候与其硬调不如回头改模型结构把那些花哨算子换掉。4.4 性能分析如何定位热点算子拿到一份 NPU 推理引擎的 profile 报告怎么知道瓶颈在哪核心是看每个算子或融合后的子图的耗时占比和实际利用率。我的习惯是这样先看总时延里有百分之多少在 NPU、多少在 CPU。如果 CPU 预处理 后处理占了一半时间先优化这个别浪费时间去抠 NPU。再看 NPU 计算阶段的热点算子列表找出 TOP3 耗时算子。对每个热点算子对照它的类型如果是访存密集算子耗时高重点查内存复用和数据排布如果是计算密集算子耗时高重点查算力利用率是否逼近峰值。这里有一个容易被忽略的点算子耗时高不一定是算子本身慢也可能是它前面有一个屏障sync barrier在等别的算子完成。NPU 上的多簇调度如果让一个依赖链很长的算子卡住了下游所有算子profile 报告会显示下游算子虽然耗时不高但总的流水效率极差。5. 常见问题与排查技巧实录5.1 NPU 推理部署中遇到的高频问题这几年在不同硬件平台Intel NPU、手机 NPU、各类国产 NPU 加速卡上摸爬滚打确实攒了一堆经验。这里整理一个排查速查表问题现象直接原因排查与解决思路编译阶段报“unsupported operator”模型里有自定义算子用 netron 查看算子列表替换为等价标准算子或注册 custom op推理结果全零或明显错误输入输出 buffer 大小/对齐设置不对检查 engine 的输入输出 shape、数据类型确认是否做了 padding时延浮动剧烈P99 远高于平均CPU fallback 或内存分配抖动用 profile 定位 fallback 算子预先分配内存池避免推理中动态分配显存峰值超标batch 一大就崩激活值缓存没复用开启引擎的内存复用选项或手动调整图的执行顺序把峰值压下来精度和原始模型差太多INT8 量化导致敏感算子溢出对敏感算子GELU、Softmax做分精度处理或改为混合精度推理NPU 利用率上不去不到 50%算子过于碎片化流水线没吃满优先做算子融合或者把多个小图合并成一个大图再编译5.2 一个典型排查案例时延抖动问题实录有一次在边缘设备上部署一个视频检测模型平均时延 12ms表现不错但 P99 飙升到 45ms客户无法接受。初步判断是内存分配或 CPU fallback 导致。排查步骤是这样的第一步先关掉 CPU 占用的其他服务重新跑一轮P99 降了一点但还有 30ms排除 CPU 抢占因素。第二步用推理框架自带的 profile 工具记录每次推理的算子耗时。结果发现大部分时候算子都按预计时间跑但每隔十几帧会出现一次阻塞阻塞位置正好在“后处理 NMS 结果拷贝”附近。第三步发现 NMS 逻辑用的是 Python 写的每次都要把 NPU 的检测结果张量拷到 CPU 侧才能算。在 batch 偶尔排队时NPU 已经算完下一帧了但 CPU 还在算上一帧的 NMS流水线就断了一下。解法是把 NMS 换成 C 实现并把张量拷贝改成双缓冲NPU 往 buffer A 写CPU 同时读 buffer B把同步次数从每帧一次降到每批一次。最终 P99 压到 16ms问题解决。这个案例能说明一个事推理 Infra 的性能问题很多时候不是某个算子慢而是算子之间的协作方式出了问题。看 profile 的时候不要只盯着单个算子耗时要额外关注同步点和数据搬运路径。5.3 几招独家避坑技巧上面这些经验虽然零散但浓缩下来有几个实际操作中特别管用的习惯导出 ONNX 时尽量把动态轴固定住。NPU 编译器在静态 shape 下能做的图优化比动态 shape 下多得多性能差距经常直接翻倍。如果服务需要动态 batch宁可一次开多个固定 shape 的 engine也不要全程动态。只要条件允许就做 INT8 量化并多做几个量化校准集。很多人对量化有心理阴影但实际跑下来用 500 张有代表性的校准图做 PTQ绝大多数视觉模型能保住 99% 以上的精度。省下的内存和带宽对 NPU 推理是质的提升。每个算子变体都要跑一遍 profile不要凭直觉判断。有一次我凭经验认定 Softmax 融合进 Attention 一定更快结果实测在新版编译器中融合后的实现反而因为改变了数据布局让下游 GEMM 的效率降低了 15%。所以那句话还是对实测为主经验为辅。整理一份框架版本和工具链版本的映射表。NPU 工具链迭代快同样的模型用新版本编译器编译后性能和精度可能都有变化。升级工具链时必须先做回归测试不能只验证精度。写在最后从算子出发看 Infra是一条值得长期走的路径我自己最大的一个体会是把注意力放在“算子”这一层能帮你把模型、框架、硬件三者打通。模型上的一次结构改动最终要落脚到一个算子序列框架层的一次性能优化最终也体现在某个算子的执行效率上硬件的每一次架构迭代本质上是在调整各类算子执行的成本模型。所以无论你是做算法、做引擎还是做芯片算子都是一个共同的“会话层”。最后顺手分享一个可以在日常工作中直接用的习惯拿到任何不熟悉的推理栈第一件事不是跑通模型而是先把一个单算子比如矩阵乘或卷积的耗时测出来再对照理论峰值算效率。这个数字就是你这套 Infra 的“天花板水位线”后面无论做多少优化都是在往这个上限上靠。尽早摸清它能帮你省掉大量走弯路的时间。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →