自定义MobileNetV2转NBG报错:STM32Cube.AI输出丢失的排查与修复
2026/8/30 11:48:29 网站建设 项目流程

如果你的工作流里也出现了那条报错,那你多半已经花了一整个下午在反复重导模型、换量化方式、甚至怀疑是不是开发板坏了。我遇到的情况几乎一模一样:同一个 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 系列芯片上部署。它背后的工程约束非常多,比如:

  • 输入张量名有明确约定,通常是images,Dtype 为 float32,维度固定为[batch, height, width, channels]
  • 输出张量名也有明确约定,通常是logitsSoftmax
  • 尾部没有训练时使用的 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

两个模型在主体结构上几乎一样,但自定义模型在输出层后面追加了SoftmaxArgMax。这两个算子,尤其是ArgMax,本来是为了在 PyTorch 里方便地取分类索引而加的,部署时其实完全不需要——分类置信度由应用层事后处理,或者直接对 logits 取 argmax 就行。但问题是,STM32Cube.AI 在把 ONNX 计算图映射到 NPU 指令时,ArgMax这个算子可能不在 NPU 的算子实现列表里,于是工具把这些尾部节点标记为“不支持的节点”。

如果仅仅是“不支持”,工具通常会在日志里明确告警。但这里还有一个关键机制:输出识别逻辑。工具会把计算图中所有“没有后继消费者的张量”当作潜在输出。当最后一个节点是ArgMax,而这个算子被判定为不可映射、需要裁剪时,它后面跟着的输出张量就一起被丢弃了,最终输出集合变成空集,于是抛出Generation does not contain any output

2.3 输入输出张量名的影响

另外一个容易忽略的差异是张量命名。ST Model Zoo 的模型对input_nameoutput_name有非常明确的规范。而自定义模型导出时如果不显式指定input_namesoutput_names,PyTorch 默认会给它们取名为xoutput之类的通用名字。大多数情况下这不会造成问题,但有些工具链版本在输出张量名不是预期约定的情况下,会走一套完全不同的解析逻辑,增加输出节点被丢弃的概率。

所以在整个排查过程中,第一步就是把模型导出的input_namesoutput_names显式写出来,尽量贴合 ST 模型库的命名约定。

3. Cube.AI 是如何识别“输出”的:一个值得理解的处理链路

3.1 从 ONNX 文件到 NBG 几大阶段

要想彻底搞明白这个报错,不能只看表面报错信息,得理解 STM32Cube.AI 从输入模型到 NBG 的完整处理链路。大致分为五个阶段:

  1. 模型解析:读取 ONNX / TFLite / Keras 模型,构建内部计算图。
  2. 形状推断:为每个节点推断输入输出张量的形状,静态形状在这里完成绑定。
  3. 算子映射:将 ONNX 算子映射到 STM32Cube.AI 内部的算子实现上,这里会生成一个“标准操作图”。
  4. 图优化与裁剪:删除不被支持的算子和多余节点,合并可融合的算子,比如把Conv + BatchNorm + ReLU融合成一个算子。
  5. 量化与代码生成:如果目标设备是 NPU,会进一步做权重量化、激活量化,最终生成 NBG 或者 C 代码。

NBG generation 是最后一步,它需要拿到非常确定的输入输出信息。只要第四步结束后,计算图没有保留任何“输出头”,最后一步就会报错。

3.2 输出张量的识别机制

工具在解析模型时,会把计算图里所有没有下游消费者的张量作为候选输出。这个机制本身没有错,但问题在于:如果这些“候选输出”属于被裁剪掉的节点,裁完以后候选列表就空了。

提示:所谓“没有下游消费者”的张量,通常就是网络最后一层的输出。但如果你在模型末端加了SoftmaxArgMaxTopK这类算子,工具在算子映射阶段可能会把它们判定为“无法在 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 的算子加速列表通常只覆盖ConvPoolingAddReLUMatMulSoftmax这类标准算子。

我在 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_axes=None, # 固定所有维度,包括 batch opset_version=16, do_constant_folding=True, )

这里有两个细节值得注意。

第一,output_names=["logits"]必须显式指定。如果不写,PyTorch 会自己给输出取一个output之类的名字。虽然不写有时候也能工作,但显式指定可以避免工具链在张量名解析上产生误判。命名本身不影响算子是否被裁剪,但会让整个计算图更规范,减少出现解析问题的概率。

第二,尽量把输入尺寸固定死。我的原始模型在导出时dynamic_axes设置了 batch 维度为动态,heightwidth没有设为动态,但 STM32Cube.AI 在内部形状推断时,对动态维度比较敏感。如果某一个维度的形状是None,工具可能在形状推断阶段无法确定输出张量的形状,从而认为该输出“不可用”。在部署任务中不需要动态维度,直接dynamic_axes=None最稳妥。

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(dim=1)结果一致。

注意:如果你的应用层需要概率值,不要在模型里重新加 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这类报错,不必急着重装工具链,按下面这个顺序排查能更高效:

  1. 打开模型看尾部结构:用 Netron 检查最后一个可计算操作是什么。如果是SoftmaxArgMaxTopKGather这类非标准部署算子,优先考虑移除或后置到应用层。
  2. 检查输入输出张量是否固定形状dynamic_axes对部署工具不友好。全部用静态维度再导出。
  3. 显式命名输入输出张量:避免默认名称带来的解析问题。
  4. 检查是否有训练残留分支:比如 Dropout、辅助分类头没有删干净,计算图里有一些死分支。
  5. 用工具链的验证命令先跑一遍支持性分析:STM32Cube.AI 提供了模型分析和性能预估功能,在生成 NBG 之前先跑一圈,可以提前发现不支持算子,而不是等到最后一步报错。
  6. 对照官方 Model Zoo 的模型文件:下载同结构模型用同一命令转换,验证工具链环境本身没有故障。这个对比往往是定位问题的突破口。

6.2 一个额外技巧:先用小模型跑通全流程

我在这次排查里还有一个比较有用的习惯:先用一个结构简单的 2 层 CNN 跑通“自定义模型 -> ONNX -> NBG -> 板上推理”整条链路,确认工具链环境没问题,再引入复杂模型。这样可以让“环境问题”和“模型问题”分离。如果小模型能生成 NBG,而 MobileNetV2 不能,那问题一定出在模型导出侧。

6.3 为什么不建议在模型里保留后处理算子

很多从 PyTorch 迁移到边缘部署的同学,习惯在模型文件里保留SoftmaxArgMax,觉得这样部署时不需要额外处理。这个想法在 CPU 部署时问题不大,但在 NPU 部署场景下是反模式。原因有三:

  • NPU 的算子加速列表通常有限,后处理算子大概率落在“不支持列表”中;
  • 后处理算子如果被工具链裁剪,可能导致输出张量意外的丢失;
  • 在单片机上自己算 Softmax 或者直接从 logits 取 argmax,成本很低,不需要为此让模型文件变得复杂。

所以我的建议是:模型文件里只保留“骨干特征提取 + 分类器”,所有部署后处理放到应用层。这也是 ST Model Zoo 模型一直以来的设计思路。

6.4 这类问题留下的通用经验

把这次排查从头回看一遍,最大的收获并不在于“去掉 ArgMax”这一个操作,而是一条通用的部署原则:模型训练时和部署时的计算图形态是有明确界限的。训练时为了损失函数、指标统计、可视化加在模型末端的各种算子,部署时都应该剥掉。模型导出前做一次“部署图精简”—去掉 Softmax、ArgMax、TopK、Dropout、辅助头,显式指定输入输出名称,固定所有维度—可以避免掉绝大多数边缘端模型转换异常。

STM32MP257 这颗芯片的 NPU 部署链路整体还是相当顺滑的,只要模型计算图满足工具链的约定,从 ONNX 到 NBG 的转换通常就是几分钟的事。希望这篇排查记录能给同样卡在 NBG generation 的人一点参考。

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

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

立即咨询