RK3588边缘AI实战:Sherpa-Onnx流式语音识别交叉编译与性能优化
1. 为什么要在RK3588上跑Sherpa-Onnx语音识别
RK3588这颗芯片在边缘计算圈子里热度一直很高,8核CPU(4×A76+4×A55)、6TOPS NPU、Mali-G610 GPU,加上丰富的外设接口,做边缘AI盒子、智能终端、工业网关都很合适。但真要把 语音实时识别 这条链路跑通,很多人第一反应是去找现成的语音模块,比如一些专用的离线语音识别芯片,或者直接调云端API。前者识别率有限、词条固定,后者依赖网络、有延迟、隐私也说不清楚。
Sherpa-Onnx这个项目正好卡在中间:它是k2-fsa团队做的开源语音推理框架,底层用ONNX Runtime做推理引擎,支持流式(streaming)和非流式(non-streaming)两种模式,模型覆盖了Zipformer、Paraformer、Whisper、SenseVoice等主流结构,还能做语音识别、语音合成、说话人识别、关键词唤醒。最关键的是它 不依赖Python运行时 ,C++核心可以直接交叉编译到ARM64,这对RK3588这种嵌入式平台太重要了。
我在一块RK3588的开发板上(8GB内存,Ubuntu 22.04,内核5.10)完整跑了一遍流式中文识别,从交叉编译、模型选型、线程绑定到延迟优化,中间踩了不少坑。这篇文章就把整个链路拆开讲清楚:为什么这么选、每一步在干什么、参数怎么算、哪些地方容易翻车。适合已经在做边缘语音项目、或者准备把语音能力塞进RK3588设备的同学参考。如果你只是想在PC上跑个Demo,那官方文档就够了;但要在资源受限的板子上做到 实时、稳定、低延迟 ,下面这些经验应该能帮你省几天时间。
2. 交叉编译Sherpa-Onnx:工具链选择和依赖处理
2.1 为什么不用板子上直接编译
很多人图省事,直接在RK3588板子上
git clone
然后
cmake
。我试过,8核A76全开编译Sherpa-Onnx大概要40分钟以上,而且ONNX Runtime的编译过程内存峰值能到3GB以上,8GB的板子如果同时跑着桌面环境,很容易OOM被kill。更麻烦的是板子上的编译工具链版本、CMake版本、Python版本都可能和官方脚本预期不一致,报错信息还特别隐晦。
正确做法是在x86主机上做 交叉编译 ,用aarch64-linux-gnu工具链,编出ARM64的二进制和动态库,再拷到板子上。这样编译速度快(主机性能强)、环境可控、还能复用编译缓存。
2.2 工具链和依赖的版本匹配
交叉编译最怕的就是glibc版本不匹配。RK3588板子上的Ubuntu 22.04用的是glibc 2.35,如果你主机上的交叉工具链glibc版本高于这个,编出来的程序在板子上会报
GLIBC_2.xx not found
。我的建议是直接用Linaro或者ARM官方发布的aarch64工具链,版本对齐到glibc 2.35附近。
依赖方面,Sherpa-Onnx核心依赖这几个:
-
ONNX Runtime
:推理引擎,必须交叉编译ARM64版本,或者用官方预编译的
onnxruntime-linux-aarch64包 - kaldi-native-fbank :做fbank特征提取,Sherpa-Onnx内部会拉取
- portaudio (可选):如果要接麦克风实时采集
- libwebsockets (可选):如果要用WebSocket服务模式
我实测下来,最稳的组合是:ONNX Runtime用官方1.16.3的aarch64预编译包,Sherpa-Onnx用v1.10.x的源码,CMake 3.22以上。
2.3 完整的交叉编译命令
先准备工具链环境变量:
export TOOLCHAIN=/opt/gcc-arm-10.3-aarch64
export CC=$TOOLCHAIN/bin/aarch64-none-linux-gnu-gcc
export CXX=$TOOLCHAIN/bin/aarch64-none-linux-gnu-g++
export AR=$TOOLCHAIN/bin/aarch64-none-linux-gnu-ar
export LD=$TOOLCHAIN/bin/aarch64-none-linux-gnu-ld
export STRIP=$TOOLCHAIN/bin/aarch64-none-linux-gnu-strip
然后配置Sherpa-Onnx的CMake:
cmake -B build-aarch64 \
-DCMAKE_TOOLCHAIN_FILE=./toolchains/aarch64-linux-gnu.toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON \
-DSHERPA_ONNX_ENABLE_PYTHON=OFF \
-DSHERPA_ONNX_ENABLE_TESTS=OFF \
-DSHERPA_ONNX_ENABLE_CHECK=OFF \
-DSHERPA_ONNX_ENABLE_PORTAUDIO=ON \
-DONNXRUNTIME_DIR=/path/to/onnxruntime-linux-aarch64-1.16.3 \
-DCMAKE_INSTALL_PREFIX=./install-aarch64
cmake --build build-aarch64 -j$(nproc)
cmake --install build-aarch64
这里有几个关键点要说明。
BUILD_SHARED_LIBS=ON
是为了生成动态库,方便后续多个程序复用;
SHERPA_ONNX_ENABLE_PYTHON=OFF
必须关掉,交叉编译Python绑定几乎不可能成功;
ONNXRUNTIME_DIR
指向预编译包,省去自己编ONNX Runtime的麻烦。
注意:如果开了
SHERPA_ONNX_ENABLE_PORTAUDIO=ON,需要先交叉编译PortAudio,否则CMake会报找不到。如果只是做文件识别或者从网络流读取音频,可以先关掉。
2.4 部署到板子上的目录结构
编译完成后,
install-aarch64
目录下会有
bin
、
lib
、
include
三部分。拷到板子上时,我习惯这样组织:
/opt/sherpa-onnx/
├── bin/ # 可执行文件
├── lib/ # 动态库
├── models/ # 模型文件
└── audio/ # 测试音频
动态库路径要加到
LD_LIBRARY_PATH
,或者直接写进
/etc/ld.so.conf.d/sherpa.conf
然后
ldconfig
。我推荐后者,因为开机自启的服务脚本里环境变量不一定生效。
3. 模型选型:流式中文识别到底用哪个
3.1 流式和非流式的本质区别
先说清楚概念,不然后面选型会晕。 流式识别 是指音频一边进来一边出结果,模型每次只看一小段(chunk),输出部分结果,适合实时字幕、语音交互。 非流式识别 是等整段音频结束再一次性识别,精度通常更高,适合录音转写。
Sherpa-Onnx支持的流式模型主要有Zipformer系列和Paraformer流式版。非流式有SenseVoice、Whisper、Paraformer大模型等。RK3588上做实时交互,必须选流式。
3.2 几个候选模型的实测对比
我在RK3588上跑了三个流式中文模型,都是int8量化版本,测试音频是10段日常对话,每段5-15秒:
| 模型 | 参数量 | 模型大小 | 单chunk推理耗时 | 实时率RTF | 字准率 |
|---|---|---|---|---|---|
| Zipformer流式中文 | ~70M | 130MB | 45ms | 0.18 | 92% |
| Paraformer流式中文 | ~220M | 480MB | 130ms | 0.52 | 94% |
| Zipformer-small | ~20M | 40MB | 18ms | 0.07 | 86% |
RTF(Real Time Factor)是推理耗时除以音频时长,小于1才能实时。Zipformer流式中文在RK3588上RTF只有0.18,意味着处理1秒音频只要0.18秒,余量很大。Paraformer精度高一点但RTF到0.52,如果同时跑其他任务就有点紧。Zipformer-small最快但精度掉得明显。
我的选择是
Zipformer流式中文int8版
,在精度和速度之间平衡最好。模型文件包括
encoder.onnx
、
decoder.onnx
、
joiner.onnx
和
tokens.txt
,总共130MB左右,放板子的eMMC里完全没压力。
3.3 模型量化的坑
官方提供的int8量化模型不是所有算子都支持,RK3588的CPU对int8有加速(A76支持dot product指令),但ONNX Runtime在ARM上的int8 kernel覆盖度一般。我实测int8比fp32快大约1.8倍,但精度掉了约1.5个百分点。如果对精度敏感,可以用fp32版本,RTF大概0.3,也还能接受。
提示:不要试图把模型转成RKNN格式跑NPU。Sherpa-Onnx的流式模型是动态shape的,RKNN对动态shape支持很差,转过去要么失败要么精度崩掉。老老实实用CPU跑,RK3588的A76性能足够。
4. 实时音频采集与推理管线的搭建
4.1 音频采集的参数选择
实时识别对音频采集有硬性要求: 采样率16000Hz、单声道、16bit PCM 。这是Sherpa-Onnx特征提取的输入要求,采集端必须对齐,否则要么识别不出来,要么需要重采样增加延迟。
RK3588板子上常见的音频输入有几种:板载ES8388 codec接模拟麦克风、USB麦克风、I2S数字麦克风阵列。我用的是ES8388,通过ALSA采集。采集命令可以用
arecord
测试:
arecord -D hw:0,0 -r 16000 -c 1 -f S16_LE -t raw test.pcm
如果板子上有多个声卡,先用
arecord -l
确认设备号。ES8388在RK3588上通常注册为card 0。
4.2 采集和推理的线程模型
这是整个管线最关键的设计。如果采集和推理在同一个线程里串行做,采集会阻塞推理,推理也会阻塞采集,延迟会累积。正确做法是 生产者-消费者模型 :
- 采集线程:从ALSA读固定大小的音频块(比如每次读320个采样点,即20ms),塞进环形缓冲区
- 推理线程:从环形缓冲区取数据,喂给Sherpa-Onnx的流式识别器,拿到部分结果就输出
Sherpa-Onnx的C++ API里,
OnlineRecognizer
配合
OnlineStream
就是为这种模式设计的。每次
AcceptWaveform
传入一小段音频,然后
Decode
,如果
IsReady
就取结果。
4.3 环形缓冲区的实现要点
环形缓冲区(ring buffer)的大小要算清楚。假设采集20ms一块,推理每块耗时45ms,那推理比采集慢,缓冲区会慢慢涨满。但实际上Zipformer流式模型每次
Decode
不是处理20ms,而是处理一个chunk(通常左右各看几帧,实际感受野约几百毫秒),所以推理耗时是按chunk算的,不是按采集块算。
我的做法是缓冲区开2秒容量(32000个采样点),采集线程写,推理线程读。如果缓冲区满了,说明推理跟不上,这时候要么丢帧要么报警。实测Zipformer流式在RK3588上不会满,但Paraformer就有风险。
4.4 端点检测(VAD)的接入
纯流式识别有个问题:说话人停顿的时候,识别器还在输出,会把静音也当成音频处理,浪费算力,还会产生无意义的中间结果。所以需要**VAD(语音活动检测)**来做端点判断。
Sherpa-Onnx内置了基于能量和基于Silero VAD两种方案。我推荐用Silero VAD,它是一个小型的ONNX模型,对噪声环境更鲁棒。接入方式是:VAD判断当前是否是语音,只有语音段才喂给识别器,语音段结束后触发一次
Reset
,准备下一句。
VAD的参数有两个关键:
threshold
(语音判定阈值,默认0.5)和
min_silence_duration
(最小静音时长,默认0.5秒)。在嘈杂环境里,threshold可以调到0.6-0.7减少误触发;如果说话人停顿短,min_silence_duration调到0.3秒。
5. RK3588上的性能优化实战
5.1 CPU亲和性绑定
RK3588是big.LITTLE架构,4个A76大核(CPU 4-7)和4个A55小核(CPU 0-3)。推理线程如果被调度到A55上,速度会慢一倍以上。所以必须做 CPU亲和性绑定 ,把推理线程钉在A76上。
#include <pthread.h>
#include <sched.h>
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(6, &cpuset); // 绑定到CPU 6(A76)
CPU_SET(7, &cpuset); // 也可以绑两个大核
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
采集线程可以绑到A55上,因为采集是IO密集型,不需要大核。这样推理和采集互不抢占。
5.2 线程数设置
ONNX Runtime默认会用所有可用核心做并行。但在RK3588上,如果推理线程本身已经绑到A76,再让ONNX Runtime开多线程反而会增加线程切换开销。我的经验是把
intra_op_num_threads
设为2,
inter_op_num_threads
设为1。这样推理用两个A76核心,剩下的核心留给系统和其他任务。
设置方式:
sherpa_onnx::SessionOptions opts;
opts.SetIntraOpNumThreads(2);
opts.SetInterOpNumThreads(1);
opts.SetGraphOptimizationLevel(ORT_ENABLE_ALL);
5.3 内存和缓存优化
RK3588的eMMC读取速度一般,模型加载如果每次都从磁盘读会很慢。我的做法是程序启动时一次性加载模型到内存,常驻不释放。130MB的模型对8GB内存来说不算什么。
另外ONNX Runtime有个
EnableCpuMemArena
选项,默认开启,会预分配内存池减少malloc次数。这个保持默认就好,不要关。
5.4 实测性能数据
优化前后对比(Zipformer流式中文int8,10段测试音频平均):
| 优化项 | 平均RTF | 首字延迟 | CPU占用 |
|---|---|---|---|
| 默认配置 | 0.35 | 620ms | 180% |
| 绑核+线程调优 | 0.18 | 380ms | 95% |
| 再加VAD | 0.15 | 360ms | 80% |
首字延迟从620ms降到360ms,这个提升在语音交互场景里体感很明显。CPU占用从180%降到80%,意味着还有余力跑其他任务。
注意:首字延迟不只是推理耗时,还包括采集缓冲、VAD判定、特征提取等环节。想进一步降低,可以把采集块从20ms减到10ms,但会增加系统调用次数,需要权衡。
6. 踩过的坑和排查思路
6.1 识别结果乱码或全是拼音
第一次跑的时候,识别出来全是拼音字母,没有汉字。排查了半天,发现是
tokens.txt
和模型不匹配。Sherpa-Onnx的Zipformer中文模型,tokens文件里是汉字+拼音混合的,如果用了英文模型的tokens,输出就会错乱。
模型和tokens必须成对使用
,从同一个发布包里拿。
6.2 板子上运行报段错误
交叉编译的程序在板子上跑,一启动就segfault。用
gdb
跟了一下,发现是ONNX Runtime的动态库版本和Sherpa-Onnx编译时链接的不一致。Sherpa-Onnx编译时用的是1.16.3,板子上放的是1.15.0的so,符号对不上。
ONNX Runtime的so必须和编译时完全一致
,包括小版本号。
6.3 音频采集有杂音导致识别率下降
ES8388的默认增益设置偏高,采集到的音频有削波。用
alsamixer
把Capture增益从默认的30降到18,识别率明显提升。另外板子的模拟地和数字地如果处理不好,会有底噪,这个只能从硬件上解决,软件端可以加个简单的高通滤波。
6.4 长时间运行内存缓慢增长
跑了几个小时之后,内存占用从200MB涨到800MB。查下来是每次识别结束没有正确释放
OnlineStream
对象。Sherpa-Onnx的C++ API里,
CreateStream
之后必须
DeleteStream
,否则会泄漏。改成RAII封装或者用智能指针管理就好了。
6.5 VAD在噪声环境下频繁误触发
工厂环境里背景噪声大,Silero VAD默认阈值0.5会频繁判定为语音。把阈值提到0.65,同时把
min_silence_duration
从0.5秒降到0.3秒,误触发减少了很多。如果噪声特别大,可以先做一次谱减法降噪再送VAD。
7. 一些可以继续挖的方向
Sherpa-Onnx在RK3588上跑通流式识别只是第一步。如果要做完整的语音交互,后面还可以接 语音合成 (Sherpa-Onnx也支持VITS、Matcha等TTS模型)和 关键词唤醒 。TTS在RK3588上跑VITS模型,RTF大概0.4左右,也能实时。
另一个方向是 多路并发 。RK3588有8个核,如果做多路电话录音转写,可以起多个识别实例,每个绑不同的核心。实测4路并发时每路RTF约0.35,还能接受。但要注意内存,每个实例加载一份模型就是130MB,4路就是520MB。
还有就是 模型微调 。如果应用场景有特定术语(比如医疗、法律),可以用自己的数据对Zipformer做微调,再导出ONNX。这块k2-fsa有完整的训练脚本,但训练需要GPU,RK3588只能做推理。
最后分享一个实用技巧:调试阶段可以把识别器的中间结果打印出来,包括每个chunk的输出和置信度。Sherpa-Onnx的结果对象里有
text
和
tokens
,
tokens
里带时间戳,对着时间戳能看出是哪段音频识别错了,方便定位是模型问题还是音频问题。
更多推荐
所有评论(0)