Skip to content

mel: drop the NeMo padding frame before the encoder (parakeet, canary, canary-qwen) (#252) - #255

Merged
cjpais merged 2 commits into
mainfrom
fix/252-parakeet-valid-length
Nov 11, 2026
Merged

cjpais merged 2 commits into
mainfrom
fix/252-parakeet-valid-length

Conversation

@cjpais

@cjpais cjpais commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #252. NeMo's centered STFT emits floor(n/hop)+1 mel frames but NeMo only counts floor(n/hop) as valid and masks the last one through pre_encode, attention and the conv module. We fed it unmasked. When floor(n/hop) is a multiple of 8 it becomes an extra encoder frame that bleeds into every real frame via attention, so parakeet clips of those lengths came back empty or cut off.

The same bug was in canary and canary-qwen (same NeMo frontend, no length masking in our encoder). For canary-qwen the extra frame was also passed to the LLM as an extra audio token.

Now the padding frame is dropped before the encoder on the single and batch offline paths of parakeet (non-kestrel), canary and canary-qwen. Dropping it is equivalent to NeMo's masking for the valid frames. Ultra (kestrel) already masked it in-graph. Streaming paths are untouched: buffered streaming already masks it, and NeMo's cache-aware path keeps the frame.

Present since the original parakeet impl.

AI Assistance

Yes, AI assisted with the investigation, code and tests.

Validation

Parakeet:

  • Issue clips (5.04s / 10.56s), v3 Q8_0: were empty / truncated, now full text matching NeMo, on CPU, Metal and --batch. Same output with or without the last sample.
  • NeMo with encoder length forced to +1 reproduces the old (broken) output exactly.
  • Encoder on the 5.04s clip vs NeMo valid frames: cos 0.85 -> 0.9994.
  • validate.py all --family parakeet --variant parakeet-tdt-0.6b-v3: unchanged vs main (jfk exact).
  • LibriSpeech test-clean (2620), Q8_0 Metal: v3 1.87% -> 1.88%, v2 1.62% -> 1.62%. ~3% of hyps change, almost all punctuation/casing on the affected lengths.
  • AMI/ICSI 30s windows from the issue (40 windows x 2 lengths): the full/trimmed mismatch from this bug is gone. The remaining gaps vs NeMo full attention come from the default [256,256] local attention (Parakeet longform memory #199), which NeMo itself reproduces in local mode. Not addressed here.
  • v2, ctc-0.6b, rnnt-0.6b, tdt_ctc-110m, unified-en, ultra spot checked on the issue clips + jfk.
  • Moving the helper into MelFrontend is a no-op for parakeet: v3 and ultra dumps and transcripts are bitwise identical to the first commit on both issue clips and jfk (CPU).

Canary / canary-qwen, encoder vs NeMo on the 5.04s clip (valid frames):

  • canary-180m-flash F32: final encoder output cos 0.990 -> 0.999995, every frame > 0.9999.
  • canary-qwen-2.5b Q8_0: encoder output cos 0.77 -> 0.999, LLM input 78 -> 77 tokens (matches NeMo), transcript now matches NeMo.
  • canary-1b-flash Q8_0 on jfk (not an affected length): last pre_encode frame cos 0.946 -> 0.99994.

LibriSpeech test-clean (2620), Q8_0 Metal, before vs after:

model before after net word errors utts better / worse
canary-180m-flash 1.93% 1.93% -2 4 / 4
canary-180m-flash, batch 8 1.93% 1.93% -2 4 / 4
canary-1b-v2 1.92% 2.06% +78 15 / 23
canary-qwen-2.5b 1.63% 1.62% -2 10 / 8
canary-qwen-2.5b, batch 8 1.63% 1.62% -2 10 / 8

canary-1b-v2: 62 of the +78 come from one utterance (672-122797-0008) that now loops on "The boy was a man of the world...". NeMo F32 produces exactly the same output; the old build only avoided it because of the padding frame (last encoder frame cos vs NeMo 0.93 -> 0.99998). The other changed utterances net +16, near-tie word choices. No NeMo WER baseline on file for 1b-v2.

Tests: ctest default passes; mel_unit and the real-model smokes (parakeet_real, decoder, multitalker e2e, buffered stream eos, stream/offline interleave) pass. clang-format clean.

Not run: WER for canary-1b / canary-1b-flash (same code path as 180m-flash), validate.py for canary / canary-qwen.

The constant-pad STFT emits floor(n/hop)+1 frames but NeMo counts
floor(n/hop) as valid and masks the rest in pre_encode, attention and the
conv module. We fed the padding frame unmasked, so whenever floor(n/hop)
is a multiple of 8 it became an extra encoder frame that leaked into
every real frame through attention, emptying or truncating transcripts.

Drop it on the single and batch offline paths for non-kestrel
checkpoints. Kestrel models (ultra) already mask it in-graph; streaming
paths are untouched.
@cjpais cjpais changed the title parakeet: drop the NeMo padding mel frame before the encoder (#252) mel: drop the NeMo padding frame before the encoder (parakeet, canary, canary-qwen) (#252) Nov 11, 2026
@cjpais
cjpais merged commit 869f03b into main Nov 11, 2026
20 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parakeet v3 loses text on clips of some lengths

1 participant