Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add cache-aware streaming and RNN-T head for Parakeet#46122

Closed
jmayank1511 wants to merge 18 commits into
huggingface:mainfrom
jmayank1511:parakeet-prompt-conditioning
Closed

Add cache-aware streaming and RNN-T head for Parakeet#46122
jmayank1511 wants to merge 18 commits into
huggingface:mainfrom
jmayank1511:parakeet-prompt-conditioning

Conversation

@jmayank1511

Copy link
Copy Markdown

What does this PR do?

Extends Parakeet TDT (#44171) with cache-aware streaming + an RNN-T
head. The cache-aware variant is shipped as Nemotron Speech
Streaming
(nvidia/nemotron-speech-streaming-en-0.6b).

Adds

  • ParakeetForRNNT — RNN-T head (LSTM prediction net + joint
    network) on the shared encoder. Supports regular and cache-aware
    checkpoints. Offline via generate(...).
  • ParakeetCacheAwareStreamingBuffer + streaming_step() +
    get_initial_streaming_state() — chunk-by-chunk greedy RNN-T
    decoding with encoder KV cache, decoder LSTM state, and last-token
    threaded across chunks. Mirrors NeMo's
    _greedy_decode_blank_as_pad_loop_frames.
  • Prompt-conditioned multilingual — one-hot language MLP after
    encoder, resolved via target_lang.
  • NeMo CausalConv2D padding alignment in subsampling.
  • Unigram SentencePiece support in ParakeetConverter.
  • Doc + unit tests for ParakeetForRNNT.

Code Agent Policy

  • I confirm that this is not a pure code agent PR.

Before submitting

  • This PR fixes a typo or improves the docs.
  • Did you read the [contributor
    guideline](https://github.com/huggingface/transformers/blob/main/CON
    TRIBUTING.md#create-a-pull-request)?
  • Was this discussed/approved via a Github issue or the forum?
  • Did you make sure to update the documentation with your
    changes?
  • Did you write any new necessary tests?

Who can review?

@eustlb @ebezzam @vasqu

jmayank1511 and others added 16 commits May 19, 2026 17:44
- Encoder gains cache-aware fields (att_context_size, att_context_style,
  conv_context_size, causal_downsampling, conv_norm_type) so cache-aware
  Parakeet checkpoints load with correct architecture in offline mode.
- Subsampling Conv2D handles causal time padding and pre-pads freq by 1
  bin to match NeMo dw_striding (128 mel -> 17 freq dims).
- Conformer conv module handles asymmetric/causal padding and LayerNorm.
- New ParakeetRNNTConfig, ParakeetRNNTJointNetwork, ParakeetForRNNT
  reusing ParakeetTDTDecoder. ParakeetRNNTGenerationMixin advances on
  blank, stays on non-blank with max_symbols_per_step guard.
- Conversion script learns the rnnt model_type and propagates the
  preprocessor.normalize flag (NeMo `NA` -> do_normalize=False).
- Auto-class registration: AutoModelForRNNT + parakeet_rnnt mappings.

End-to-end test on cache_aware_rnnt.nemo matches NeMo: "What is natural
language processing?" on en-US_sample.wav.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
NeMo's `dw_striding` causal subsampling pads each strided Conv2d with
`(left=kernel-1, right=stride-1)` on BOTH freq and time axes — not the
asymmetric (left-only-on-time) scheme initially tried. Replace the
custom pre-pad + per-layer half-pad logic with this uniform per-layer
pad, matching the CausalConv2D source exactly.

Subsampling outputs now match NeMo bit-for-bit (max diff 7e-4 vs prior
1.9e3) and `_get_subsampling_output_length` is updated to compute the
correct (kernel-1)+(stride-1) padding total for causal mode.

Verified WER on 261-sample eval: HF 21.33% vs NeMo 21.59%
(324/1519 errors vs 328/1519) — within numerical noise.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
- ParakeetConverter now dispatches on `proto.trainer_spec.model_type`,
  building a Unigram tokenizer when the SentencePiece model is type 1
  and BPE when it is type 2. parakeet-rnnt-1.1b uses Unigram and now
  converts cleanly.
- convert_nemo_to_hf.py ignores the training-only `nb_augmentation_prob`
  preprocessor flag.
- parakeet.md gains an architecture bullet and a usage section for
  ParakeetForRNNT, plus autodoc stubs for ParakeetRNNTConfig and
  ParakeetForRNNT.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…e_encoded, buffer)

This pulls in the encoder streaming runtime so cache-aware Parakeet checkpoints can
run chunk-by-chunk online inference, alongside their existing offline support.

Encoder additions:
- ParakeetEncoder.forward gains cache_last_channel/_time/_channel_len, att_context_size,
  use_cache, drop_extra_pre_encoded.
- _resolve_att_context_size, get_initial_cache_state(), _build_att_window_mask helpers.
- ParakeetEncoderRelPositionalEncoding.forward accepts a context_length override.
- ParakeetEncoderConvolutionModule threads cache_last_time and emits an updated cache.
- ParakeetEncoderAttention threads cache_last_channel (sliding window K/V) and emits
  an updated cache; relative-shift is sliced over total_key_length.
- ParakeetEncoderBlock returns (hidden, new_cache_channel, new_cache_time).

Output classes gain cache fields; new ParakeetCTCModelOutput. ParakeetForCTC.forward
accepts the cache args and returns the new output type.

ParakeetCacheAwareStreamingBuffer derives chunk sizing, pre-encode cache and STFT
lookahead from the model+processor; iterates (inputs, drop_extra_pre_encoded) pairs.

convert_encoder_config now derives intermediate_size = d_model * ff_expansion_factor,
passes through att_context_probs (training-only) and the cache-aware fields.

WER on 261-sample eval, batch=8:
- Cache-aware CTC streaming via buffer: 8.95% (136/1519)
- Cache-aware RNN-T offline: 21.20% (322/1519) — matches NeMo (21.59%)

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Adds a usage section showing chunk-by-chunk online inference with
ParakeetCacheAwareStreamingBuffer + autodoc stub.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
…nts)

