☰
基于 OpenVINO 部署 Silero VAD:If 子图内联、ONNX 模型转换与 f32 精度实践
2026/10/2 17:37:51 网站建设 项目流程
  • 人工智能
  • 语音

【免费下载链接】silero-vad

Silero VAD: pre-trained enterprise-grade Voice Activity Detector

项目地址:https://gitcode.com/GitHub_Trending/si/silero-vad
点击查看免费下载

导读

本文围绕 Silero VAD 仓库中 examples/openvino 目录的配套文档展开,讲解如何把仓库内随附的silero_vad.onnx转换成可直接在 OpenVINO 中加载、编译与推理的模型。你将掌握:ONNX 前端为何无法加载原模型的根本原因(If 子图内的 Conv/STFT 静态秩推断失败)、convert.py如何通过"经验性条件求值 + 死代码消除 + Identity 折叠 + 静态形状修复"完成等价变换、如何通过verify.py做逐位级验证,以及推理时为什么必须把 CPU 插件精度锁定为 f32、I/O 协议如何与仓库 OnnxWrapper 保持一致。

问题背景:stock 模型为什么无法加载

仓库随附的原始模型位于 src/silero_vad/data/silero_vad.onnx(文件约 2.32 MB,与文档描述的 2.27 MB 一致)。它由 TorchScript 追踪导出,无法直接被 OpenVINO 的read_model加载,报错如下:

OpConversionFailure: Model wasn't fully converted. -- Conv-16: While validating ONNX node '<Node(Conv): If_0_then_branch__Inline_0__/decoder/decoder/2/Conv_output_0>': Check 'data.get_partial_shape().rank().is_static()' failed The input data tensor's rank has to be known (static)

根因链条可以拆成三步:

  1. Conv 算子藏在 If 子图里:STFT 前端特征计算被封装为Conv算子,且这些Conv位于If节点的then_branch/else_branch子图中。OpenVINO 的 ONNX 前端在转换时无法为子图内的这些Conv推断出静态的输入张量秩(rank仍为动态)。
  2. If 数量多且嵌套:图中有一个顶层If用于在 8 kHz 与 16 kHz 两条路径之间切换,另外每个分支下还残留 7 个由 TorchScript 追踪留下的嵌套If节点。因此仅仅把采样率常量折叠掉(即直接写死sr)是不够的——其余嵌套 If 依然会让前端转换失败。
  3. 所有条件都与音频内容无关:这些 If 的条件只依赖采样率和张量形状(sr取 8000 还是 16000),不依赖实际音频数据。这意味着对任一固定配置(固定采样率、固定 batch),所有分支都可以在离线阶段一次性解析掉——这正是convert.py转换思路成立的前提。

convert.py:四步离线等价变换

convert.py 的核心流程(convert函数)如下:加载模型 → 循环内联所有 If → 死代码消除 → 折叠 Identity → 固定静态形状 → 类型推断与校验 → 保存(可选导出 IR)。下面结合源码逐段拆解。

1. 经验性求值并内联每个 If

eval_cond(convert.py)用一个"探针"技巧求 If 条件的实际取值:深度拷贝当前模型,把条件张量追加为 graph 的额外输出,再用 onnxruntime 在 CPU 上运行一次,从输出中取出条件值并断言它是标量布尔。

inline_if(convert.py)随后把选中的分支整体替换掉 If 节点:

  • 把分支内部的节点、initializer 统一重命名,加F{step}::前缀避免与外层作用域名字冲突(deep_rename会递归进入嵌套子图,并按 ONNX 作用域规则跳过被遮蔽的名字);
  • 分支输出若来自内部,则重命名直接映射到 If 节点的对应输出;若分支输出是外层捕获的张量,则补一个Identity节点做桥接;
  • 分支的 initializer 拷贝进外层 graph。

convert的主循环(convert.py)反复扫描If节点,逐个求值、内联,直到图中不再存在If,最后断言图中也不存在Loop、Scan这类控制流算子,保证产出一个纯数据流图。

2. 死代码消除(DCE)

dce(convert.py)从 graph 输出反向做可达性分析:只保留能连到输出节点的节点,并递归收集其输入及子图中从外层捕获的输入;随后同步清理不可达的 initializer 和 graph input。这一步的意义不止于"瘦身"——未选中的采样率分支(例如--sr 8000时整个 16 kHz 分支)连同其全部权重会被物理移除,这正是文档所说文件从 2.27 MB 降到 1.23 MB 的原因。

3. 折叠 Identity 节点

eliminate_identity(convert.py)反复遍历并移除 Identity 节点,把它的输入/输出引用在消费者与生产者之间重连(保留"输出到输出"别名这类必要场景)。文档明确指出其必要性:较新版本的 OpenVINO 在序列化 IR 时会把这类节点写成opset16::Identity,而较旧的 CPU 插件"能解析但不能执行",转换脚本在测试中真实踩过这个坑——这也是 README 反复强调"IR 要用目标 OpenVINO 版本本地重新生成"的原因。

4. 固定静态形状

内联与清理完成后,脚本把input、state、output、stateN四个 I/O 的形状从动态维度(dim_param)改写为固定值(convert.py),删除残留的value_info,再调用shape_inference.infer_shapes做全图形状推断,最后onnx.checker.check_model校验并用onnx.save写出干净模型。默认输出文件名为silero_vad_16k.onnx(--sr 8000时则为silero_vad_8k.onnx),见main中的默认值逻辑(convert.py)。

命令行用法

文档给出的完整命令如下,注意输出文件名与采样率选择:

pip install numpy onnx onnxruntime openvino python convert.py # writes silero_vad_16k.onnx python convert.py --sr 8000 # writes silero_vad_8k.onnx python convert.py --save-ir # also exports OpenVINO IR python verify.py silero_vad_16k.onnx ../../tests/data/test.wav "../c++/aepyx.wav" python verify.py silero_vad_8k.onnx --sr 8000 "../c++/aepyx_8k.wav"

补充说明(均与脚本实现一致):

  • convert.py默认输入是仓库自带的 silero_vad.onnx(REPO_MODEL定义于 convert.py,通过parents[2]定位到仓库根);也可传[input.onnx] [output.onnx]位置参数指定自定义路径;
  • --sr仅接受 8000 / 16000 两个取值(convert.py),对应保留的分支与输入长度:16 kHz 时input为[1, 576],8 kHz 时[1, 288](见 convert.py 的 feeds 构造);
  • --save-ir需要已安装 openvino,会在输出旁生成同名.xml+.bin,且compress_to_fp16=False保持 f32 权重(convert.py);
  • 依赖方面:convert.py只要求 numpy、onnx、onnxruntime;openvino 仅在--save-ir时才需要(见文件头部 docstring,convert.py)。

必须设置推理精度为 f32

文档给出了一条容易被忽略的硬性建议:在支持 bf16 的 CPU(具备 AMX 或 AVX512 BF16 指令集,多见于服务器级 Xeon)上,OpenVINO CPU 插件默认会静默启用 bf16 推理。对 Silero VAD 这种带循环状态的小模型,这不是"可以接受的小误差":

  • 每个 chunk 的数值误差会通过循环状态(state,形状[2, 1, 128])逐步累积放大,直到语音分段结果发生变化;
  • 文档实测:真实音频上默认精度与 stock 模型的最大绝对差可达3e-1,而 f32 仅为2e-6——相差约 5 个数量级。

因此推理时务必显式指定精度,Python 端写法:

compiled = core.compile_model(model, "CPU", {"INFERENCE_PRECISION_HINT": "f32"})

C++ 端对应ov::hint::inference_precision(ov::element::f32)。verify.py的OvConverted类也遵循这一约定(verify.py),并在输出中打印当前 OpenVINO 版本与 "inference precision forced to f32"。模型本身很小,f32 带来的开销可以忽略。

转换后模型的 I/O 契约与推理协议

转换后模型已移除sr输入(采样率分支被固定),I/O 如下:

张量方向形状(16 kHz)形状(8 kHz)类型
input输入f32[1, 576]= 64 上下文 + 512 新样本f32[1, 288]= 32 上下文 + 256 新样本f32
state输入[2, 1, 128][2, 1, 128]f32
output输出[1, 1][1, 1]f32(语音概率)
stateN输出[2, 1, 128][2, 1, 128]f32

推理循环协议与仓库内 OnnxWrapper 完全一致(对照OnnxWrapper.__call__,utils_vad.py):

  • 每个 chunk 为 512 个新样本(8 kHz 时 256 个),拼上上一轮的上下文组成input;
  • 把本次输出的stateN链回下一次的state;
  • 从本次input尾部截取最后 64(8 kHz 为 32)个样本作为下一轮的上下文;
  • 新数据流开始时,state与ctx都清零。

文档给出的最小推理示例:

import numpy as np import openvino as ov req = ov.Core().compile_model("silero_vad_16k.onnx", "CPU", {"INFERENCE_PRECISION_HINT": "f32"}).create_infer_request() state = np.zeros((2, 1, 128), np.float32) ctx = np.zeros((1, 64), np.float32) for chunk in stream_of_512_sample_chunks: x = np.concatenate([ctx, chunk[None]], axis=1) res = req.infer({"input": x, "state": state}) prob, state, ctx = res["output"][0, 0], res["stateN"], x[:, -64:]

verify.py:逐位级验证与分段一致性检查

verify.py 把转换后的模型与 stock 模型在"链式状态"下做端到端对比,任何不符都会以非零退出码结束(verify.py),其判定标准为:

  • onnxruntime 中转换模型 vs stock 必须逐位相等(max abs diff 恰好为 0.0);
  • OpenVINO 中 max abs diff 必须小于1e-4;
  • 真实 wav 上的语音分段结果必须与 stock 完全一致。

对比引擎有三个(verify.py):OrtStock(stock 模型,保留sr输入)、OrtConverted(转换模型、onnxruntime 后端)、OvConverted(转换模型、OpenVINO 后端、强制 f32)。它们共享相同 reset/chunk 拼接逻辑,其中上下文大小 64/32 与 chunk 大小 512/256 的映射和推理协议一一对应。

测试音频有两类:

  • 合成音频(synthetic_audio,verify.py):一段约 22 秒、包含静音、50/60/120/180 Hz 工频谐波、高斯噪声、4 Hz 包络 + 220/710/2400 Hz 类共振峰载波、100→1000 Hz 扫频、强噪声与尾部静音的拼接信号,随机种子固定为 42。文档强调对比是纯数值性的,不需要像真实语音;
  • 真实 wav:任意 16 bit 单声道 PCM 文件,采样率必须匹配(load_wav会断言帧率、声道数与位深,verify.py)。

分段判定函数segments(verify.py)以概率 0.5 为阈值、最短 8 个 chunk 为一段,统计起始/结束索引并逐段比对。

文档给出的实测验证结论(可复现依据即上述脚本与仓库内测试数据):

  • 在 onnxruntime 中,转换模型对两种采样率均逐位一致(max abs diff 0.0),说明该变换对所选采样率是数学意义上的恒等变换;
  • OpenVINO + f32 下,16 kHz 对 tests/data/test.wav 与 examples/c++/aepyx.wav(合计 229 秒、7161 个 chunk)max abs diff 为2.3e-06,29/29 与 65/65 个分段全部一致;
  • 8 kHz 对 examples/c++/aepyx_8k.wav max abs diff 为4.5e-06,79/79 个分段一致;
  • 上述结果在 Linux 与 Windows、两个 OpenVINO 版本上复现;stock 模型 sha256 为1a153a22f4509e292a94e67d6f9b85e8deb25b4988682b7e174c65279d8788e3(可直接对 src/silero_vad/data/silero_vad.onnx 校验)。

使用限制(Caveats)

  • 单采样率、固定 batch:每个转换后的模型只保留一个采样率分支(sr输入被移除),batch 固定为 1。文档明确指出部分被内联的 guard 可能依赖 batch,因此 batch=1 已被穷尽验证,更大的 batch 未验证;
  • IR 对版本敏感:IR(xml + bin)与生成它的 OpenVINO 版本绑定。部署时应使用目标 OpenVINO 版本重新生成 IR,或直接read_model加载清洗后的 ONNX;
  • wav 文件格式:verify 只接受 16 bit 单声道 PCM,采样率须与--sr匹配(verify.py)。

常见问题(FAQ)要点

每个 OpenVINO 大版本都要重新生成模型吗?不需要。唯一版本敏感的产物是 IR;convert.py产出的清洗后 ONNX 是普通 ONNX 文件,OpenVINO 直接read_model即可加载,跨 OV 版本的行为与 stock 模型跨 onnxruntime 版本一致。这也是仓库选择发布转换脚本而非二进制产物、并建议"如要发布 IR 就用目标版本本地生成"的原因。

IR 向前还是向后兼容?IR 向前兼容、不向后兼容:旧版本生成的 IR 可被新运行时加载(IR 格式自 2022.1 起稳定),但新版本生成的 IR 可能无法在旧运行时上使用——opset16::Identity无法被旧 CPU 插件执行正是实例,也是convert.py折叠 Identity、README 建议本地重生成 IR 的由来。

是否依赖具体 CPU 型号?文件本身与硬件无关,编译在加载时针对目标进行;唯一的 CPU 相关行为是默认推理精度——bf16 能力的 CPU 上插件默认 bf16,错误会经循环状态累积到分段改变;按 README 设置INFERENCE_PRECISION_HINT=f32后,各 CPU 上与 onnxruntime 的误差均约 1e-6。

只能跑在 Intel CPU 上吗?不是。CPU 插件按指令集特性而非厂商分派:AMD x86-64 同样可运行(Intel 是验证与调优的主要对象),ARM64(含 Apple Silicon)也官方支持。Intel 独占的是加速器插件(集成 GPU 与 NPU),本示例未涉及。

与仓库主代码的关系

本示例的推理协议与仓库主库的 OnnxWrapper 完全同构——上下文拼接、状态链回、清零时机与num_samples/context_size的 512/64、256/32 映射均可在 utils_vad.py 中找到对应实现;verify.py中的segments分段逻辑也与仓库get_speech_timestamps的阈值化思想一致。因此,在 OpenVINO 上运行转换模型时,可以沿用仓库 tests/test_basic.py 中read_audio+ 分块推理的使用范式,仅在推理后端上替换为 OpenVINO 的 InferRequest。若需进一步了解模型结构本身,可参考 src/silero_vad/model.py 与 hubconf.py。

  • 人工智能
  • 语音

【免费下载链接】silero-vad

Silero VAD: pre-trained enterprise-grade Voice Activity Detector

项目地址:https://gitcode.com/GitHub_Trending/si/silero-vad
点击查看免费下载

相关推荐

上一篇:xCrash与主流崩溃监控方案对比:为什么选择xCrash
下一篇:ponytail React 倒计时示例剖析:同一个模型,267 行代码如何变成 9 行

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询