做端侧 AI 部署这几年,我遇到过太多类似的场景:模型在服务器上跑得好好的,精度高、速度快,可一旦要搬到板子上,从 SSD 里把模型文件读出来,再交到 NPU 手里去执行,中间就会冒出一堆莫名其妙的坑。慢、精度掉、内存爆、算子不支持,甚至干脆编译不过。其实这些问题的根源,大多不在模型本身,而在“从 SSD 到 NPU”这条路上。模型文件不是直接丢给 NPU 就能跑的,它要经过解析、量化、图优化、编译、加载、内存分配、驱动调度等一系列流程,每一环都可能成为瓶颈或雷区。
这篇文章我就把这条路完整拆开,从模型文件躺在 SSD 上的二进制状态开始,一直到 NPU 真正执行算子、吐出结果为止,中间每一道工序是干什么的、为什么必须做、怎么做才算稳,我都会讲清楚。适合正在做端侧部署的工程师、想把算法搬上嵌入式平台的算法同学,以及所有想搞懂 AI 推理底层逻辑的开发者。读完之后,你至少能回答三个问题:模型部署为什么这么麻烦?NPU 工具链到底在做什么?遇到部署问题该往哪个环节排查?
1. 一条模型文件的“取经路”:从 SSD 到 NPU 的整体流程
先说结论。一个训练好的模型,从躺在 SSD 上的静态文件,变成 NPU 上实时运行的推理任务,至少要经过七个阶段:
- 模型文件解析与计算图读取
- 精度变换(量化/精度校准)
- 算子映射与图优化
- 编译与链接,生成 NPU 可执行格式
- 运行时加载与模型初始化
- 内存分配与 IO 缓冲区设置
- 驱动调度与算子执行
如果你用的是某家厂商的 NPU 工具链,这七个阶段通常会被封装成两个动作:离线编译(在 PC 上完成 1 到 4)和在线加载(在设备上完成 5 到 7)。听起来很简单对吧?但麻烦就麻烦在,每个阶段背后都藏着大量细节,任何一个环节理解不到位,后面排查问题的时候就会像无头苍蝇。
这整个过程,我习惯拿“写代码 → 编译 → 运行”来类比。模型文件相当于源代码,NPU 工具链里的编译器相当于 gcc,而生成的 NPU 可执行文件相当于二进制程序。但 AI 模型编译比普通程序编译复杂得多,因为它不止要处理“代码”,还要处理“数据流”——也就是张量如何在算子和内存之间流动。更麻烦的是,NPU 的硬件架构和 CPU/GPU 完全不同,它不是一个通用的指令执行器,而是由一堆专用计算单元(卷积核、矩阵乘法单元、激活单元等)组成的,所以你的模型必须被“翻译”成它能听懂的方言。
我见过不少第一次接触端侧部署的同学,以为把模型文件拷贝到板子上,调一下推理库的接口就能跑了。实际上一跑就报错,或者跑起来发现 NPU 占用率几乎为零,CPU 在疯狂计算。这就是因为整个部署流程压根没走通,模型根本没被真正编译进 NPU 的执行体系里。下面我把每一阶段掰开揉碎了讲。
2. 第一步:解析模型文件,SSD 上的二进制不是 NPU 能直接认识的
2.1 模型文件到底装了什么
你在 SSD 上看到的模型文件(.pt、.onnx、.pb、.tflite,或者厂商自定义的格式),本质上是一个序列化的数据容器。里面主要装了结构信息、权重参数、元数据:
- 计算图结构:定义了张量怎么在算子之间流转,哪些节点是卷积、哪些是池化、哪些是归一化。
- 权重和偏置:训练得到的参数,占了文件体积的 90% 以上。
- 版本信息和超参数:比如算子集版本(opset)、输入输出的名称和形状。
解析阶段要做的就是把这个二进制文件反序列化,在内存里还原出一张完整的计算图。听起来像读文件?其实比读文件麻烦。因为模型文件里有大量压缩编码、字符串表、序列化协议头,解析器要逐字节校验,遇到损坏文件或者版本不匹配直接拒绝工作。
我踩过最离谱的坑是:从某地方下载了一个预训练模型,在 PC 上用 PyTorch 加载一点问题没有,但拿到 NPU 工具链里转换,第一步就报“protobuf 解析失败”。后来排查是文件传输过程中被截断了,SHA 校验对不上。所以强烈建议:模型文件一定要先校验完整性,再进转换流程。别嫌这步麻烦,能帮你省两小时排查时间。
2.2 解析器要做的不只是“读”
解析器读完文件之后,还要干三件事:拓扑排序、算子归类、属性规范化。拓扑排序是为了确定执行顺序,因为计算图是 DAG(有向无环图),必须先算前驱才能算后继。算子归类是按 NPU 的算子库把图中的节点分类——哪些是 NPU 原生支持的算子,哪些是“看起来能支持但需要转换”的算子,哪些是根本不支持、只能丢给 CPU 的算子。属性规范化则是对算子的参数做统一格式化,比如卷积的 padding 策略在不同框架里有不同表达方式,统一格式之后后续才好处理。
这里有个很容易被忽略的点:输入输出的 shape 是用动态的还是静态的。静态 shape 是编译期就固化的,动态 shape 是运行时才能确定的。大多数 NPU 工具链都倾向静态 shape,因为静态可以让编译器做激进的内存优化和指令调度。你的模型如果带动态 shape,解析阶段通常会有提示——别忽略它,后面图优化阶段会变成大麻烦。
3. 第二步:量化与精度校准,NPU 的“内存洁癖”决定了它钟爱低精度
3.1 为什么非量化不可
训练好的模型,权重大多是 FP32 精度,也就是每个数字占 4 字节。对于服务器 GPU 来说这点内存不算什么,但在端侧 NPU 上,内存和带宽都是稀缺资源。一块中端 NPU 芯片的一级缓存可能只有几百 KB,你不可能把一个 100MB 的 FP32 模型全部塞进去。
更重要的是,很多 NPU 的乘加单元本身就是为 INT8 设计的。做 INT8 乘法比做 FP32 乘法省一半晶体管、快一倍速度、少四分之三的带宽消耗。这就是为什么几乎所有端侧 NPU 工具链都默认“量化到 INT8”。你在工具链里看到的“quantize”选项,本质上是做了一个精度到存储效率的交换:用一点点精度损失,换来 4 倍体积压缩和 2 到 3 倍的推理加速。
这个概念我常用一个类比解释:FP32 像是用 32 位小数记账,精确到分毫,INT8 像是四舍五入记整数账,大额账单没问题,但零碎小钱积少成多就偏了。
3.2 后训练量化与量化感知训练
量化通常分两种路线:
第一种是后训练量化(PTQ):模型训练完之后再量化。方法是准备一批校准数据(通常是几百到几千张代表真实分布的图片),跑一遍前向推理,统计每个激活值的动态范围,然后据此计算每个张量的缩放因子(scale)和零点(zero point)。工具链自动完成,不用改模型代码,但遇到敏感的小模型或者分布极不均衡的数据,精度损失可能比较大。
第二种是量化感知训练(QAT):在训练阶段就模拟量化误差,让模型自己去适应低精度表达。QAT 的效果通常优于 PTQ,尤其是在语义分割、目标检测这类对细节敏感的任务上。但代价是要会改训练代码,还要重新训模型,成本高。
我在实际项目中,默认先试 PTQ,如果精度损失在可接受范围内,就直接用。如果掉点超过 1 到 2 个点,才考虑 QAT。很多初学者一上来就 QAT,浪费时间不说,还不一定有效果。先做后训练量化,拿到精度报告再决策,这才是成本最低的路径。
3.3 校准数据的选择是量化效果的分水岭
这是整个量化阶段最容易被敷衍的环节。我见过有人随便拿 10 张训练集图片做校准,结果量化后模型精度崩得一塌糊涂。校准数据必须:
- 数量足够:一般建议 100 到 1000 张,太少统计出的动态范围不可靠。
- 分布覆盖:跟真实业务场景的数据分布一致。你部署的是户外摄像头场景,就别拿室内标注图做校准。
- 类别均衡:目标检测模型校准数据要覆盖所有类别,别让模型只见过一类目标。
校准数据集的效果可以这样评估:量化后的模型在校准集上的输出分布应当与原模型尽量重叠。工具链通常会生成量化前后输出的相似度指标(比如余弦相似度),我通常要求这个值在 0.99 以上,低于 0.98 就说明量化哪里出了问题,值得深入排查。
4. 第三步:算子映射与图优化,把“通用图”翻译成 NPU 的“方言”
4.1 算子映射:从框架算子到硬件算子
这一步是整个流程里的“翻译官”。你从 PyTorch 导出的 ONNX 模型里,卷积算子叫Conv,但 NPU 里的卷积可能分为Conv2D、DepthwiseConv2D、GroupConv等不同指令,每个指令的硬件行为都不一样。算子映射要做的就是:识别图中的每个算子,按照 NPU 的指令集给它找对应的实现。
映射规则通常写在工具链的“算子支持列表”里。每个算子都有三种状态:原生支持(直接映射到 NPU 指令)、可转换支持(拆成多个 NPU 指令组合实现)、不支持(只能回退到 CPU 执行)。
这里要特别关注“模型理论快不快,就看 CPU 回退多不多”。有些工具链对不支持的算子会静默回退到 CPU,这种回退你肉眼在日志里很难发现,但它会严重拖慢推理速度。正确做法是从工具链生成的调试报告里,逐一核对每个算子的实际映射状态,目标是 NPU 运算占比超过 95%。如果低于 90%,说明这个模型结构跟当前 NPU 的契合度很差,可能要改模型结构,而不是死磕工具链配置。
4.2 图优化:内存复用和算子融合
映射之前或之后,编译器都会对计算图做一系列优化,三个方向最核心:
算子融合:把多个相邻算子合并成一个算子。比如 Conv 后面的 BatchNorm 可以融合进卷积的权重里,省掉一次内存读写。最经典的是 Conv-BN-ReLU 融合成单算子,这在主流 NPU 工具链里基本都是自动完成的。融合后不仅算子数量变少,还避免了中间结果的访存开销。
常量折叠:计算图中有些节点输入全是常量,比如某些 reshape 的 shape 参数是固定的,那编译器可以直接算好,运行时不再执行。
内存复用:NPU 上内存管理比 CPU 严格得多,编译器会分析张量的生命周期,让两个不会同时存活的张量共用同一块内存。这一步如果做得好,能把内存峰值降一半以上。我在一个二值化模型上实测过,开启内存复用后占用从 31MB 降到 17MB,效果很夸张。
4.3 Shape 推导与布局转换
千万别小看这步。NPU 内部处理张量时,用的数据布局和 ONNX 里的标准布局往往不一样。比如很多 NPU 喜欢 NHWC 布局(按像素连续存,适合卷积访存),而 PyTorch 用的是 NCHW。编译器需要在图中插入布局转换算子,而这种转换本身也消耗时间。
所以做端侧部署时,尽量在模型训练或转换阶段就统一布局,减少运行时转换次数。如果你发现模型里的 Transpose 算子特别多,这就是布局不匹配的信号,通常优化空间很大。
5. 第四步:编译与链接,把计算图变成 NPU 指令序列
5.1 编译器到底在做什么
这一步已经进入工具链的“编译后端”。图优化之后,编译器会做指令调度——把每个算子映射成 NPU 执行单元能识别的指令序列,然后排好序。
简单说,NPU 的指令分三类:
- 计算指令:驱动 MAC 阵列做卷积、矩阵乘这类运算。
- 搬运指令:控制 DMA 把数据从片外 DRAM 搬到片上 SRAM,算完之后再搬回去。
- 控制指令:同步、跳转、等待。
编译器排指令序列的时候,要尽量让计算指令和搬运指令重叠——NPU 在算上一块数据的时候,DMA 同时在搬下一块数据,这样计算单元才不会闲置。这个技术在芯片设计里叫“流水线调度”,做得好不好直接影响实际跑分。
5.2 为什么编译一次那么慢
NPU 编译器做事比 CPU 编译器多得多,因为它是面向特定硬件的“专用编译器”,要做很多静态分析:内存生命周期、指令级并行度、DMA 冲突检测、缓存命中优化……再加上它还往往要跑一遍“仿真验证”,在 PC 上模拟 NPU 执行过程,估算性能指标。所以编译一个模型花十几分钟到半小时是很正常的,别急,它在认真干活。
如果你嫌弃编译慢,可以这样优化:尽可能把模型的 shape 定死,不要让编译器去推导大量的动态分支;在设计模型时就分批处理和静态 batchnorm,减少融合优化的工作量;用工具链的增量编译功能,只改某个子图时避免重新编译整图。
5.3 产出物:NPU 可执行文件
编译的最终产出,可能是一个厂商自定义格式的加密二进制文件,比如xxx.nb、xxx.kmodel、xxx.bin之类。这个文件就是 NPU 的“可执行程序”,里面包含了指令代码段、权重常量段、内存分配表、算子元信息等。
这里我特别提醒:不要把生成的模型文件随意跨工具链版本使用。不同版本的工具链生成的格式可能不兼容,轻则加载失败,重则推理结果全是乱码。我在项目中会严格记录每一版模型对应的工具链版本,防止部署环境升级后老模型突然跑不了。
6. 第五步:运行时加载,从“静态文件”到“运行时实例”
这一步开始,就进入设备端了。你的板子启动,SSD 上的模型二进制被读到系统内存,然后 NPU 的运行时库(Runtime)开始干活。
运行时加载要做的事比你想的多:
- 解析可执行文件的头部信息,确认格式合法,校验工具链版本是否匹配。
- 把指令序列和权重按内存分配表加载进 NPU 可访问的内存区域。NPU 一般不能直接访问普通的内存,只能访问特定区域或经过映射的地址空间。
- 建立执行调度上下文,为后续真正的推理执行做好准备。
这个阶段最常见的问题是“运行时版本和编译版本不匹配”。我建议在板子上的运行时库里打全版本日志,至少包括:运行时版本号、NPU 驱动版本、模型目标版本、DDR 配置。这四个信息一旦能对齐,很多兼容问题三分钟就能定位。
7. 第六步:内存分配与模型初始化,一切蓄势待发
7.1 NPU 内存的三级结构
要理解这一阶段,得先知道 NPU 的内存拓扑。一般 NPU 有三层存储:
- 寄存器:最小最快,存单条指令的操作数。
- 片上 SRAM/缓存:速度很快但容量极小(几百 KB 到几 MB),计算前数据必须在这里。
- 片外 DDR:容量大但速度慢,模型参数和中间结果的大头在这里。
所以运行时初始化时最重要的任务就是“三级存储的调度规划”:哪些数据常驻片上、哪些数据按需搬运、哪些数据只能放片外。这些规划在编译期已经做了大半,运行时要负责把这些规划落地,也就是完成物理地址的分配。
7.2 输入输出的 Buffer 分配
模型执行期间,输入数据从摄像头或 CPU 侧来,输出结果要送回 CPU 侧,所以运行时还要分配至少三块缓冲区:
- 输入缓冲区:CPU 侧将预处理好的图像数据写入,然后 DMA 搬运到 NPU。
- 工作缓冲区:NPU 计算过程中存放中间激活值,通常在 NPU 内存域。
- 输出缓冲区:NPU 计算完成后把结果写到这里,CPU 再读取。
内存分配要关注的硬约束是对齐。NPU 的 DMA 控制器通常要求地址按 16 或 32 字节对齐,有些还要求行字节数是某个值的整数倍。代码里经常看到align_up(x, 32)这种操作,就是为了满足硬件的对齐要求。这也是为什么常见图像缩放(resize)后输入宽度不是随便定的,而是会 pad 到某个对齐值。不注意对齐问题的话,推理结果会莫名其妙地错位、花屏。
7.3 上下文与流的创建
很多 NPU 运行时库设计了“上下文(Context)”和“流(Stream)”的概念,类似 CUDA。上下文表示一个独立的执行环境,流表示一组按顺序执行的任务队列。初始化时要创建好这些,推理时只需要提交任务就行。要注意的是,Context 一旦创建会占用不少内存,别每个请求都 create/destroy。正确做法是常驻一个上下文,推理任务进入取出,做完放回池子。我在一个多路视频流项目里,用这种方式把模型初始化时间从 80ms 降到了接近于零,因为上下文只创建了一次。
8. 第七步:推理执行,NPU 真正跑起来的那一刻
8.1 一次推理的完整时序
模型初始化和内存分配都好了,一次真正的推理调用是这样一个流程:
- CPU 侧将预处理后的输入图像写入输入缓冲区(可能还要做一次颜色空间转换、归一化)。
- 调用运行时接口提交推理任务,比如
forward()或run()。 - 运行时将任务封装成一条调度命令,提交给 NPU 驱动。
- 驱动把输入缓冲区地址、工作缓冲区地址、指令起始地址等信息写入 NPU 的特定寄存器或内存队列。
- NPU 通过中断或轮询机制发现新任务,开始解析指令序列。
- DMA 将输入数据从 DDR 搬运到片上 SRAM。
- 计算单元执行算子指令,中间结果在片上流转。
- 最终输出经过 DMA 写回到输出缓冲区。
- NPU 发出完成中断或置位一个状态标记。
- 驱动检测到完成,通知运行时,CPU 从输出缓冲区读取结果。
这个过程看似线性,但高性能场景里会有大量“异步重叠”:CPU 在预处理第 N 帧图像的同时,NPU 可能正在计算第 N-1 帧,DMA 正在搬运第 N+1 帧的数据。这就是流水线式的深度学习推理,也是为什么实际部署性能那么依赖“队列深度”和“多线程协作”。
8.2 同步和异步,你至少要知道的区别
绝大多数 NPU 运行时同时提供同步接口和异步接口。同步接口调用简单,但 CPU 会阻塞等待 NPU 完成,白白浪费了 NPU 干活时 CPU 的空闲时间。异步接口则把任务提交后立即返回,CPU 可以继续做别的事,后面再来查询结果。
我做视频流推理时,几乎一律用异步。因为视频帧是连续到达的,完全可以先提交第 1 帧,趁 NPU 算第 1 帧时做第 2 帧的预处理,这样单帧延迟没降低,但整体吞吐率能提升接近一倍。唯一的代价是要写一点状态同步的代码,核心思想是“帧序号 + 完成回调”,别让 CPU 在同步阻塞上干等。
8.3 Batch Size 和多核调度
Batch Size 不只在训练时有用,推理时同样影响吞吐。很多 NPU 支持多个输入打包成一个 batch 一起计算,因为单次指令调度的开销会被均摊。可以测试 batch 1 和 batch 4 的吞吐差异,通常会有明显提升。
但要注意,增大 batch 会占用更多内存,也会增加单次推理的延迟。如果你的场景对单帧延迟敏感(比如实时控制),用 batch 1;如果更看重整体吞吐(比如离线批量检测),那就加大 batch。这两者常常是拿天平做的交换,没有绝对好坏。
8.4 后处理也不容忽视
NPU 算完之后,CPU 还要承接后处理:比如目标检测的 NMS(非极大值抑制)、语义分割的 argmax、分类任务的 softmax。如果后处理代码写得很烂,推理速度再快也没用。
这里分享一个经验:把输出数据格式对齐到后处理希望的格式,尽量在编译期就确定。比如输出 Layout 是NCHW还是NHWC,channel 数是 4 的倍数吗,这些都是可以在模型编译阶段配置的,千万别等运行时再做大量转置。一条 Transpose 在 NPU 上跑可能只要几微秒,但在 CPU 上做一次 1MB 数据的转置要好几毫秒,差距很大。
9. 常见问题与排查技法,从“零认知”到“一眼定位”
9.1 五类高频问题排查表
| 现象 | 可能环节 | 排查方向 |
|---|---|---|
| 模型转换时直接报错 | 解析/映射 | 检查框架版本、opset 版本、算子是否受支持,开启工具链 Debug 日志看具体是哪个算子 |
| 推理结果全是 0 或乱码 | 内存分配 | 检查输入输出 Buffer 地址是否对齐,DMA 配置是否正常,是否有越界写导致数据被覆盖 |
| 精度掉得很严重 | 量化 | 检查校准数据是否覆盖真实分布,量化相似度指标是否低于 0.98,被回退到 CPU 的算子是否过多 |
| 跑起来 NPU 占用率低,CPU 烧高 | 图优化/映射 | 检查算子落位报告,看哪一层被分配到 CPU 上执行,可能是该算子在 NPU 上无实现 |
| 模型可以跑,但延迟忽高忽低 | 驱动/流水线 | 检查是否是同步接口造成 CPU 阻塞,查看是否有定期内存分配/释放行为,确认缓冲是否常驻复用 |
9.2 排查切入点
开头提到过,部署问题不能只看表面。我处理过的案例里,至少三成“精度问题”最终定位是输入预处理和训练阶段不一致,比如训练时做的是 RGB 均值减法,部署时忘了减;或者训练时图像缩放用双线性,部署时用了最近邻。这一类问题跟 NPU 一点关系没有,纯粹是数据链路的偏差。
所以我会建议按这个顺序排查:
- 先验证原始模型(在 PC 上)精度正常,排除模型本身的问题。
- 再验证未量化模型在 NPU 上的精度,看是否正常,这一步能帮助确认工具链的基础链路是通的。
- 最后验证量化后模型的精度,有差异基本就锁定在量化环节。
- 如果所有精度正常但端到端应用效果差,往预处理和后处理流程上找。
这套排查逻辑看似基础,但真的能帮你节省大量无脑翻日志的时间。
9.3 工具链日志是你最好的朋友
很多工程师遇到问题喜欢上网搜,但端侧 NPU 的工具链绝大多数没有公开社区问答,而且不同芯片厂商的工具链差异巨大。与其临时搜索,不如花一点时间把工具链的日志机制搞清楚。
一般来说,NPU 工具链有多个日志等级:Silent、Error、Warning、Info、Debug、Verbose。项目初始阶段,我强烈建议把日志级别开到 Debug 或 Verbose,虽然日志量大,但你能看到每个算子是怎么映射的、每个优化做了什么、每个警告是从哪里来的。到了调优后期再降级到 Warning 级别,避免性能抖动。
还有一个独门技巧:许多工具链允许你导出中间表示,比如量化前的 ONNX、图优化后的计算图、指令调度前的抽象汇编。每次出问题时,把中间表示导出后跟正常的版本做 diff,往往一眼就能看出编译器在哪一步把计算图改坏了。这个技巧我在排查一个“精度崩坏但没有任何报错”的诡异问题时立了大功。
9.4 内存优化相关的三个细节
内存溢出是端侧部署的高频问题,几点心得:
- 输入输出 Buffers 提前分配、重复使用。不要在推理循环里频繁 malloc/free。现代内存分配器虽然高效,但在嵌入式环境里碎片化问题依然存在。
- 注意模型文件加载到 NPU 可访问内存的地址范围。有些 NPU 不能直接访问所有内存范围,超出范围会导致 DMA 错误。
- 优先使用工具链给出的内存复用方案。很多工具链可以自动做内存池规划,把你频繁使用的 Buffer 全部打包进一个大的内存块,别手动分配零散内存。
10. 性能调优的冷门经验补充
讲完这些,再补几句真正动手部署后才会有体会的东西。
第一个经验是:NPU 强不强,不完全看算力,更看数据搬运的效率。经常有厂商宣传“XX TOPS”,但实际推理就是跑不满。问题常在于模型里的算子太碎,导致 DMA 搬运频繁,计算单元一直等数据。我见过一个模型,算子数量从 400 多个优化到 180 个,编译之后的推理速度直接快了 1.7 倍,期间没有改任何计算逻辑,纯粹是靠图优化把碎片算子融合成大的运算块。
第二个经验是:多参考厂商自带的例程和模型库。很多开发者在接到一块新 NPU 板子时,会想当然地拿自己在 PC 上训练的分支模型直接跑,结果各种碰壁。其实厂商通常提供了适配好、验证过的模型示例库,包括分类、检测、分割等常见任务的现成例程。先跑通例程确认整体链路,再把自定义模型逐步替换上去,这条路径的成功率最高,排错成本最低。我第一次接触某国产 NPU 时,直接拿一个自定义分割模型过去,折腾了两天没跑通;后来老老实实跑通厂商提供的分割例程,再对比差异,半小时就定位到是自己模型的 Resize 输出尺寸不符契约。
第三个经验是:版本管理意识要强。模型的输入尺寸、工具链版本、运行库版本、量化校准集版本都可能影响最终部署效果。我习惯用一个配置文件把这些信息全部固化下来,每次模型发布,模型文件、配置、日志、精度报告一起归档,避免过几个月后要复现时抓瞎。
11. 写在最后的一点个人体会
这条从 SSD 到 NPU 的链路,每个环节都不算难,难的是把它们串在一起之后,出了问题你能准确判断故障发生在哪一段。我自己最开始做部署时,碰到问题第一步就是怀疑工具链有 bug,后来才意识到绝大多数问题出在自己对流程的理解不够——比如没有做对齐、Calibration 数据没选好、CPU 回退的算子过多、同步接口拖慢了流水线。
如果你现在正准备把一个模型搬上 NPU 板子,我建议按照这篇文章的顺序,先确认你的模型文件在这个流程的每个阶段都走通了,再谈优化。不要一上来就追求“看着很厉害的高级配置”,先把完整流程跑通,拿到一组基线数据和精度报告,之后的所有优化都基于这个基线去对比,效率和幸福感都会高很多。这条路上没有银弹,但只要你把每一个环节都吃透,NPU 部署这件事,其实比想象中要可控得多。