第三章:RV1126BP平台 Sherpa ASR RKNN自主模型转换

[项目复盘报告 V3.0] 自主模型转换:攻克动态ONNX模型至RKNN的技术难题

项目属性内容
项目名称第三部分:RV1126BP平台 Sherpa ASR RKNN自主模型转换
项目周期2025-07-31 ~ 2025-08-09 (估算)
项目用时31.5(约3.9人天)
复盘日期2025-08-20
核心人员Potter White

1. 项目背景与启动

在 V2.0 项目中,我们验证了 sherpa-onnx 框架因目标平台 RKNN SDK 版本过低而无法直接使用 NPU 加速,同时 sherpa-ncnn 的纯 CPU 方案也已达到性能瓶颈。因此,为了充分利用 RV1126b 平台的 NPU 硬件,本项目(V3.0)的核心目标是绕开 sherpa-onnx 的编译限制,自主完成从 ONNX 模型到 RKNN 模型的转换工作

这项工作的起点,是处理 Sherpa ASR 模型中普遍存在的动态输入维度问题。

2. 基础问题分析与通用转换策略

2.1 理解动态维度 [N, ...]

在 ONNX 模型中,维度 N 通常作为一个占位符,代表动态的批处理大小(Batch Size)。例如,一个 [N, 39, 80] 的输入形状意味着模型可以一次性处理 N 个输入样本。然而,为了在嵌入式 NPU 上实现最高效的运算,RKNN 工具链通常要求模型的输入形状是固定的、静态的。

2.2 确定通用技术流程

基于以上分析,我确定了模型转换的通用三步走策略:

  1. 静态化 ONNX 模型: 编写脚本,将原始 ONNX 模型中所有动态维度 N 固定为一个确定的值,通常是 1,表示一次只处理一个输入。
  2. 提取模型元数据: 分析并提取 sherpa-onnx 模型中特有的 custom_string 元数据。该数据包含了如 vocab_size 等推理时必需的参数,需要在转换时提供给 RKNN 工具链。
  3. 执行转换: 使用 rknn-toolkit2 对静态化处理后的 ONNX 模型进行转换。

这个通用流程对于模型结构相对简单的组件是行之有效的。


3. encoderjoiner 模型的标准转换流程

encoderjoiner 模型的内部不包含复杂的动态控制流算子。因此,它们可以严格遵循上述的通用转换策略。我为此设计的自动化脚本执行流程如下:

graph TD
    subgraph "用户操作"
        A[执行转换脚本: ./convert.py encoder]
    end

    subgraph "自动化脚本执行流程"
        B(1.加载原始 encoder.onnx) --> C{2.检查输入维度};
        C -- 发现动态维度 'N' --> D[3.将 'N' 修改为 1];
        D --> E[4.保存为 encoder_fixed.onnx];
        E --> F(5.加载原始 onnx 获取元数据);
        F --> G[6.构造 custom_string];
        G & E --> H(7.调用 rknn-toolkit2);
        H --> I[8.生成 encoder.rknn];
    end

    A --> B
    I --> J((成功))

流程说明:

  1. 脚本首先加载原始的 encoder.onnx 模型。
  2. 通过编程方式检查其输入张量的维度,识别出动态维度 N
  3. N 修改为静态值 1,生成一个中间的 _fixed.onnx 文件。
  4. 同时,脚本通过 onnxruntime 读取原始模型的元数据,并将其格式化为 RKNN 所需的 custom_string
  5. 最后,将静态化的模型和元数据字符串一同送入 rknn-toolkit2,顺利完成转换,生成最终的 encoder.rknn 文件。

对于 joiner 模型,其转换流程与 encoder 完全一致。然而,当此标准流程应用于 decoder 模型时,遭遇了严重的转换失败。


4. decoder 模型的特殊挑战与深度调试

decoder 模型因其内部包含基于输入形状的动态控制流(If 算子),导致标准转换流程失效,需要进行额外的、更复杂的预处理。

4.1 初始尝试的失败与问题定位

我首先尝试了两种直接的方法,但均以失败告终,这帮助我精准地揭示了问题的根源。

  • 尝试 1 - 直接转换动态模型: rknn-toolkit2 在加载模型阶段直接报错,明确指出不支持动态输入维度 N
  • 尝试 2 - 转换时指定输入尺寸: 通过 rknn.load_onnx 的参数强制指定输入尺寸,模型加载成功,但在构建(rknn.build)阶段报错 All outputs ['decoder_out'] of model are constants

4.2 核心难题:常量折叠优化引发的逻辑失效

为了解决“All outputs are constants”这一核心错误,我将注意力转向了对 ONNX 模型本身的预处理。

  • 操作: 我首先执行了标准流程的第一步,将 decoder.onnx 的动态输入维度 N 固定为 1,生成了 decoder_fixed.onnx
  • 现象: 使用这个静态化模型进行转换,复现了与“尝试2”完全相同的错误。
  • 根本原因分析:
    1. 动态控制流: 通过 Netron 可视化工具分析 decoder 模型,我发现其内部存在一个 If 算子。这个 If 算子的判断条件,依赖于前面 ShapeGather 算子所计算出的某个中间张量的形状。
    2. 逻辑固化: 在原始动态模型中,这个形状是可变的,因此 If 分支的走向是不确定的。但当我将输入维度 N 固定为 1 后,这个依赖于输入形状的判断条件也随之变成了一个常量(永远为 True 或永远为 False)。
    3. 过度优化: RKNN 工具链在 build 过程中执行了名为“常量折叠”(fold_constant)的优化。当它检测到 If 算子的条件永远不变时,便会“智能”地剪除掉那个永远不会被执行的分支。在 decoder 模型中,这种剪枝进一步触发了连锁反应,导致从 Shape_7Gemm_15 的一整条计算链路被移除,最终使得模型的输出 decoder_out 被错误地判定为一个与输入无关的常量值,从而抛出“模型无效”的错误。

4.3 最终解决方案:引入专业模型简化工具

问题的根源在于如何处理被固化的 If 算子。在尝试了禁用优化和手动修改计算图(均告失败)后,我确定了最终的解决方案。

  • 最终策略: 在标准流程的基础上,增加一个关键的“模型简化”步骤。我引入了专业的 Python 库 onnx-simplifier
  • onnx-simplifier 的作用: 该工具能够正确地执行常量折叠。它会自动评估 If 算子的条件,安全地剪除静态分支,并且最重要的是,能够保证最终生成的简化后 ONNX 模型在拓扑结构上是完全合法的

4.4 decoder 模型的最终转换流程

基于上述分析,我为 decoder 模型设计了专有的、更为鲁棒的四步转换流程,并固化到自动化脚本中。

graph TD
    subgraph "用户操作"
        A[执行转换脚本: ./convert.py decoder]
    end

    subgraph "自动化脚本执行流程 (针对Decoder的特殊处理)"
        B(1.加载原始 decoder.onnx) --> C{2.检查输入维度};
        C -- 发现动态维度 'N' --> D[3.将 'N' 修改为 1];
        D --> E[4.保存为 decoder_fixed.onnx];
        E --> F(5.**调用 onnx-simplifier**);
        F --> G[6.**剪除静态分支并重新排序图**];
        G --> H[7.保存为 decoder_simplified.onnx];
        H --> I(8.加载原始 onnx 获取元数据);
        I --> J[9.构造 custom_string];
        J & H --> K(10.调用 rknn-toolkit2);
        K --> L[11.生成 decoder.rknn];
    end

    A --> B
    L --> M((成功))

流程说明:encoder 的标准流程相比,decoder 的流程在第5步增加了一个至关重要的环节:使用 onnx-simplifier 对已静态化的 _fixed.onnx 模型进行深度优化。这一步不仅安全地移除了导致问题的 If 算子,还确保了输出的 _simplified.onnx 是一个计算图完整、拓扑正确的合法模型。最终,这个被完美“净化”过的模型可以被 rknn-toolkit2 毫无障碍地转换。


5. 项目成果与结论

5.1 主要成果

  1. 成功打通了包含动态控制流的复杂 ONNX 模型到 RKNN 模型的转换链路。
  2. 建立了一套差异化的、标准化的模型转换流程: 为简单模型设计了标准流程,为复杂模型(如decoder)设计了包含额外简化步骤的增强流程。
  3. 产出了一套可复用的自动化模型转换脚本 (unified_onnx_to_rknn_converter.py)。 该脚本能够智能识别模型类型,并自动应用相应的转换流程,实现了对所有模型组件的“一键式”转换。

5.2 结论

本次技术攻坚证明,处理带有动态控制流(如 If 算子)的模型向嵌入式平台转换时,核心挑战在于如何弥合动态模型与静态硬件要求之间的差距。简单的尺寸固定化往往会破坏模型内部的计算逻辑,导致转换工具的优化过程出现误判。在这种情况下,引入专业的、经过验证的模型优化工具(如 onnx-simplifier),在送入硬件厂商的工具链之前对模型进行预处理和净化,是比尝试手动修改计算图或调整转换工具参数更可靠、更高效的解决方案。

此阶段的成功,为后续在 RV1126b 平台上开发基于 NPU 的高性能推理引擎奠定了坚实的基础。