BERT-ONNX-CPU 方案

BERT 分类模型导出为 ONNX 后,直接用 ONNX Runtime 在 CPU 上推理,不需要转 RKNN,不需要 GPU/NPU。


BERT 分类模型 ONNX 转 RKNN 全流程实录

一句话总结

训练好的 BERT 模型导出为 ONNX 后,直接用 ONNX Runtime CPU 推理即可,不需要转 RKNN。 RKNN NPU 无法正确处理 BERT embedding 层(整数 token ID 查表),经过 6 轮调试后放弃 NPU 方案。最终在香橙派 ARM CPU 上实现:置信度 92.26%,推理耗时 111ms,已集成到 HXXedge v1.0.1。


整体流程概览

Colab 训练 (bert-base-chinese)
    ↓ HF 格式(PyTorch)
Colab 导出 ONNX
    ↓ 检查 IsNaN(仅报告,不修改)→ ONNX Runtime 验证 → 记录基准 logits
    ↓ ✅ 验证通过后才下载
下载 ONNX + tokenizer + categories
    ↓
龘思 Docker 转 RKNN(rknn-toolkit2 == 2.3.2)
    ↓ 无 mean/std 预处理、optimization_level=0、disable_rules
    ↓ .rknn 文件
上传香橙派
    ↓
rknn-toolkit-lite2 推理验证 → 对比 logits 是否接近基准

第 1 步:Colab 训练(已完成)

详见 colab_train.py


第 2 步:Colab 导出 ONNX(关键步骤)

为什么要强调"在 Colab 上先验证"

教训:ONNX 导出后如果不验证就下载,到了 Docker / 香橙派上才发现模型有问题,每次排查都要走完整链路(Colab → 下载 → Docker 转换 → 上传香橙派 → 测试),极其耗时。

正确做法:在 Colab 中完成以下全部验证,并记录原始 logits 作为基准,确认无误后再下载:

  1. ✅ 导出 ONNX(不做任何图修改)
  2. ✅ ONNX Runtime 推理验证(结果正确)
  3. ✅ 记录原始 logits 值和置信度(用于与 RKNN 对比)

IsNaN 算子问题(核心坑点与策略反转)

现象:PyTorch 导出 BERT 时,ONNX 图中包含 12 个 IsNaN 算子。RKNN 加载后输出警告 Unsupport CPU op: IsNaN in this librknnrt.so。

根因:PyTorch 2.x 的 ONNX 导出器对 BERT 的 LayerNormalization 分解时,额外生成了 NaN 安全检查:

计算结果 x → IsNaN(x) → Where(is_nan, default_val, x) → 最终输出

RKNN 运行时(librknnrt 2.3.2)不支持 IsNaN 算子。

改 opset_version 有用吗?

opset_version 结果
14 ❌ 版本转换失败,产生 IsNaN
17 ❌ PyTorch 自动升到 18 再降回 17,仍产生 IsNaN
18 ❌ 同样有 IsNaN

结论:改 opset_version 无效,IsNaN 是 PyTorch 导出器固定生成的。

❌ 失败方案:IsNaN + Where 替换为 Identity

曾尝试的方案:将 Where(IsNaN(x), default, x) 替换为 Identity(x),跳过 NaN 检查。

为什么失败:替换严重破坏了模型精度!

指标 ONNX(Colab 基准) RKNN(IsNaN→Identity 替换后)
logits[0]("你们老板是谁") 5.2765 -0.4722
logits 范围 [-1.13, 5.28] [-0.85, 1.01]
argmax 0(领导团队)✅ 6(产品体系)❌
softmax 置信度 92.26% 13.62%(≈随机)

诊断过程: 1. 全零输入 vs 正常输入 → 输出不同 ✅ 说明模型在读数据 2. 不同问题 → 输出不同 ✅ 但 logits 太平坦 3. 对比 Colab 基准 logits → 差距巨大,说明 IsNaN→Identity 替换破坏了计算图

根因分析:IsNaN+Where 虽然理论上是"防御性检查"(正常输入不会产生 NaN),但替换后 onnxsim 简化会进一步重组图结构,导致计算精度严重退化。

✅ 最终方案:不修改 ONNX 图

策略:保留 IsNaN 算子,不做任何替换,不做 onnxsim 简化
RKNN 转换时:IsNaN 在 CPU 上执行(有警告但不影响结果)

关键认识:RKNN 的 "Unsupport CPU op: IsNaN" 是警告,不是错误。RKNN 运行时可以在 CPU 上执行这些不支持的算子,只是速度稍慢。模型精度不受影响。

Colab 验证通过标准

运行导出脚本后,确认以下全部通过:

⚠️ 检测到 12 个 IsNaN 算子(不做替换)
✅ ONNX 模型结构正确
✅ ONNX 推理验证通过(预测结果正确)
📊 softmax 置信度: 92.26%(足够尖锐)

重要:必须记录原始 logits 值,后续与 RKNN 推理结果对比。如果 RKNN logits 与 ONNX 基准接近,说明转换成功;如果差距大,说明转换有问题。

完整脚本:export_onnx.py


第 3 步:龘思 Docker 转 RKNN

环境准备

conda activate rkllm
cd /Daas/data/models/rkllm/BERT

目录结构

BERT/
├── export_rknn.py              # 转换脚本
├── bert_classifier_inference.py # 推理脚本(副本)
├── output_onnx/                # ONNX 模型(从 Colab 下载)
│   ├── model.onnx              # ~93 KB(图结构)
│   ├── model.onnx.data         # ~390 MB(权重数据)
│   ├── categories.json
│   ├── tokenizer.json
│   └── tokenizer_config.json
└── output_rknn/                # RKNN 输出
    ├── bert_classifier_w8a8_rk3588.rknn
    └── categories.json

转换配置要点

rknn.config(
    # ⚠️ BERT 文本模型不做均值/标准差预处理!
    # 整数 token ID 不能被归一化(否则破坏 embedding 查表)
    target_platform='rk3588',
    optimization_level=0,        # 0=基础(对 BERT 更安全)
    quantized_dtype='w8a8',
    disable_rules=['fuse_mul_into_sdpa'],  # 该规则与 BERT attention 不兼容
)

踩坑记录

错误 原因 修复
dynamic_input [[[1, 64]]] len is 1, expect 2 dynamic_input 格式不对 设为 None(固定形状不需要)
input shape ['batch_size', 'sequence_length'] not support ONNX 用了符号维度名 导出时去掉 dynamic_axes
mean_values ([0]) for input 0 is wrong, expect 64 mean_values 长度不匹配 删除 mean/std(文本模型不需要)
do_quantization=True but dataset is None 量化需要校准数据 先设 False 跑通流程
fuse_mul_into_sdpa TypeError 优化规则与 BERT 不兼容 disable_rules=['fuse_mul_into_sdpa']
Unsupport CPU op: IsNaN + Segfault RKNN 不支持 IsNaN 算子 Colab 端替换 IsNaN→Where 为 Identity(不做 onnxsim)
IsNaN 替换 + onnxsim 后精度崩塌 92%→14% onnxsim 简化重组图结构 只做 IsNaN 替换,跳过 onnxsim

完整脚本:export_rknn.py(Docker 端)


第 4 步:香橙派 NPU 推理

环境

关键区别

工具 用途 导入方式
rknn-toolkit2 模型转换(Docker) from rknn.api import RKNN
rknn-toolkit-lite2 模型推理(香橙派) from rknnlite.api import RKNNLite

部署文件

/home/orangepi/rkllm/rknn-toolkit2/2.3.2/bert_classifier/
├── bert_classifier_w8a8_rk3588.rknn   # RKNN 模型
├── categories.json                     # 类别定义
├── tokenizer.json                      # 分词器
├── tokenizer_config.json               # 分词器配置
└── bert_classifier_inference.py        # 推理脚本

推理脚本核心逻辑

from rknnlite.api import RKNNLite
from tokenizers import Tokenizer

# 加载
rknn = RKNNLite()
rknn.load_rknn('bert_classifier_w8a8_rk3588.rknn')
rknn.init_runtime()

# 推理
encoding = tokenizer.encode("你们老板是谁")
input_ids = np.array([encoding.ids], dtype=np.int32)
attention_mask = np.array([encoding.attention_mask], dtype=np.int32)
outputs = rknn.inference(inputs=[input_ids, attention_mask])

# 解析
pred_label = outputs[0][0].argmax()
answer = categories[str(pred_label)]['answer']

完整脚本:bert_classifier_inference.py


调试迭代记录(5 轮)

从 ONNX 导出到香橙派推理成功,经历了 5 轮完整的「Colab 导出 → 下载 → Docker 转换 → 上传香橙派 → 测试」循环。每轮至少 10-15 分钟,教训深刻。

轮次 问题现象 根因 修复 耗时
第 1 轮 dynamic_input len is 1, expect 2 dynamic_input 格式 [[[1,64]]] 不对 设为 None(固定形状不需要) ~15min
第 2 轮 input shape ['batch_size','sequence_length'] not support ONNX 用了符号维度名(dynamic_axes) 导出时去掉 dynamic_axes,用固定整数维度 ~15min
第 3 轮 mean_values ([0]) for input 0 is wrong, expect 64 mean_values 长度不匹配输入维度 改为 [[0]*64](后改为删除) ~15min
第 4 轮 fuse_mul_into_sdpa TypeError RKNN 优化规则与 BERT attention 不兼容 disable_rules=['fuse_mul_into_sdpa'] ~15min
第 5 轮 IsNaN→Identity 替换 + onnxsim 后精度崩塌(92%→14%) onnxsim 简化重组图结构破坏精度 IsNaN 替换保留,跳过 onnxsim ~30min