Supports NeMo's hybrid_rnnt_ctc_bpe_models_prompt variant where the encoder
output is conditioned on a language/task prompt before the joint network.

ParakeetRNNTConfig:
- num_prompts: int (default 0; >0 enables prompt conditioning)
- prompt_dictionary: dict[str, int] mapping language code -> prompt index
- __post_init__ validates indices vs num_prompts.

ParakeetForRNNT:
- When num_prompts>0, builds `prompt_kernel = Sequential(Linear(num_prompts+enc_hidden,
  2*enc_hidden), ReLU, Linear(2*enc_hidden, enc_hidden))` matching NeMo's structure.
- get_audio_features and forward accept `target_lang: str` or `prompt_id: int`. After
  the encoder, a one-hot prompt is concatenated with the encoder output and projected
  back through prompt_kernel.

ParakeetRNNTGenerationMixin:
- _prepare_model_inputs pops target_lang/prompt_id from kwargs before the per-step
  forward (which only sees a slice of the encoder output) and forwards them only to
  get_audio_features.

convert_nemo_to_hf:
- When NeMo's model_defaults has initialize_prompt_feature=True, the converter reads
  num_prompts and prompt_dictionary into the HF config. NeMo's prompt_kernel.0/.2
  weights now load directly into HF's nn.Sequential of the same shape (no warnings).

Smoke test on cache_aware_40_langs_unified_600m... checkpoint:
  model.generate(input_features, attention_mask, target_lang="en-US")
  -> "What is natural language processing? <en-US>"

Co-Authored-By: Claude Opus 4.7 <[email protected]>
Mirrors NeMo's `_greedy_decode_blank_as_pad_loop_frames` + `conformer_stream_step`
for cache-aware Parakeet RN-T checkpoints, including the prompt-conditioned
multilingual variant.

ParakeetForRNNT additions:
- get_initial_streaming_state(batch_size, target_lang, prompt_id):
  returns the streaming state dict — encoder caches, decoder LSTM state from BEFORE
  last_token (`dec_h`, `dec_c`), cached decoder output `last_dec_g`, last committed
  token, and resolved prompt index.
