SenseVoice 移植到 Sophon BM1684X 实战:RTF 0.0095,105 倍实时

这是端侧语音 AI 实战系列的第 2 篇。第 1 篇聊的是架构取舍,这篇是纯手艺:把 SenseVoice Small 从 PyTorch 一路搬到 Sophon BM1684X 这颗国产 TPU 上跑起来,完整流程、关键决策、性能数据和踩过的坑都在这。

关键结果先放:F16 精度下,RTF 0.0095,约 105 倍实时,识别结果和 F32 完全一致。


0. 为什么写这篇

如果你搜过"SenseVoice 部署到 BM1684X"“TPU-MLIR 转 bmodel”“Sophon 跑语音识别”,大概率会发现——资料少得可怜。算能(Sophon)这条国产 TPU 路线在边缘/盒子/工控场景用得不少,但中文社区里把主流 AI 模型真正移植上去、还带实测数据的踩坑记录,几乎是空白。

我最近把 SenseVoice Small 完整移植到了 BM1684X,跑通了从 PyTorch 导出到 aarch64 板卡部署的全链路。这篇就把过程记下来,既给同样在啃这条路的人省点时间,也算给自己留个档。


1. 先认识一下 SenseVoice

SenseVoice Small 是阿里达摩院开源的多语种语音识别模型,它有几个特点特别适合端侧:

  • 单次前向 + CTC 解码,没有自回归循环。这点至关重要——它不像 Whisper 那样要一个 token 一个 token 地解码,而是一把前向出结果。所以它天生就快,非常适合追求低延迟的端侧场景。
  • 自动语种识别:中 / 英 / 粤 / 日 / 韩,不用你指定语种,模型自己判断。
  • 同时输出情感事件标签(中性/开心/生气、语音/音乐/掌声等)。

它的输入输出形状是这样的:

  • 输入:音频特征 [1, 166, 560](Fbank-80 + LFR-7,对应最长约 10s 音频)
  • 输出:logits [1, 170, 25055](前 4 帧是 prompt,后 166 帧才是识别结果)

记住"166 帧 ≈ 10s""无自回归"这两点,后面的设计决策都跟它们有关。


2. 整条移植链路

在这里插入图片描述

Sophon 的非 LLM 模型移植,标准路径是这样:

PyTorch (funasr 加载)
   │  export_onnx.py        【开发机:WSL/Linux x86】
   ▼
ONNX (simplify 后)
   │  gen_bmodel.sh         【TPU-MLIR Docker 内:sophgo/tpuc_dev】
   ▼
BModel (.bmodel)
   │  build.sh 交叉编译      【aarch64 交叉编译 Docker】
   ▼
C++ 推理程序 → scp 到板卡 → 运行

四步:导出 ONNX → 转 bmodel → 交叉编译 C++ → 部署板卡。下面逐步说,重点讲每步的坑。


3. Step 1:PyTorch → ONNX

用 funasr 加载 SenseVoiceSmall,首次运行会自动从 ModelScope 拉权重,导出成 ONNX 再用 onnxsim 化简。

cd sensevoice/python
python export_onnx.py
# 产物:models/onnx/sensevoice_small_sim.onnx

这一步本身不难,但有两个点要注意:

(1)固定输入形状。 模型固定吃 166 帧(约 10s),所以导出时就要把输入固定成 [1, 166, 560]。超过 10s 的音频要截断,不足的补零。端侧模型大多走静态 shape,这和云端动态 batch 的思路完全不同——静态 shape 是后面 TPU 编译能不能高效跑的前提

(2)那 4 个 prompt 帧别去动它。 SenseVoice 的语种识别,靠的是输入里前 4 个"可学习的 prompt 向量"。很多人会想"我知道是中文,能不能把 language_id 喂进去省点事"——实际上 forward 内部根本不使用外部传入的 language_id,语种完全由模型从音频内容自己判断,结果从输出前 4 帧解码得到。保持原样导出就行,别自作聪明改。


4. Step 2:ONNX → BModel(TPU-MLIR)

这是 Sophon 移植的核心一步,用官方的 TPU-MLIR 工具链,在 sophgo/tpuc_dev Docker 里跑:

docker run --rm \
  -v $(pwd):/workspace \
  -v $(pwd)/0_Toolkits:/toolkits \
  sophgo/tpuc_dev:latest \
  bash /workspace/sensevoice/python/gen_bmodel.sh F16

脚本内部做的就是 TPU-MLIR 的标准两段式:model_transform(ONNX → MLIR)→ model_deploy(MLIR → bmodel),中间指定量化精度。

F32 还是 F16?这是这篇最该记住的一个决策。 我两个都生成了,实测下来:

  • F16 推理速度比 F32 快了将近 8 倍(后面有数据)。
  • 而且 F16 和 F32 的识别结果完全一致——没有任何精度损失。

为什么 F16 能这么稳?因为 SenseVoice 是 CTC 模型,输出是 argmax 取最大概率的 token,对数值精度的容忍度很高;F16 的精度损失不足以改变 argmax 的结果。所以生产环境直接上 F16,没有理由用 F32。

