sherpa-onnx 中 Whisper tiny.en 的 ONNX 模型张量接口详解:从 Encoder/Decoder 拆分的 I/O 契约到 RKNN 端侧部署
sherpa-onnx 中 Whisper tiny.en 的 ONNX 模型张量接口详解从 Encoder/Decoder 拆分的 I/O 契约到 RKNN 端侧部署【免费下载链接】sherpa-onnxSpeech-to-text, text-to-speech, speaker diarization, speech enhancement, source separation, and VAD using next-gen Kaldi with onnxruntime without Internet connection. Support embedded systems, Android, iOS, HarmonyOS, Raspberry Pi, RISC-V, RK NPU, Axera NPU, Ascend NPU, x86_64 servers, websocket server/client, support 12 programming languages项目地址: https://gitcode.com/GitHub_Trending/sh/sherpa-onnx导读本文以 scripts/whisper/rknn/tiny-en-onnx-info.md 为核心系统解读 sherpa-onnx 仓库中 Whispertiny.en模型导出为 ONNX 后的完整输入/输出张量契约Encoder 以 80 维 Mel 频谱为输入、输出 4 层 Cross-Attention 的 K/V 缓存Decoder 则以令牌与两类 KV Cache 为输入、逐 token 输出 logits 与增量 KV Cache。读完本文你将理解这套“Encoder 跑一次、Decoder 逐 token 滚动推理”的接口设计如何支撑流式解码以及它如何与仓库中的 ONNX 导出、RKNN 转换、RK3588 板端推理脚本形成完整的部署链路。一、背景Whisper 模型为何要拆成 Encoder 与 Decoder 两个 ONNXWhisper 的推理过程天然分为两个阶段Encoder 阶段把整段音频最长 30 秒的 Mel 频谱一次性编码为音频特征Decoder 阶段基于编码特征从|startoftranscript|开始逐 token 自回归生成文本。在 scripts/whisper/rknn/export_onnx.py 中作者将两者拆分为两个独立的 ONNX 模型tiny.en-encoder.onnx与tiny.en-decoder.onnx并做了两个关键改造取消 30 秒硬限制脚本头部注释明确说明 “we have removed the 30 seconds constraint from whisper. You can use any T 30”通过修改AudioEncoder.forward见 export_onnx.py不再要求n_ctx严格等于 1500而是允许不超过 3000 帧的任意长度引入 Tensor CacheDecoder 不再在每次调用时重新计算历史注意力而是由外部维护 KV Cache每次只增量计算当前 token详见下文第三节。tiny-en-onnx-info.md记录的正是拆分后两个 ONNX 模型的精确 I/O 签名——它是后续所有测试与部署脚本ONNX 验证、RKNN 转换、板端运行所共同遵守的接口契约。二、tiny.en Encoder把 30 秒音频压成 4 层 Cross-Attention 缓存2.1 输入NodeArg(nametiny.en-mel, typetensor(float), shape[1, 80, 3000])维度含义取值来源1batch size固定为 180Mel 频带数n_melstiny.en 为 80 维large-v3/turbo 等为 128 维3000时间帧数30 秒 × 100 帧/秒Whisper 每 10ms 一帧该张量由whisper.log_mel_spectrogram生成。在板端推理脚本 test_on_rk3588_board.py 中特征提取走的是kaldi_native_fbank的OnlineWhisperFbank并经过log10 → clamp → normalize处理若帧数不足 3000则在尾部补 0 padding 到固定长度保证 Encoder 输入形状恒为[1, 80, 3000]。2.2 输出Cross-Attention 的 K/V 缓存cross_k_0 ~ cross_k_3: shape[1, 1500, 384] cross_v_0 ~ cross_v_3: shape[1, 1500, 384]维度含义4 组 K/V对应 tiny.en 解码器的 4 个 Transformer 层n_text_layer 41500音频特征帧数此处为 30 秒音频编码后的完整帧序列384模型隐藏维度n_text_state 384为什么 Encoder 要输出 K/V 而不是直接输出特征关键在 export_onnx.py 的AudioEncoderTensorCache类Encoder 前向完成后立即用TextDecoder每一层的cross_attn.key()与cross_attn.value()线性投影把音频特征转换为 Cross-Attention 所需的 K/V从而把“逐层计算 cross K/V”这部分计算从 Decoder 中剥离出去。这样 Decoder 每次前向都无需重新投影整段音频显著降低逐 token 解码时的重复计算量——这正是端侧 NPU 部署所追求的极致省算力设计。在 export_onnx.py 中输出名按cross_k_{i}、cross_v_{i}i 0..3生成最终导出为tiny.en-encoder.onnx并写入包含模型结构信息n_text_layer、n_text_ctx、n_text_state、sot_sequence等的 custom metadata见 export_onnx.py后续推理脚本正是从该 metadata 中读取这些超参数。三、tiny.en Decoder带增量 KV Cache 的逐 token 解码器3.1 输入共 14 个张量tiny.en-tokens: tensor(int32), shape[1, 1] tiny.en-self_k_0 ~ self_k_3: tensor(float), shape[1, 448, 384] tiny.en-self_v_0 ~ self_v_3: tensor(float), shape[1, 448, 384] tiny.en-cross_k_0 ~ cross_k_3: tensor(float), shape[1, 1500, 384] tiny.en-cross_v_0 ~ cross_v_3: tensor(float), shape[1, 1500, 384] tiny.en-offset: tensor(int32), shape[1] tiny.en-mask: tensor(int32), shape[448]输入形状作用tokens[1, 1]当前待解码的 token id每次调用只喂 1 个self_k/v_0..3[1, 448, 384]4 层自注意力 K/V 缓存448 最大文本上下文长度n_text_ctx384 隐藏维cross_k/v_0..3[1, 1500, 384]4 层交叉注意力 K/V即 Encoder 的输出原样传入offset[1]当前解码位置已生成的 token 数用于定位 KV 缓存写入位置与位置编码mask[448]一维因果掩码0 表示允许、1 表示屏蔽前 n 个位置为 0其余为 1self KV cache 的初始化在 export_onnx.py 与 test_onnx.py 中self_kv_pair被初始化为全零张量(1, 448, 384)随着解码推进逐步填充。mask 的生成causal_mask_1d(n, L)见 export_onnx.py返回长度 L 的一维 int32 张量前 n 个位置为 0允许关注其余为 1屏蔽。在自注意力实现modified_self_qkv_attentionexport_onnx.py中被屏蔽位置填充-60000而非-inf以避免 NPU 上对无穷值的处理问题。3.2 输出共 9 个张量tiny.en-logits: tensor(float), shape[1, 1, 51864] tiny.en-this_self_k_0 ~ 3: tensor(float), shape[1, 1, 384] tiny.en-this_self_v_0 ~ 3: tensor(float), shape[1, 1, 384]输出形状作用logits[1, 1, 51864]当前 token 的词汇表得分51864 为 tiny.en 的词表大小n_vocabthis_self_k/v_0..3[1, 1, 384]当前 token 在每层产生的增量K/V用于回填外部缓存这里的设计非常精巧Decoder不直接更新传入的 self KV cache而是把“本步新产生的 K/V”作为输出返回this_self_*由调用方负责把它写回缓存中对应offset位置。这种“输入旧缓存 输出新增量”的模式使得 ONNX 图内部无需可变状态天然适配 NPU 静态图推理。3.3 解码循环中的调用方式综合 test_onnx.py 与 test_on_rk3588_board.py逐 token 解码循环的核心逻辑为先喂入sot_sequencetiny.en 为[50257, 50362]即|startoftranscript|与|notimestamps|每步把输出的this_self_k/v写回缓存、offset 1取logits[0, 0].argmax()作为下一个 token id循环执行run_decoder([token] self_kv cross_kv [offset, mask])直到输出eottiny.en 为 50256或达到步数上限每步把返回的this_self_k/v写入self_kv[i-1][:, offset:offset1, :]。其中n_text_ctx 448、n_text_state 384、n_text_layer 4等超参数在板端脚本 test_on_rk3588_board.py 中按模型名硬编码tiny/base/small/medium 对应不同的层数与维度最终生成的 token 序列通过tiny.en-tokens.txt由convert_tokens生成见 export_onnx.py做 base64 解码还原为文本。四、张量形状背后的模型结构参数tiny-en-onnx-info.md中出现的所有形状都能在 Whisper tiny.en 的模型配置中找到对应关系参数值在张量接口中的体现n_mels80Encoder 输入第二维n_audio_ctx1500Encoder 输出Cross K/V的帧数n_audio_state384隐藏维n_audio_head6注意力头数n_text_layer4cross_k/v_0..3、self_k/v_0..3的组数n_text_ctx448self K/V 缓存的时间维度、mask 长度n_text_state384self K/V 的隐藏维n_vocab51864logits 最后一维这些参数在导出时通过add_meta_data写入 ONNX 的 custom metadata见 export_onnx.py而test_onnx.py在init_encoder中直接从meta[n_text_layer]等键读取保证测试代码与导出配置的一致性。五、从 ONNX 到 RKNN张量契约的沿用该文档所在的scripts/whisper/rknn/目录README.md给出了完整的 RKNN 部署流程张量契约在整条链路中被原样沿用1. 导出 ONNX./export_onnx.py --model tiny.en生成tiny.en-encoder.onnx与tiny.en-decoder.onnx即本文档记录 I/O 签名的两个模型。2. 本地验证 ONNX./test_onnx.py --model tiny.en --wav ./en-16k.wav该脚本test_onnx.py会打印 Encoder/Decoder 的输入输出签名与本文档一致并完整跑一遍解码循环输出识别文本。3. 转换为 RKNNpython3 ./export_rknn.py --target-platform rk3588 --in-model ./tiny.en-encoder.onnx --out-model ./tiny.en-encoder.rknn python3 ./export_rknn.py --target-platform rk3588 --in-model ./tiny.en-decoder.onnx --out-model ./tiny.en-decoder.rknn转换脚本 export_rknn.py 支持rk3562/rk3566/rk3568/rk3576/rk3588等目标平台它先用 onnxruntime 读取 ONNX 的输入输出与 custom metadata将其拼接为custom_string写入 RKNN 模型见 export_rknn.py并使用optimization_level0、do_quantizationFalse构建export_rknn.py即保持 FP32 精度、不做量化以保住 tiny.en 的识别质量。仓库 README 显示转换结果约 22Mencoder与 95Mdecoder。4. 板端推理./test_on_rk3588_board.py --encoder ./tiny.en-encoder.rknn --decoder ./tiny.en-decoder.rknn --tokens ./tiny.en-tokens.txt --wav ./en-16k.wav板端脚本 test_on_rk3588_board.py 使用rknnlite.api.RKNNLite加载 RKNN 模型并绑定NPU_CORE_0特征提取与解码循环的输入组装方式与 ONNX 版本完全一致——也就是说只要遵循本文档记录的张量契约同一套解码逻辑可以在 CPUONNX Runtime、NPURKNN之间无缝切换。此外generate_encoder_data.py 与 generate_decoder_data.py 会按该契约把 Mel、tokens、KV cache、offset、mask 逐张量导出为.raw文件并生成输入清单用于 QNN 等其他平台的输入数据准备注意 QNN 需要将输入转置为(1, 3000, 80)见 generate_encoder_data.py。六、小结tiny-en-onnx-info.md虽然只是一份 I/O 签名清单却是整个 Whisper ONNX/RKNN 部署链路的“接口宪法”Encoder以[1, 80, 3000]Mel 为输入输出 4 层[1, 1500, 384]的 cross K/V把音频编码与 cross 投影一次性完成Decoder以tokens self KV cache cross KV cache offset mask为输入输出logits this_self KV增量实现无内部状态的逐 token 滚动解码该契约在 export_onnx.py导出、test_onnx.pyCPU 验证、export_rknn.pyNPU 转换、test_on_rk3588_board.py板端运行中全程保持一致。理解这套张量接口是自定义解码策略如 beam search、热词引导、适配新 NPU 平台或移植到其他推理引擎的前提对于希望把 Whisper 类模型部署到边缘设备的开发者这份契约与配套脚本构成了可直接复用的完整参考实现。【免费下载链接】sherpa-onnxSpeech-to-text, text-to-speech, speaker diarization, speech enhancement, source separation, and VAD using next-gen Kaldi with onnxruntime without Internet connection. Support embedded systems, Android, iOS, HarmonyOS, Raspberry Pi, RISC-V, RK NPU, Axera NPU, Ascend NPU, x86_64 servers, websocket server/client, support 12 programming languages项目地址: https://gitcode.com/GitHub_Trending/sh/sherpa-onnx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考