跳到主要内容

SL2610 系列上的实时语音识别

· 阅读需 15 分钟
Ye Htet
Ye Htet
Embedded AI @ Synaptics

#synaptics-coralboard #asr #moonshine-v2 #edge-ai

setup

图 1:搭配 USB 麦克风的 Synaptics Coralboard。

自动语音识别(ASR)为各类应用中的智能语音设备提供了支持。得益于 AI 模型的进步以及像 Synaptics Torq NPU 这样的硬件加速,开发者如今可以在低功耗产品中部署实时语音识别。这为用户与产品的交互开启了更加直观的方式。如果您有兴趣将实时语音识别引入自己的边缘 AI 应用,本博客将向您展示具体做法。

演示所需物品:

  1. Astra Machina SL2619 或 Synaptics Coralboard

  2. USB 麦克风(带延长线)

  3. USB 转以太网延长线(用于在开发板上安装软件包)

如果您想直接跳到演示部分,请点击这里

Moonshine V2——现已支持流式处理!

Moonshine 是由 Moonshine AI 开发的一系列面向低延迟转录的语音识别模型(Jeffries 等,2024;Kudlur 等,2026)。他们提供了微型模型,参数量小至 3400 万(约 130MB),即使在浮点精度下也能在 Astra 设备上实时运行。

基础的 Moonshine 模型基于经典的编码器-解码器 Transformer 模型(Vaswani 等,2017)。它由一个编码器和一个解码器组成:编码器审视整段语音以捕捉上下文,并将这一“编码”传递给解码器,由解码器自回归地生成转录结果。随后于 2026 年发布的 Moonshine V2 进行了改进,在模型的编码器部分引入了滑动窗口注意力机制。这意味着我们不再需要等待整段语音结束才开始输出第一个 token,从而缩短了首 token 时间(TTFT),因为编码器现在可以将编码帧“流式”传递给解码器。

SL2610 与 NPU

SL2610 是一款异构 SoC:

  • Arm Cortex-A55 核心(CPU),

  • Mali-G31 GPU,以及

  • Torq T1 NPU(1 TOPS)。

Torq 是 Synaptics 围绕 Google 的 Coral NPU 打造的芯片与编译器技术栈,Coral NPU 是一个开源的 RISC-V NPU 核心,原生支持 AI 工作负载。在设计上,该 NPU 针对 AI 时代普遍存在的矩阵运算进行了优化。

我们将在芯片上运行完整的 ASR 系统,把 Moonshine V2 模型放在 NPU 上。有关我们这样做的原因,请参阅性能提升表格

使用 Torq 准备模型

我们希望把模型放到 NPU 上。想了解更多细节,可以阅读本博客。以下是步骤列表:

  1. 获取 ONNX 模型组件

  2. 使模型静态化

  3. 使用 Torq 工具链生成二进制文件

模型拆分(5 个组件到 2 个组件)

Moonshine Voice 将完整模型拆分为 5 个 ONNX 组件:frontend、encoder、adapter、cross_kv 和 decoder_kv。

components

图 2:Moonshine V2 的 ONNX 模型组件。

组件
frontend有状态的音频前端,接收原始音频块 + 滚动卷积缓冲区,运行带步长的因果卷积堆栈,输出特征嵌入 + 更新后的缓冲区。
encoderTransformer 编码器层(有状态),使用因果/滑动窗口注意力掩码将 frontend 特征转化为带上下文的隐藏状态,从而可以逐块运行。
adapter一个小型投影(pos_emb + proj),将编码器隐藏状态重塑为解码器交叉注意力所期望的形式。
cross_kv每块从编码器隐藏状态中预先计算一次交叉注意力的 K/V 投影,这样解码器就无需在每个生成的 token 处重新计算它们。
decoder_kv自回归解码器:接收上一个 token + 自注意力 KV 缓存 + 预计算的交叉注意力 KV,输出下一个 token 及更新后的自注意力缓存。

对于给定的输入音频块大小,decoder_kv 之前的各组件可以按序工作,因此我们可以通过融合它们来优化拆分。这将减少设备到主机(NPU - CPU)之间的数据传输,使完整模型保持在原位[1]。我们将其称为“编码器”,而把 decoder_kv 简称为“解码器”。

修改模型(ONNX Graphsurgeon)

由于 Torq T1 NPU 支持固定长度的 I/O 和操作,我们需要将动态维度更新为某个固定大小。当前的流水线使用 ONNX 手术工具 onnx_graphsurgeon 来完成此操作。

此外,如果当前版本的 Torq 编译器不支持某些特殊算子(请务必拉取最新更新!它们经常更新),我们可以通过将其分解为更简单的算子来临时支持它们[2]。例如,在 frontend 中,我不得不用多项式来拟合 asinh 算子,而编码器中的 layernorm 由于 MLIR lowering 的问题需要被分解。

VMFB 文件(NPU 的二进制文件)

一旦我们准备好静态的 ONNX 文件,就可以将其传递给 torq-compiler,生成可在 Astra SL2610 上运行的二进制文件。你可以按照该仓库中的文档来“lower”ONNX 模型。首先,我们使用 IREE 工具生成一个优化过的 MLIR 文件,然后将其交给 Torq,由后者输出 .vmfb 二进制文件。

torq-tools 仓库的导出流水线已经自动化了这一过程,我们可以使用以下命令运行完整导出(请务必先阅读 readme 并设置好环境):

torq-export-model moonshine_streaming --chunk-len 1280 --input-seconds 8 --extract-embeddings --export-attention --convert-dtypes

以下是一些有用的参数:

  • --chunk-len(必填,无默认值):馈送给编码器的每个流式块的音频采样数(例如 1280 samples = 80ms @16kHz)。

  • -i/--input-seconds(默认为 5):约束解码器的输入(交叉 KV 缓存)及其自 KV 缓存。换句话说,这限制了静态解码器每次能够关注的总音频长度(上下文)。

  • --extract-embeddings:将解码器的首个输入从整数 token id 切换为浮点 inputs_embeds 张量,把嵌入表查找移到主机端(与单独导出的 decoder_token_embeddings.npy 配对使用)。

  • --convert-dtypes:精度转换。生成 .vmfb 文件时请务必包含此参数。

对于当前的演示,我选择了 8 秒的最大转录长度,编码器每次处理 1280 个采样的音频(80ms)。这应会在以下目录中生成 encoder.vmfbdecoder.vmfb 文件

torq-tools-dev/models/UsefulSensors/moonshine-streaming-tiny/export/torq/converted/static

另请注意在下面的目录中生成的五个文件:adapter_pos_emb.npydecoder_token_embeddings.npystreaming_config.jsontokenizer.jsonconfig.json

torq-tools-dev/models/UsefulSensors/moonshine-streaming-tiny/export/onnx/float/static

运行演示

这里有一个使用 Moonshine V2 模型的简单实时语音转录演示,它改编自 Moonshine Voice 仓库,可完全在 Coralboard 或 Machina 套件上运行。当你对着连接到开发板的麦克风说话时,转录内容会随着你的讲话而更新,我们称之为“预览”,它会随着更多音频的到来而自我更新。正如我们之前提到的,这只有依靠 V2(流式版本)的改进才成为可能——编码器使用滑动窗口注意力将嵌入生成到一个不断增长的缓冲区中(而非每次都重新处理整段音频片段)。

请按照 torq-examples 中以及 Coralboard 上 moonshine_streaming 目录下的 README.md 文件操作,并运行以下命令。

python src/infer.py -m ../models/Synaptics/moonshine-streaming-tiny-torq \
--hw-type astra_machina --device <mic> --vad-silence 8 --vad-threshold 0.010 --preview-every 5

请确保有一个麦克风连接到开发板(我使用了 USB 延长线,让麦克风远离开发板上的嗡嗡声)。你可以通过运行以下命令来查看 --device 编号:

python src/infer.py --list-device

你也可以提供一个预先录制的 WAV 文件来模拟实时转录:

python src/infer.py -m ../models/Synaptics/moonshine-streaming-tiny-torq \
--vad-silence 0.5 --wav ../../librispeech_clean.wav --realtime

demonstration

图 3:转录输出

与真实文本相比,转录结果相当准确:

Yesterday you were trembling for a health that is dear to you. Today you fear for your own. Tomorrow it will be anxiety about money. The day after tomorrow, the diatribe of a slanderer. The day after that, the misfortune of some friend. Then the prevailing weather. Then something that has been broken or lost. Then a pleasure with which your conscience and your vertebral column reproach you. Again, the course of public affairs.

你可能已经注意到,我把 --vad-silence 设为了 0.5 秒,因为这段音频清晰且噪声低,VAD 很容易捕捉到语音。当你用自己的麦克风测试时,请务必相应地调整这个旋钮,因为背景噪声可能会导致你的转录过于频繁地中断。

以下是该算法的工作流程:

pipeline

图 4:实时流式转录流水线。

一个工作线程持续将原始音频推入队列。另一个独立线程将音频重采样到 16kHz,并将其切分为固定的 80ms(1280 采样)片段,然后运行一个轻量级的基于能量的 VAD 来判定当前的语音阶段。如果你正在讲话,它会将该音频块馈送给运行在 NPU 上的编码器。编码器需要一定的前瞻量,因此我们需要丢弃一些预热的编码器输出帧,但每个音频块都会向交叉注意力缓冲区(cross-kv 缓存)追加固定批次的新帧。然后我们每隔一段时间(比如每 15 个块)启动一次解码器,以预览已经说了什么。

设计难题

以下是出现的一些设计问题:

  1. 模型组件要求静态形状:由于解码器需要固定的 cross-kv 输入和 self-kv 缓冲区,我们需要固定话语(utterance)长度。此外,每次解码都会在整段话语上运行。

  2. 主机 CPU 与设备 NPU 之间的内存边界:我们希望限制跨边界的内存传输,因此需要让 cross-kv 和 self-kv 缓存常驻于设备上。

  3. 何时以及如何运行解码器?我们应该从话语开头重新解码整段音频吗?

简单的部分:让 KV 缓存常驻设备

这是一项关键优化。我们需要让数据常驻在 NPU 上,而不是通过缓慢的内存连接来回搬运:

  1. 交叉注意力 KV 缓存在每次解码调用中都是恒定的。

  2. 自注意力 KV 缓存可在解码调用的各次前向传播之间复用。

仅仅通过让这两个缓存都常驻设备,我们就能显著削减昂贵的内存传输。此外,由于模型组件和 I/O 都是静态的,实现这一特性非常简单。

棘手的部分:决定何时“提交”

获得实时预览的最直接、最准确的方式,就是简单地从头重新解码整段转录。毕竟,解码器将拥有整段音频的更佳上下文,也就是最大化的 cross-kv 缓存。但正如你所见,这可能会慢得令人望而却步,因为计算成本随输入音频长度的平方增长。此外,由于我们采用了静态模型组件,每次解码器调用在带掩码输入的情况下都要昂贵得多。

作为替代,我们可以通过随着音频推进而提交预览的前缀来削减这一成本。我们押注于这样一个想法:有了足够的上下文,最初的转录就是准确的,现在可以定稿了(既然它们只会是相同的 token,重做只是一种浪费)。

因此我目前使用两条启发式规则来决定何时提交一个 token:

  1. 它不再变化(LocalAgreement-2):该 token 在最近两次连续解码中保持一致

  2. 它已经“老”了:该 token 至少落后前沿 3 秒的音频。

这两条启发式规则都可以针对你的具体用例进行调整,只需分别尝试一些数值,看看它们与“黄金”全上下文解码的一致程度即可。

更难的部分:连续流式处理

在我当前的设置下,转录并非真正意义上无限音频长度的流式转录。具体来说,我们受限于在固定话语上限(设为 8 秒)内进行流式处理。

即便如此,我们仍可以通过在系统层面与 VAD 协同来获得伪流式的用户体验。当检测到长时间停顿时,我们可以重置转录流水线以及模型组件的状态和缓冲区。只要没有超过 8 秒固定窗口的长话语,我们就能体验到流畅的流式转录。

其他策略,如对 cross-kv 缓存进行滑动窗口处理,或在话语边界之间进行缓冲和拼接,可能会影响准确率,但值得探索。

我们诚邀你研究如何在系统层面协调模型,以提供最佳的流式转录。

总结

在本博客中,我们了解了:

  • Moonshine V2 流式模型的简要概览,

  • 如何从 PyTorch 导出和准备模型的简短指南,

  • 实时流式语音转录的简单演示,以及

  • 一些设计难题及其解决方法。

下一步是什么?

  • 正如我们前面讨论的,要在系统层面实现更好的协调以处理两段话语之间的间断,仍有一些工作要做。

  • 为了进一步改善延迟和内存占用,我们可以采用 INT8 量化(尤其是在解码器上),代价是损失一些准确率。

参考文献

  • Jeffries, N., King, E., Kudlur, M., Nicholson, G., Wang, J., & Warden, P. (2024). Moonshine: Speech recognition for live transcription and voice commands. arXiv preprint arXiv:2410.15608.

  • Kudlur, M., King, E., Wang, J., & Warden, P. (2026). Moonshine v2: Ergodic streaming encoder asr for latency-critical speech applications. arXiv preprint arXiv:2602.12241.

附注

  1. 正如我们稍后将看到的,我们也可以把 I/O 全部放到设备上,但融合它们免去了这里的麻烦。
  2. 注意不要做过头,因为这可能会影响准确率或延迟。

一些性能数据

Tiny 模型

组件CPU (ms)NPU (ms)加速比DRAM: CPU → NPU (MB)内存: CPU → NPU (MB)
Encoder81.623.43.49×122.3 → 82.990.1 → 41.0
Decoder37.928.31.34×170.5 → 131.3138.0 → 68.2

表 1:在 Coralboard 上对 3400 万参数模型(tiny)使用 NPU 相比 CPU 的性能提升。

Small 模型

组件CPU (ms)NPU (ms)加速比DRAM: CPU → NPU (MB)内存: CPU → NPU (MB)
Encoder409.0174.22.35×361.1 → 526.3328.9 → 262.9
Decoder99.068.61.44×391.4 → 313.4358.9 → 164.2

表 2:在 Coralboard 上对 1.23 亿参数模型(small)使用 NPU 相比 CPU 的性能提升。

decode-steps 图 5:采用完整重新解码时,步数随音频长度呈二次方增长。

decode-latency 图 6:因此,采用完整重新解码策略时,解码调用的延迟比增量策略增长得快得多。

queue-depth 图 7:采用完整重新解码时,队列深度几乎难以跟上实时进度。