Skip to content

funasr_onnx ContextualParaformer / SeacoParaformer return results only for the first batch of a file list #3837

Closed

Description

@tanishqbaweja

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) == 1

With 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 files

Output:

['你你你你', '你你你你你你你你你你你', '你你你你你你你你你你你你你你你你你']
['你你你你']

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

  1. LauraGPT commented on Nov 4, 2026

    @LauraGPT
    Collaborator

    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.6 and funasr-torch 0.1.4 remain 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions