Skip to content

Fix bugs in NeMo transducer modified beam search.#3589

Merged
csukuangfj merged 1 commit into
k2-fsa:masterfrom
csukuangfj:fix-nemo-beam-search-decoder
May 8, 2026
Merged

Fix bugs in NeMo transducer modified beam search.#3589
csukuangfj merged 1 commit into
k2-fsa:masterfrom
csukuangfj:fix-nemo-beam-search-decoder

Conversation

@csukuangfj

@csukuangfj csukuangfj commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3588

cc @MingxinLiang

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced transducer beam search decoder with improved emission tracking and frame advancement logic for better hypothesis management.
  • Refactor

    • Fixed method signature in recognizer implementation for consistency.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label May 8, 2026
@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d641747d-62cf-487f-ae97-6f0b919e4e59

📥 Commits

Reviewing files that changed from the base of the PR and between 0f6e4c0 and 1c90514.

📒 Files selected for processing (2)
  • sherpa-onnx/csrc/offline-transducer-modified-beam-search-nemo-decoder.cc
  • sherpa-onnx/csrc/online-recognizer-paraformer-impl.h

📝 Walkthrough

Walkthrough

This PR fixes an infinite loop bug in the NeMo transducer modified beam search decoder by introducing per-hypothesis emission tracking with a per-frame symbol cap, forcing blank transitions when the cap is reached. A minor return-type fix for a deleted copy assignment operator in the Paraformer recognizer is also included.

Changes

NeMo Modified Beam Search Decoder - Per-Hypothesis Symbol Emission Tracking

Layer / File(s) Summary
Hypothesis State Tracking
sherpa-onnx/csrc/offline-transducer-modified-beam-search-nemo-decoder.cc
NeMoHypothesis struct adds num_symbols field initialized in constructor and preserved through copy/assignment operations to track emission count per hypothesis.
Decoding Parameter and Frame Limit
sherpa-onnx/csrc/offline-transducer-modified-beam-search-nemo-decoder.cc
Introduces max_symbols_per_frame parameter that caps non-blank symbol emissions within a single encoder frame during decoding.
Forced Blank Advance Logic
sherpa-onnx/csrc/offline-transducer-modified-beam-search-nemo-decoder.cc
When hypothesis reaches max_symbols_per_frame, beam search generates a forced blank-advance candidate that clones decoder states, resets num_symbols, and advances the frame.
Blank/Unk and Non-Blank Candidate Handling
sherpa-onnx/csrc/offline-transducer-modified-beam-search-nemo-decoder.cc
Blank/unk transitions reset num_symbols to 0; non-blank transitions update num_symbols with TDT-specific frame advancement based on predicted duration while non-TDT mode keeps frame offset unchanged but increments the counter.

Paraformer Copy Assignment Operator Signature Fix

Layer / File(s) Summary
Operator Return Type
sherpa-onnx/csrc/online-recognizer-paraformer-impl.h
Deleted copy assignment operator return type updated from value to reference (OnlineRecognizerParaformerImpl&) to follow C++ conventions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • k2-fsa/sherpa-onnx#2423: Implements per-frame multi-symbol decoding capped by max_symbols_per_frame in greedy search decoder, establishing the same pattern across different decoding algorithms.
  • k2-fsa/sherpa-onnx#2606: Changes TDT decoding logic in NeMo transducer decoders with per-frame token counters and max-tokens-per-frame caps, directly related to the beam search variant.
  • k2-fsa/sherpa-onnx#3555: Modifies NeMo transducer decoding to allow multiple non-blank emissions per encoder frame using max_symbols_per_frame-style approach, establishing parallel change patterns.

Suggested labels

size:L

Poem

🐰 A hypothesis per frame, no longer runs astray,
With symbol counts and blanks to guard the way,
When caps are hit, advance the frame with care,
No infinite loops in the decoder's air! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@csukuangfj csukuangfj merged commit 2a3a9d9 into k2-fsa:master May 8, 2026
26 of 27 checks passed
@csukuangfj csukuangfj deleted the fix-nemo-beam-search-decoder branch May 8, 2026 14:02

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to limit the number of symbols emitted per frame in the NeMo transducer decoder, preventing potential infinite loops. It also fixes the return type of a deleted assignment operator in the Paraformer implementation. The review feedback suggests defining the hardcoded symbol limit as a constexpr and optimizing performance by using std::move to avoid redundant deep copies of hypothesis states.


int32_t vocab_size = model_->VocabSize();
int32_t blank_id = vocab_size - 1; // NeMo models have blank at the end
int32_t max_symbols_per_frame = 10;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The max_symbols_per_frame value is hardcoded. It would be better to make this a configurable parameter in the decoder options to allow users to tune the behavior for different models. At the very least, it should be marked as constexpr to indicate it is a constant.

Suggested change
int32_t max_symbols_per_frame = 10;
constexpr int32_t kMaxSymbolsPerFrame = 10;

Comment on lines +178 to +193
NeMoHypothesis new_hyp;
new_hyp.ys = hyp.ys;
new_hyp.timestamps = hyp.timestamps;
new_hyp.durations = hyp.durations;
new_hyp.ys_probs = hyp.ys_probs;
new_hyp.context_state = hyp.context_state;
new_hyp.allocator = allocator;
new_hyp.log_prob = hyp.log_prob;
new_hyp.num_symbols = 0;
new_hyp.decoder_states.reserve(hyp.decoder_states.size());
for (const auto &state : hyp.decoder_states) {
new_hyp.decoder_states.push_back(Clone(allocator, &state));
}
new_hyp.frame_offset = hyp.frame_offset + 1;
all_candidates.emplace_back(new_hyp.log_prob, std::move(new_hyp));
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual copying of NeMoHypothesis fields and deep copying of decoder_states is redundant and inefficient. Since hyp is not used again in this iteration after the continue, you can use std::move(hyp) to avoid expensive cloning of ONNX tensors. This significantly improves performance as it avoids memory allocations and data copying for the decoder states.

Suggested change
NeMoHypothesis new_hyp;
new_hyp.ys = hyp.ys;
new_hyp.timestamps = hyp.timestamps;
new_hyp.durations = hyp.durations;
new_hyp.ys_probs = hyp.ys_probs;
new_hyp.context_state = hyp.context_state;
new_hyp.allocator = allocator;
new_hyp.log_prob = hyp.log_prob;
new_hyp.num_symbols = 0;
new_hyp.decoder_states.reserve(hyp.decoder_states.size());
for (const auto &state : hyp.decoder_states) {
new_hyp.decoder_states.push_back(Clone(allocator, &state));
}
new_hyp.frame_offset = hyp.frame_offset + 1;
all_candidates.emplace_back(new_hyp.log_prob, std::move(new_hyp));
continue;
NeMoHypothesis new_hyp = std::move(hyp);
new_hyp.num_symbols = 0;
new_hyp.frame_offset += 1;
all_candidates.emplace_back(new_hyp.log_prob, std::move(new_hyp));
continue;

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

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]

1 participant