- _decoder_pred_step(last_token, dec_h, dec_c) -> (g, new_h, new_c):
  manually drives the LSTM + projector so the new state can be discarded if the
  prediction is blank. Mirrors NeMo's `RNNTDecoder.predict`.
- streaming_step(inputs, drop_extra_pre_encoded, state, att_context_size=None):
  runs the encoder cache-aware on one chunk, applies the prompt MLP per-chunk when
  num_prompts > 0, then iterates encoder frames. Inner loop emits up to
  max_symbols_per_step non-blanks per frame; commits LSTM state only on non-blank
  predictions. Returns the chunk's emitted tokens per batch element.

Why a manual pred step (not reusing `ParakeetTDTDecoder` cache):
The decoder's internal cache updates whenever the input token is non-blank, but
RN-T greedy needs the cache to update only when the *prediction* is non-blank.
Re-calling the decoder with the same `last_token` after a non-blank emission
double-steps the LSTM and corrupts state. Manually driving the LSTM lets us
decide AFTER the joint+argmax whether to commit — matching NeMo exactly.

Validation on 261-sample English manifest:
- Cache-aware Parakeet RN-T (English) streaming: 7.04% (107/1519). Offline: 8.56%.
- Cache-aware Parakeet RN-T (multilingual, prompt-conditioned, en-US) streaming:
  9.22% (140/1519). NeMo offline-only on same: 9.15%.

Streaming and offline both produce reasonable transcriptions; streaming is the
correct mode for these cache-aware-trained checkpoints.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
ParakeetCacheAwareStreamingBuffer's chunk-0 layout (no pre-encode pad, no
drop) mismatches the encoder's training-time input distribution: the
encoder is trained with cache_last_channel always non-None, which makes
forward_internal always slice drop_extra_pre_encoded encoded frames off the
front. With the previous default, chunk 0 fed the conformer drop=0
noisy left-padded frames, producing a corrupted initial decoder state that
silenced output for the first several seconds on short utterances.

Match NeMo's reference streaming script (pad_and_drop_preencoded=True):
on chunk 0 the buffer now uses the steady-state chunk_size, prepends
pre_encode_cache_mel zero mel frames (post-normalization), and yields the
same drop_extra_pre_encoded as subsequent chunks. The old behavior is
preserved behind the constructor flag for parity testing.

WER on the 4 manifests that initially regressed (multi_rnnt vs NeMo offline):
  051022_English_Bias_Corpus_1.0: 5.66% -> 5.02% (~62% of gap closed)
  ASR_custom_2_0:                 8.11% -> 7.65% (~65% of gap closed)
  bs_general_noise_00:           18.91% -> 18.19% (~60% of gap closed)
  bs_general_noise_10:           11.19% -> 10.78% (~51% of gap closed)
…ibutions

Adds the standard joint-authorship copyright line under the existing
HuggingFace one (same Apache 2.0 license), following the pattern already
established by upstream transformers files like megatron_bert:

    # Copyright YYYY The HuggingFace Inc. team. All rights reserved.
    # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

Applied to the five files with substantive NVIDIA-authored additions in
this PR: modular_parakeet.py (RNN-T head, cache-aware streaming buffer,
prompt conditioning, streaming RN-T greedy decoder), configuration,
generation, convert_nemo_to_hf, and the Unigram branch in
convert_slow_tokenizer. modeling_parakeet.py is auto-generated from the
modular and inherits the header on regeneration.
@ebezzam ebezzam added the Audio label May 21, 2026

@eustlb eustlb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks a lot for raising this PR! 🤗

Looks to me that a lot of the duplicated logic from RNNT and TDT does not have to be duplicated as TDT should be an extension of RNNT

