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 里带时间戳,对着时间戳能看出是哪段音频识别错了,方便定位是模型问题还是音频问题。

Logo

邀请您加入社区

更多推荐