端侧语音AI实战,SenseVoice移植到sophon TPU 全记录
本文详细记录了将阿里达摩院开源的SenseVoice Small语音识别模型移植到国产Sophon BM1684X TPU芯片的全过程。
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
更多推荐

所有评论(0)