关键诊断手段:

  1. 基准 logits 对比法:在 Colab 用 ONNX Runtime 记录原始 logits,与 RKNN 推理结果对比
  2. ONNX 基准:[5.28, -0.15, 0.89, ...],置信度 92.26%
  3. RKNN 退化:[-0.47, 0.90, -0.59, ...],置信度 13.62%(≈随机)

  4. 输入敏感性测试:传入全零输入 vs 正常输入 vs 不同输入,确认模型是否真正读取数据

  5. 结果:三种输入产生不同输出 ✅ 说明模型在计算,但精度退化

  6. 控制变量法:逐步排除 mean/std、optimization_level、IsNaN 替换、onnxsim 等因素


踩坑总结与经验

1. 一定要在 Colab 端完成 ONNX 验证 + 记录基准 logits

不要:导出 ONNX → 下载 → Docker 转 RKNN → 上传香橙派 → 发现不行 → 重来

要:导出 ONNX → ONNX Runtime 验证 → 记录原始 logits → 确认结果正确 → 再下载

每次"重来"的成本是:下载(几分钟)+ Docker 转换(几分钟)+ 上传 + 测试,至少 10-15 分钟。5 轮下来超过 2 小时。

基准 logits 对比法:如果 RKNN 推理结果异常,先对比 RKNN logits 与 Colab 基准 logits。差距大说明转换有问题,差距小说明是后处理问题。

2. IsNaN 必须替换,但绝不能做 onnxsim

PyTorch 2.x 的 ONNX 导出器对 BERT 固定会生成 12 个 IsNaN 算子,无论 opset 14/17/18 都一样。

RKNN 运行时不支持 IsNaN:不是警告后忽略,而是直接 Segfault。必须替换。

做法 结果 原因
IsNaN→Identity + onnxsim ❌ 精度崩塌(92%→14%) onnxsim 重组图结构破坏计算
不替换 IsNaN ❌ Segfault librknnrt.so 不支持 IsNaN
IsNaN→Identity,跳过 onnxsim ✅ 正确方案 替换避免崩溃,不做简化保护精度

3. BERT 文本模型不需要 mean/std 预处理

RKNN 的 mean_values / std_values 是为图像模型设计的(像素值归一化)。BERT 的输入是整数 token ID,直接传入 embedding 层查表。设置 mean/std 可能导致 RKNN 对输入做浮点归一化,破坏 embedding 查表。

✅ 正确做法:不设置 mean_values 和 std_values。

4. rknn.config() 的 disable_rules

RKNN 编译器有图优化规则,某些规则与 BERT 的 attention 结构不兼容:

disable_rules=['fuse_mul_into_sdpa']  # 必须禁用,否则 build 时 TypeError

5. rknn-toolkit2 vs rknn-toolkit-lite2

rknn-toolkit2 rknn-toolkit-lite2
用途 模型转换 + 推理 仅推理
运行环境 Docker / PC 香橙派等嵌入式设备
导入 from rknn.api import RKNN from rknnlite.api import RKNNLite
安装 pip 安装(有依赖约束) 设备预装或 pip 安装

文件索引

文件 位置 说明
export_onnx.py extern/attachments/topic/LLMs/BERT/ Colab ONNX 导出(不修改图)
export_rknn.py Daas-v2/data/models/rkllm/BERT/ Docker RKNN 转换
bert_classifier_inference.py Daas-v2/data/models/rkllm/BERT/ 香橙派推理脚本(含诊断)
colab_train.py extern/attachments/topic/LLMs/BERT/ Colab 训练脚本
generate_training_data.py extern/attachments/topic/LLMs/BERT/ 训练数据生成
preset_qa.json extern/attachments/topic/LLMs/BERT/ 预设知识(17 条)
BERT模型详解 notes/人工智能/LLMs/ BERT 原理笔记

当前状态


最终方案:ONNX Runtime CPU(不需要 RKNN)

为什么放弃 RKNN NPU

问题 说明
RKNN 精度崩塌 6 轮调试后 logits 始终为 13.62%(≈随机),与基准 92.26% 差距巨大
根因 RKNN 编译器无法正确处理 BERT embedding 层(整数 token ID 查表 + 浮点计算混合)
结论 BERT 类文本模型不适合走 NPU,CPU 推理完全够用

ONNX Runtime CPU 推理效果

指标 数值
置信度 92.26%(与 Colab 基准一致)
推理耗时 ~111ms(香橙派 ARM CPU)
分类准确率 7/7 测试全部正确

跨平台验证

BERT-ONNX-CPU 方案在两个平台均验证通过:

平台 CPU 架构 BERT 推理 TTFT 结果
香橙派 Orange Pi 5 Plus ARM RK3588 ~110ms ~200ms ✅ 5/5 正确
本地 Windows PC x86 Intel/AMD ~90ms ~100ms ✅ 5/5 正确

本地 Windows 测试用例:

问题 分类 TTFT 结果
你们有哪些核心产品? 产品体系 126ms ✅ 详细回答
你好啊 问候 115ms ✅ 友好回复
你是谁 身份信息 97ms ✅ 正确身份
哈哈 超范围 84ms ✅ 礼貌拒答
请介绍广州啊 超范围 93ms ✅ 礼貌拒答

结论: ONNX Runtime CPU 方案跨平台通用,ARM/x86 都能跑,不需要 GPU/NPU。x86 PC 性能更强,响应更快。

BERT 语义分类 vs 关键词匹配

keywords 字段的作用

categories.json 中的 keywords 字段仅用于训练数据生成,推理时完全不用:

阶段 keywords 用途
训练时 作为"种子问法"生成训练数据变体
推理时 ❌ 完全不使用,只是参考文档

实例对比

用户问:"你们电话是多少啊"(比 keywords 里的"电话"、"联系方式"都长)

方法 结果 原因
关键词匹配 ❌ 无法匹配(没有完全匹配的关键词) 只能匹配"电话"等短词,无法理解完整问句
BERT 语义 ✅ 正确分类为「联系方式」(96% 置信度) 理解整句语义,知道这是在问电话

多问法测试

问法 关键词匹配 BERT 语义
"你们电话是多少啊" ❌ 没有"多少啊"关键词 ✅ 理解是问电话
"能打哪个号码" ❌ 没有"号码"关键词 ✅ 理解是问电话
"联系方式给我" ✅ 有"联系"关键词 ✅ 理解是问电话
"怎么找到你们" ❌ 没有匹配关键词 ✅ 理解是问联系方式

核心优势

特性 关键词匹配 BERT 语义分类
泛化能力 ❌ 只能匹配预设关键词 ✅ 任意问法都能理解
维护成本 需要穷举所有问法 只需少量种子问法训练
抗干扰 ❌ 多一个字就匹配失败 ✅ 忽略无关词汇
推理速度 快(字符串匹配) 较快(~100ms)

BERT vs 大模型(GPT/DeepSeek/Qwen)

在大模型火爆的当下,BERT 仍有独特优势:

维度 BERT 大模型
速度 ~100ms 1-3秒
资源 CPU 即可 需要 GPU
成本 免费 API 按 token 收费
确定性 ✅ 同问同答 ❌ 可能幻觉/不一致
隐私 ✅ 完全离线 ❌ 数据要上传
灵活性 ❌ 只能分类 ✅ 任意问答

BERT 仍有优势的场景

场景 为什么选 BERT
客服 FAQ 问题有限,BERT 更快更稳
边缘设备 香橙派/树莓派跑不动大模型
低延迟要求 100ms vs 秒级
成本敏感 零 API 费用
隐私合规 数据不出本地

大模型不可替代的场景

场景 为什么需要大模型
开放对话 问题无边界
内容生成 写文章、代码
复杂推理 多步逻辑分析
零样本任务 不想训练新模型

最佳实践:组合使用

用户提问
  ↓
BERT 快速分类(100ms)
  ├─ 命中预设 → 直接返回(快、稳、免费)
  └─ 未命中 → 调用大模型(灵活、但慢/贵)

总结: BERT 是"专精快",大模型是"广慢贵"。固定场景 BERT 更优,开放场景大模型更强,组合使用才是最佳方案。

HXXedge v1.0.1 集成架构

用户提问
  ↓
① BERT 分类(ONNX Runtime CPU,~110ms)
  ↓ 置信度 ≥ 70%?
  ├─ 是 → 返回对应答案
  └─ 否 → ② 分级拒答
           ├─ 含公司关键词 → 礼貌拒答(领域内)
           └─ 不含关键词 → 超范围拒答

部署文件

/home/orangepi/hqxbotEdge/hd001e_HXXedge_1.0.1/
├── app.py                    # Flask 入口(只加载 BERT)
├── config.py                 # 配置(BERT 模型路径等)
├── bert_classifier.py        # BERT 分类器模块(ONNX Runtime CPU)
├── routes/main.py            # 二级流水线
└── restart.sh                # 重启脚本

/home/orangepi/rkllm/rknn-toolkit2/2.3.2/bert_classifier/output_onnx/
├── model.onnx                # ~390MB(ONNX 模型)
├── model.onnx.data           # 权重数据
├── categories.json           # 17 类定义
├── tokenizer.json            # 分词器
└── tokenizer_config.json     # 分词器配置

相关笔记