Comment on lines +633 to +636
class ParakeetForRNNTModelTester:
def __init__(
self,
parent,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's add integration tests

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The model nvidia/nemotron-3.5-asr-streaming-0.6b will be available soon, will add the integration tests soon

Comment on lines +297 to +309
# Streaming: prepend cached K/V from previous chunks and update the sliding window cache.
new_cache = None
if cache_last_channel is not None:
cache_len = cache_last_channel.shape[1]
cache_shape = (batch_size, cache_len, -1, self.head_dim)
k_cache = self.k_proj(cache_last_channel).view(cache_shape).transpose(1, 2)
v_cache = self.v_proj(cache_last_channel).view(cache_shape).transpose(1, 2)
key_states = torch.cat([k_cache, key_states], dim=2)
value_states = torch.cat([v_cache, value_states], dim=2)
new_cache = torch.cat([cache_last_channel, hidden_states], dim=1)[:, -cache_len:]

total_key_length = key_states.shape[2]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We have SlidingWindowCache. It should be initialised like here

then all this should be:

Suggested change
# Streaming: prepend cached K/V from previous chunks and update the sliding window cache.
new_cache = None
if cache_last_channel is not None:
cache_len = cache_last_channel.shape[1]
cache_shape = (batch_size, cache_len, -1, self.head_dim)
k_cache = self.k_proj(cache_last_channel).view(cache_shape).transpose(1, 2)
v_cache = self.v_proj(cache_last_channel).view(cache_shape).transpose(1, 2)
key_states = torch.cat([k_cache, key_states], dim=2)
value_states = torch.cat([v_cache, value_states], dim=2)
new_cache = torch.cat([cache_last_channel, hidden_states], dim=1)[:, -cache_len:]
total_key_length = key_states.shape[2]
if past_key_values is not None:
key_states, value_states = past_key_values.update(key_states, value_states, self.layer_idx)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

Comment on lines +281 to +288
cache_last_channel: torch.Tensor | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> tuple[torch.Tensor, torch.Tensor]:
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

past_key_values is used throughout the lib with a strong API. Use

Suggested change
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]:
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: torch.Tensor | None,
attention_mask: torch.Tensor | None = None,
past_key_values: Cache | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> tuple[torch.Tensor, torch.Tensor | None]::

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done..

Comment on lines +65 to +70
cache_last_channel (`torch.Tensor` of shape `(num_layers, batch, left_ctx, hidden_size)`, *optional*):
Updated attention cache from the encoder (sliding KV window). Pass to the next chunk call.
cache_last_time (`torch.Tensor` of shape `(num_layers, batch, hidden_size, conv_left_ctx)`, *optional*):
Updated convolution cache from the encoder. Pass to the next chunk call.
cache_last_channel_len (`torch.Tensor` of shape `(batch,)`, *optional*):
Number of valid frames currently stored in `cache_last_channel`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

cf other comments but:

  • kv cache should be past_key_values, as throughout the lib
  • our convention for the conv cache is padding_cache

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

Comment on lines +1406 to +1420
class ParakeetRNNTJointNetwork(nn.Module):
"""Joint network that combines encoder and decoder outputs to predict tokens (no duration head)."""

def __init__(self, config: ParakeetRNNTConfig):
super().__init__()
self.activation = ACT2FN[config.hidden_act]
self.head = nn.Linear(config.joint_hidden_size, config.vocab_size)

def forward(
self,
decoder_hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
) -> torch.Tensor:
joint_output = self.activation(encoder_hidden_states + decoder_hidden_states)
return self.head(joint_output)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

no reason to be defined distincly from TDTJointNetwork no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

Comment on lines +1465 to +1471
if self.num_prompts > 0:
enc_hidden = config.encoder_config.hidden_size
self.prompt_kernel = nn.Sequential(
nn.Linear(self.num_prompts + enc_hidden, 2 * enc_hidden),
nn.ReLU(),
nn.Linear(2 * enc_hidden, enc_hidden),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what released models use this feature? Looks like it can be removed no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

nvidia/nemotron-3.5-asr-streaming-0.6b will use this feature. This will be available in hugginface in next few days.

target_lang: str | None = None,
prompt_id: int | None = None,
**kwargs: Unpack[TransformersKwargs],
) -> ParakeetRNNTOutput:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

here the signature should be (almost) the same as for voxtral realtime, specifically with padding_cache and past_key_values

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

[For maintainers] Suggested jobs to run (before merge)

run-slow: auto, parakeet

@eustlb

eustlb commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

This was added in #46331, closing here

@eustlb eustlb closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants