Repository navigation
Description
ContextualParaformer.__call__ in runtime/python/onnxruntime/funasr_onnx/paraformer_bin.py (inherited by SeacoParaformer) returns results only for the first batch when a call has more files than batch_size:
model = SeacoParaformer(model_dir, batch_size=1)
res = model([wav1, wav2, wav3], "魔搭 你好") # len(res) == 1With batch_size=2, the third file is missing. Nothing is printed: each lost batch goes through the except ONNXRuntimeError branch, which logs input wav is silence or noise at WARNING and adds no result, and the package's logger setup (logging.basicConfig(level=logging.ERROR)) hides that warning in a plain script.
Cause: __call__ computes the hotword embedding once per call ([num_hotwords, 512]), but the batch loop overwrites it:
bias_embed = np.expand_dims(bias_embed, axis=0)
bias_embed = np.repeat(bias_embed, feats.shape[0], axis=0)So the second batch sends a 4-D bias_embed, and ONNX Runtime rejects it: Invalid rank for input: bias_embed Got: 4 Expected: 3. ContextualParaformer / SeacoParaformer in runtime/python/libtorch/funasr_torch/paraformer_bin.py have the same loop (bias_embed = torch.unsqueeze(bias_embed, 0).repeat(feats.shape[0], 1, 1)) and raise RuntimeError: Number of dimensions of repeat dims can not be smaller than number of dimensions of tensor on the second batch.
Repro on main (1125860) without downloading a model: tiny ONNX exports with the input/output names of funasr/models/contextual_paraformer/export_meta.py (no pretrained weights), the real WavFrontend and ONNX Runtime. Run it with runtime/python/onnxruntime on PYTHONPATH:
Script
import json
import wave
from pathlib import Path
import numpy as np
import torch
work = Path("hotword_repro")
work.mkdir(exist_ok=True)
tokens = ["<blank>", "<s>", "</s>", "你", "好", "魔", "搭", "<unk>"]
class Embedder(torch.nn.Module): # hotword [N, 10] -> hw_embed [10, N, 512]
def forward(self, hotword):
weight = torch.ones(len(tokens), 512)
return torch.nn.functional.embedding(hotword.long(), weight).transpose(0, 1)
class Backbone(torch.nn.Module): # bias_embed [B, N, 512]; every frame predicts "你"
def forward(self, speech, speech_lengths, bias_embed):
bias = torch.tensor([0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0])
logits = speech[..., :1] * 0.0 + bias_embed.sum((1, 2), keepdim=True) * 0.0 + bias
return logits, speech_lengths // 4
hotword = torch.zeros(3, 10, dtype=torch.int32)
speech, lengths = torch.zeros(2, 12, 4), torch.tensor([12, 8], dtype=torch.int32)
torch.onnx.export(
Embedder(), (hotword,), str(work / "model_eb.onnx"), dynamo=False,
input_names=["hotword"], output_names=["hw_embed"],
dynamic_axes={"hotword": {0: "N"}, "hw_embed": {1: "N"}},
)
torch.onnx.export(
Backbone(), (speech, lengths, torch.zeros(2, 3, 512)), str(work / "model.onnx"), dynamo=False,
input_names=["speech", "speech_lengths", "bias_embed"], output_names=["logits", "token_num"],
dynamic_axes={"speech": {0: "B", 1: "T"}, "speech_lengths": {0: "B"},
"bias_embed": {0: "B", 1: "N"}, "logits": {0: "B", 1: "T"}, "token_num": {0: "B"}},
)
(work / "tokens.json").write_text(json.dumps(tokens))
(work / "config.yaml").write_text(json.dumps({
"frontend_conf": {"n_mels": 4, "lfr_m": 1, "lfr_n": 1, "dither": 0.0},
"model_conf": {"predictor_bias": 1},
}))
(work / "am.mvn").write_text(
"<AddShift> 4 4\n<LearnRateCoef> 1 [ 0 0 0 0 ]\n<Rescale> 4 4\n<LearnRateCoef> 1 [ 1 1 1 1 ]\n"
)
wavs = []
for index, samples in enumerate([4000, 8000, 12000]): # 0.25 s, 0.5 s, 0.75 s
tone = (np.sin(np.arange(samples) * 2 * np.pi * 440 / 16000) * 3000).astype("<i2")
with wave.open(str(work / f"{index}.wav"), "wb") as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(16000)
output.writeframes(tone.tobytes())
wavs.append(str(work / f"{index}.wav"))
from funasr_onnx import ContextualParaformer
model = ContextualParaformer(str(work), batch_size=1)
print([model(wav, "魔搭 你好")[0]["preds"][0] for wav in wavs]) # one call per file
print([row["preds"][0] for row in model(wavs, "魔搭 你好")]) # one call with all three filesOutput:
['你你你你', '你你你你你你你你你你你', '你你你你你你你你你你你你你你你你你']
['你你你你']
Expected: the second line equals the first, one result per file.
Environment: Windows 11; Python 3.11.17 with torch 2.10.0+cpu and numpy 2.4.6, and Python 3.12.15 with torch 2.8.0+cpu and numpy 1.26.4; onnx 1.19.0, onnxruntime 1.23.2, kaldi-native-fbank 1.22.5, librosa 0.11.0; funasr_onnx from runtime/python/onnxruntime on main.
Activity
LauraGPT commentedon Nov 4, 2026
The fix is merged in #3838 as
50c3b97bfb7e124f1d83958cf7211b04f70bfe68. Independent checks reproduced all eight original multi-batch failures, then passed 52 selected CPU tests and 48 real L20Z FP32 ONNX/TorchScript contract cases. Exact-head and merged-main CI artifacts were accepted separately, including package source bytes and both runtime test matrices.This establishes the batching fix with analytic fixed-weight graphs and the real wrappers/frontend; it is not pretrained recognition-quality, SeACo timestamp-output, Windows or NPU validation.
Delivery distinction: this is now a source fix. The already-published official
funasr-onnx 0.4.6andfunasr-torch 0.1.4remain unchanged and do not contain it; the CI packages using those version strings are candidate build artifacts, not replacement PyPI releases. Standalone package delivery requires new versions.LauraGPT commentedon Nov 5, 2026
Package delivery is now complete for this fix:
Install the standalone runtime you use, for example
python -m pip install --upgrade funasr-onnx==0.4.7orpython -m pip install --upgrade funasr-torch==0.1.5. Updating the separatefunasrpackage alone does not replace these distributions.Both releases contain the merged #3838 hotword multi-batch fix. Fresh PyPI wheel/sdist downloads were checked against the GitHub assets and accepted source bytes by SHA256 and size; each GitHub release includes
SHA256SUMS. Earlier release files were not overwritten. The prior analytic CPU/L20Z validation scope remains unchanged: this is not new pretrained-quality, SeACo timestamp-output, Windows or NPU validation.