顺带提一句通用经验:不是所有模型都能无脑 F16。带 softmax 温度敏感、或有大词表 argmax 边界的模型,量化前一定要做精度对比验证。SenseVoice 属于"放心上 F16"那一类,但你换个模型就得重新验。


5. Step 3:交叉编译 C++ 推理程序

板卡是 aarch64,开发机是 x86,所以要交叉编译。我用一个 Docker 镜像固化交叉编译环境(免得污染本机):

docker build -t sophon-cross-build docker/   # 只需一次
bash sensevoice/cpp/build.sh

C++ 端的结构大致是:BMRuntime 推理主类 + 音频前端(Fbank+LFR 特征提取)+ tokenizer(CTC 贪心解码)。推理用 Sophon 的 bmruntime API,特征提取依赖 kaldi-native-fbank

这里有个真把我卡住的坑,单独拎出来说 👇


6. ⚠️ 踩坑:kaldi-native-fbank 的双静态库链接

特征提取我用的是 kaldi-native-fbank,需要交叉编译成 aarch64 静态库。编译时第一次链接,报了一堆 FFT 相关的 undefined reference,卡了不短时间。

原因是:kaldi-native-fbank 内部依赖一个 kissfft 做 FFT,它会单独编出一个 libkissfft-float.a 只链接 libkaldi-native-fbank-core.a 是不够的,必须把这两个静态库都链上:

libkaldi-native-fbank-core.a   ← 主库
libkissfft-float.a             ← 内部 FFT 依赖,漏了它就 undefined reference

交叉编译它的关键 cmake 参数(aarch64 静态库,关掉测试和 Python 绑定):

cmake .. -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
         -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
         -DKALDI_NATIVE_FBANK_BUILD_TESTS=OFF \
         -DKALDI_NATIVE_FBANK_BUILD_PYTHON=OFF \
         -DBUILD_SHARED_LIBS=OFF

这种"主库 + 隐藏的内部依赖库"的链接坑,在交叉编译里特别常见,而且报错信息往往不会直接告诉你缺的是 kissfft。记一笔,能省别人半天。


7. Step 4:部署到板卡 & 跑起来

把编译好的二进制、bmodel、tokens.txt 传到板卡(tokens.txt 从 ModelScope 的 iic/SenseVoiceSmall 目录拿),运行:

./sensevoice_bm1684 models/ test.wav F16

输出长这样:

[Timing] audio=5611.5ms  feat=33.7ms  infer=19.5ms  total=53.2ms  RTF=0.0095

--- SenseVoice Result ---
Text     : 对我做了介绍啊,那么我想说的是呢,大家如果对我的研究感兴趣呢。
Language : <|zh|>
Emotion  : <|NEUTRAL|>
Event    : <|Speech|>

一段 5.6 秒的音频,总耗时 53 毫秒。语种、情感、事件全自动识别出来了。


8. 性能实测(BM1684X,约 5.6s 音频)

在这里插入图片描述

统计口径:特征提取 + TPU 推理,不含模型加载(实际部署模型会预加载进内存)。

精度 特征提取 TPU 推理 合计 RTF
F32 ~34ms ~155ms ~189ms 0.034
F16 ~34ms ~20ms ~54ms 0.0095

RTF 0.0095 ≈ 105 倍实时,意思是处理 1 秒音频只花约 9.5 毫秒。这个速度,单板同时跑几十路实时语音流都绰绰有余。

这里有个反直觉的结论,也是这篇最值得玩味的地方:

F16 模式下,特征提取(CPU)占了总耗时的 63%——TPU 推理本身只剩 20ms,反而是跑在 CPU 上的 Fbank 特征提取成了瓶颈。

这说明什么?当你把模型这一块优化到极致(F16 推理只要 20ms)之后,瓶颈会转移到你最容易忽略的 CPU 预处理环节。下一步如果还要压延迟,该优化的不是模型,而是特征提取——比如 SIMD 优化 Fbank、或者把特征提取也搬到 TPU/专用硬件。

这也是端侧工程和"调模型"思维最大的不同:端侧要盯的是整条链路的耗时分布,而不是只盯模型那一块。 模型快到一定程度,瓶颈就跑到别处去了。


9. 小结

把 SenseVoice 移植到 BM1684X,核心就四步,真正的价值在那些"文档不会告诉你"的细节里:

  • 静态 shape、固定 166 帧,是端侧编译高效运行的前提;
  • prompt 帧别动,语种识别靠它,改了就废;
  • F16 直接上,CTC 模型对量化容忍度高,无精度损失还快 8 倍;
  • kaldi-native-fbank 要链两个静态库,漏 kissfft 必报错;
  • 优化到后期,瓶颈在 CPU 特征提取,不在模型。

国产 TPU 这条路资料是少,但跑通之后,这套流程对 Whisper、Paraformer、其它 CTC/编码器模型都是通用的。


这个系列我在持续写端侧语音 AI 的工程实战——模型怎么移植到 Sophon / MTK / RK 这些端侧芯片、怎么压延迟、踩过哪些坑。我在 BM1684X / MTK NeuroPilot 上落地过 Whisper、SenseVoice、ChatTTS、Qwen 等模型的移植与全链路语音 Agent。

如果你也在受限硬件上做端侧 AI,或者有模型上板的需求,欢迎交流。

GitHub:https://github.com/superLin006

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