自定义MobileNetV2转NBG报错:STM32Cube.AI输出丢失的排查与修复

发布时间:2026/8/30 11:48:31
自定义MobileNetV2转NBG报错:STM32Cube.AI输出丢失的排查与修复 如果你的工作流里也出现了那条报错那你多半已经花了一整个下午在反复重导模型、换量化方式、甚至怀疑是不是开发板坏了。我遇到的情况几乎一模一样同一个 MobileNetV2 架构ST Model Zoo 提供的版本能顺利生成 NBG自己训练导出的版本却在 STM32Cube.AI 里栽了跟头报错只有一句Generation does not contain any output。先说结论问题出在自定义模型的计算图尾部和输入输出的声明方式上不是 NPU 不支持 MobileNetV2也不是工具链版本不稳定。下面把我整个排查链路和最终修复方式完整写出来如果你也卡在 NBG generation 这一步直接按这个思路查能省下不少时间。1. 报错现场换工具链版本和重导模型都救不了1.1 我到底在做什么这是一个典型的 STM32MP257 Edge AI 部署项目。硬件是 STM32MP257 系列的评估板板上集成了 NPU 单元用来跑图像分类推理。我用自定义数据集训练了一个 MobileNetV2输入尺寸 128x128三通道 RGB类别数 10训练框架是 PyTorch。训练完成后导成 ONNX再用 STM32Cube.AI 8.1.0 命令行工具转换成 NBG 格式最后由应用层加载 NBG 文件在 NPU 上执行。项目本身不复杂但卡在模型转换这一步整整耗了两天。1.2 复现命令和报错内容我用的转换命令大致是这样的stm32ai generate \ --name custom_mobilenetv2 \ --model mobilenetv2_custom.onnx \ --input-type float \ --output-type int8 \ --nbg \ --workspace ./output_dir前面一小段日志看着很正常模型导入、形状推断、算子映射都过了但到了“generate”阶段直接报错[INFO] Model loaded successfully [INFO] Inputs: [name: x shape: [1, 3, 128, 128]] [INFO] Outputs: [name: output shape: [1, 10]] [INFO] Optimizing graph... [INFO] Mapping operators to NPU... [ERROR] Generation does not contain any output [ERROR] NBG generation failed注意那行[INFO] Outputs:——工具明明识别到了output张量形状也对却在生成阶段说“没有任何输出”。这说明模型整体解析没问题问题出在工具把输入模型的计算图“规整、优化、算子映射”到内部中间表示时输出节点被丢弃了。1.3 我试过的无效手段遇到这种报错人的第一反应都是先怀疑工具链版本和模型格式兼容性。我依次试了把 STM32Cube.AI 从 8.1.0 升级到 9.0.0报错完全一致在 PyTorch 导出时换opset_version从 11 换到 13、16、17均无效把 ONNX 转成 TFLite 再转换报错变成另一个格式相关错误但依然过不去用onnxsim对模型做简化再试依然失败换一个更小的自定义 CNN 模型能正常生成 NBG说明工具链本身、开发环境、NPU 配置都是好的。最后抱着验证心态把 ST Model Zoo 仓库里的 MobileNetV2 ONNX 文件下载下来原封不动跑同一条命令一次通过。到这里已经能确定问题不是出在 STM32MP257 或 STM32Cube.AI而是我的自定义 MobileNetV2 在导出时留下的计算图结构和 ST 官方模型的交付形态有本质差异。2. 为什么 ST Model Zoo 的 MobileNetV2 能一次通过模型交付形态的差距2.1 ST Model Zoo 模型的“干净”是有意设计的ST Model Zoo 是意法半导体官方维护的模型仓库里面的模型都经过了严格验证可以直接生成 NBG 并在 STM32 系列芯片上部署。它背后的工程约束非常多比如输入张量名有明确约定通常是imagesDtype 为 float32维度固定为[batch, height, width, channels]输出张量名也有明确约定通常是logits或Softmax尾部没有训练时使用的 Dropout、BatchNorm 残差分支、辅助分类头模型不包含被代码作为后处理运算符使用的 ArgMax、TopK 等节点所有算子在 STM32Cube.AI 的算子映射表中都能找到对应实现尺寸完全固定没有动态维度。这些条件看上去简单但正是自定义模型最容易踩坑的地方。2.2 自定义 MobileNetV2 和别人家的 MobileNetV2 差在哪我打开自己的 ONNX 模型和 ST Model Zoo 的 ONNX 模型一对比差异就明显了。最典型的是尾部结构。我的模型尾部是这样的GlobalAveragePooling - Flatten - Linear - Softmax - ArgMax而 ST Model Zoo 的模型尾部是这样的GlobalAveragePooling - Conv 1x1 - Flatten - Linear两个模型在主体结构上几乎一样但自定义模型在输出层后面追加了Softmax和ArgMax。这两个算子尤其是ArgMax本来是为了在 PyTorch 里方便地取分类索引而加的部署时其实完全不需要——分类置信度由应用层事后处理或者直接对 logits 取 argmax 就行。但问题是STM32Cube.AI 在把 ONNX 计算图映射到 NPU 指令时ArgMax这个算子可能不在 NPU 的算子实现列表里于是工具把这些尾部节点标记为“不支持的节点”。如果仅仅是“不支持”工具通常会在日志里明确告警。但这里还有一个关键机制输出识别逻辑。工具会把计算图中所有“没有后继消费者的张量”当作潜在输出。当最后一个节点是ArgMax而这个算子被判定为不可映射、需要裁剪时它后面跟着的输出张量就一起被丢弃了最终输出集合变成空集于是抛出Generation does not contain any output。2.3 输入输出张量名的影响另外一个容易忽略的差异是张量命名。ST Model Zoo 的模型对input_name和output_name有非常明确的规范。而自定义模型导出时如果不显式指定input_names和output_namesPyTorch 默认会给它们取名为x、output之类的通用名字。大多数情况下这不会造成问题但有些工具链版本在输出张量名不是预期约定的情况下会走一套完全不同的解析逻辑增加输出节点被丢弃的概率。所以在整个排查过程中第一步就是把模型导出的input_names和output_names显式写出来尽量贴合 ST 模型库的命名约定。3. Cube.AI 是如何识别“输出”的一个值得理解的处理链路3.1 从 ONNX 文件到 NBG 几大阶段要想彻底搞明白这个报错不能只看表面报错信息得理解 STM32Cube.AI 从输入模型到 NBG 的完整处理链路。大致分为五个阶段模型解析读取 ONNX / TFLite / Keras 模型构建内部计算图。形状推断为每个节点推断输入输出张量的形状静态形状在这里完成绑定。算子映射将 ONNX 算子映射到 STM32Cube.AI 内部的算子实现上这里会生成一个“标准操作图”。图优化与裁剪删除不被支持的算子和多余节点合并可融合的算子比如把Conv BatchNorm ReLU融合成一个算子。量化与代码生成如果目标设备是 NPU会进一步做权重量化、激活量化最终生成 NBG 或者 C 代码。NBG generation 是最后一步它需要拿到非常确定的输入输出信息。只要第四步结束后计算图没有保留任何“输出头”最后一步就会报错。3.2 输出张量的识别机制工具在解析模型时会把计算图里所有没有下游消费者的张量作为候选输出。这个机制本身没有错但问题在于如果这些“候选输出”属于被裁剪掉的节点裁完以后候选列表就空了。提示所谓“没有下游消费者”的张量通常就是网络最后一层的输出。但如果你在模型末端加了Softmax、ArgMax、TopK这类算子工具在算子映射阶段可能会把它们判定为“无法在 NPU 上执行”进而在图优化阶段裁掉。裁掉之后原来的输出张量变成了中间张量新的输出列表就是空的。这个逻辑解释了为什么“工具明明识别到了 output却在 generate 阶段报错”——日志里的 Outputs 信息来自第一步模型解析不代表最终经过优化和裁剪后的结果。3.3 为什么 TFLite 和 ONNX 的行为会有差异我还试过在导出后把模型转成 TFLite当时希望绕开 ONNX 的算子映射问题。TFLite 的解析逻辑和 ONNX 略有不同它已经把计算图“固化”成 TFLite op算子相对更接近部署形态。但问题就在于我的 ONNX 里包含了ArgMax转换到 TFLite 后这个算子依然存在而 NPU 侧对 ArgMax 的支持依然有限。所以结果还是一样——最后仍然失败。反过来看 ST Model Zoo 的模型输出层就干干净净地停在Linear或者Conv之后。工具在优化阶段可以轻松识别出logits是最终输出直接进入量化与生成流程。4. 定位根因Netron 里的一丝丝线索4.1 用 Netron 仔细检查导出模型的尾部结构在我把所有“换版本、换格式”一类的基础排查做完之后我开始认真看 ONNX 模型内部结构。工具用的是 Netron这个工具在模型部署排查时几乎是必备的——它能把 ONNX 计算图完整可视化每个节点、每个张量名、每个维度都一目了然。打开模型后我看到了熟悉的金字塔结构从输入x开始一路卷积、池化、跳跃连接最后是GlobalAveragePooling-Flatten-Linear-Softmax-ArgMax。关键就在ArgMax。它的输入是Softmax的输出张量输出张量直接是模型的输出。但ArgMax的输出是一个整数索引没有概率信息。很多边缘部署工具链对这类“索引类”算子的支持都很弱因为 NPU 的算子加速列表通常只覆盖Conv、Pooling、Add、ReLU、MatMul、Softmax这类标准算子。我在 STM32Cube.AI 的算子支持文档里确认了一下ArgMax在 CPU 侧有实现但对于 NPU 后端生成 NBG 时并不支持。也就是说这个算子不能映射到 NPU 硬件加速指令上工具链在生成 NBG 时只能“跳过”或者“裁剪”它。而一旦裁剪输出就没有了。4.2 下一步把尾部后处理从模型图里拿掉我先把 PyTorch 模型里 forward 部分最后的torch.argmax(...)和torch.softmax(...)移除让模型只输出 logits。然后重新导出import torch model.eval() # 直接使用 model.classifier[3] 作为最终输出不让最后一个维度过 Softmax / ArgMax dummy_input torch.randn(1, 3, 128, 128) torch.onnx.export( model, dummy_input, mobilenetv2_custom_fixed.onnx, input_names[images], output_names[logits], dynamic_axesNone, # 固定所有维度包括 batch opset_version16, do_constant_foldingTrue, )这里有两个细节值得注意。第一output_names[logits]必须显式指定。如果不写PyTorch 会自己给输出取一个output之类的名字。虽然不写有时候也能工作但显式指定可以避免工具链在张量名解析上产生误判。命名本身不影响算子是否被裁剪但会让整个计算图更规范减少出现解析问题的概率。第二尽量把输入尺寸固定死。我的原始模型在导出时dynamic_axes设置了 batch 维度为动态height和width没有设为动态但 STM32Cube.AI 在内部形状推断时对动态维度比较敏感。如果某一个维度的形状是None工具可能在形状推断阶段无法确定输出张量的形状从而认为该输出“不可用”。在部署任务中不需要动态维度直接dynamic_axesNone最稳妥。4.3 修复后重新生成 NBG重新转换stm32ai generate \ --name custom_mobilenetv2_fixed \ --model mobilenetv2_custom_fixed.onnx \ --input-type float \ --output-type int8 \ --nbg \ --workspace ./output_fixed_dir日志显示[INFO] Inputs: [name: images shape: [1, 3, 128, 128]] [INFO] Outputs: [name: logits shape: [1, 10]] [INFO] Optimizing graph... [INFO] Quantizing model with 500 calibration samples... [INFO] NBG generation succeeded问题解决。5. 修复后完整验证NBG 生成成功并部署到板端5.1 确认量化结果和 NPU 运行效果NBG 生成成功只是第一步部署到板端后的推理结果才算数。因为我的模型输出是 logits没有经过 Softmax所以在应用层直接argmax就能得到分类索引跟 PyTorch 里的model(x).argmax(dim1)结果一致。注意如果你的应用层需要概率值不要在模型里重新加 Softmax——那样很可能又触发算子映射问题。正确的做法是取 logits 后在单片机上用 exp 自行计算归一化概率或者直接基于 logits 排序获取 top-k 类别。大多数分类场景下直接对 logits 做 argmax 就够了。在板端验证阶段我把 500 张验证集图片预处理成 128x128 RGB然后加载 NBG 推理。对比结果是NPU 上的 argmax 结果和 PC 上 PyTorch 推理的 argmax 结果完全一致说明量化和转换损失没有影响分类决策。精度和原始 float 模型相比Top-1 下降不到 1%这是 int8 量化下非常正常的表现。5.2 一套更稳妥的导出参数建议如果你也准备用 PyTorch - ONNX - STM32Cube.AI - NBG 这条路径我建议直接参考下面的导出设置配置项推荐值原因opset_version16 或 17算子定义更完备形状推断更稳input_names[images]与 ST 模型库约定一致output_names[logits]避免默认命名造成解析歧义dynamic_axesNone固定所有维度避免形状推断失败do_constant_foldingTrue提前把常量计算折叠减少图中冗余节点尾部算子去掉 Softmax / ArgMax / TopK这些后处理留给应用层模型精度权重 float32工具链量化时自行完成 int8 转换这些配置在 STM32MP257 上验证过可以和 STM32Cube.AI 8.1.0 及后续版本配合使用。6. 这类问题的一个通用排查思路6.1 排查清单遇到Generation does not contain any output这类报错不必急着重装工具链按下面这个顺序排查能更高效打开模型看尾部结构用 Netron 检查最后一个可计算操作是什么。如果是Softmax、ArgMax、TopK、Gather这类非标准部署算子优先考虑移除或后置到应用层。检查输入输出张量是否固定形状dynamic_axes对部署工具不友好。全部用静态维度再导出。显式命名输入输出张量避免默认名称带来的解析问题。检查是否有训练残留分支比如 Dropout、辅助分类头没有删干净计算图里有一些死分支。用工具链的验证命令先跑一遍支持性分析STM32Cube.AI 提供了模型分析和性能预估功能在生成 NBG 之前先跑一圈可以提前发现不支持算子而不是等到最后一步报错。对照官方 Model Zoo 的模型文件下载同结构模型用同一命令转换验证工具链环境本身没有故障。这个对比往往是定位问题的突破口。6.2 一个额外技巧先用小模型跑通全流程我在这次排查里还有一个比较有用的习惯先用一个结构简单的 2 层 CNN 跑通“自定义模型 - ONNX - NBG - 板上推理”整条链路确认工具链环境没问题再引入复杂模型。这样可以让“环境问题”和“模型问题”分离。如果小模型能生成 NBG而 MobileNetV2 不能那问题一定出在模型导出侧。6.3 为什么不建议在模型里保留后处理算子很多从 PyTorch 迁移到边缘部署的同学习惯在模型文件里保留Softmax、ArgMax觉得这样部署时不需要额外处理。这个想法在 CPU 部署时问题不大但在 NPU 部署场景下是反模式。原因有三NPU 的算子加速列表通常有限后处理算子大概率落在“不支持列表”中后处理算子如果被工具链裁剪可能导致输出张量意外的丢失在单片机上自己算 Softmax 或者直接从 logits 取 argmax成本很低不需要为此让模型文件变得复杂。所以我的建议是模型文件里只保留“骨干特征提取 分类器”所有部署后处理放到应用层。这也是 ST Model Zoo 模型一直以来的设计思路。6.4 这类问题留下的通用经验把这次排查从头回看一遍最大的收获并不在于“去掉 ArgMax”这一个操作而是一条通用的部署原则模型训练时和部署时的计算图形态是有明确界限的。训练时为了损失函数、指标统计、可视化加在模型末端的各种算子部署时都应该剥掉。模型导出前做一次“部署图精简”—去掉 Softmax、ArgMax、TopK、Dropout、辅助头显式指定输入输出名称固定所有维度—可以避免掉绝大多数边缘端模型转换异常。STM32MP257 这颗芯片的 NPU 部署链路整体还是相当顺滑的只要模型计算图满足工具链的约定从 ONNX 到 NBG 的转换通常就是几分钟的事。希望这篇排查记录能给同样卡在 NBG generation 的人一点参考。