Repository navigation
feat: offline support for the Nemotron unified ASR checkpoint - #108
Merged
localai-org-maint-bot merged 10 commits intoOct 29, 2026
Merged
localai-org-maint-bot merged 10 commits into
localai-org-maint-bot merged 10 commits into
Conversation
Unified checkpoints (att_context_style chunked_limited_with_rc) fell into the cache-aware streaming branch. The converter wrote streaming keys and the loader set streaming.present, so stream_begin accepted the model and ran a cache-aware stream on it. The converter now writes a parakeet.unified.* block for that style and no streaming keys. The loader reads the block, checks that the presets are [L,C,R] triples with C >= 1, and keeps streaming.present false. stream_begin and the CLI --stream refuse unified models with a clear error, and parakeet-cli info prints the presets. StreamingEncoder and StreamingMel checked their preconditions with assert. Release builds compile asserts out, so these checks now throw a catchable error. Models that reach them today already satisfy the conditions, so their behaviour does not change. Existing checkpoints convert to the same bytes as before: the 110m f32 and the parakeet_realtime_eou_120m-v1 f16 GGUFs match sha256. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
test_unified_load_negative doctors a unified GGUF (malformed presets, zero chunk, an out-of-range default, a missing flag or key) and checks that the loader refuses each one. test_unified_offline checks the unified fields and streaming off, then compares mel, encoder, prompt kernel, RNNT token ids and text against a NeMo baseline. It also checks that stream_begin refuses the model. gen_nemo_baseline.py adds a mel tensor to the prompt baseline (an addition for the nemotron-3.5 baseline too). For a unified checkpoint it skips the cache-aware streaming half, which does not apply. Parity rows and the README model list record the offline state. The encoder stage passes only with NeMo's valid frame count, because the production path feeds every mel frame. That is an open issue. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
The loader gate reads only the key-value section, so the doctored copies no longer carry the checkpoint's tensors. Each case used to copy the full GGUF to the system temp directory. The copies now sit beside the source file and take a few kilobytes. A comment in model_loader.hpp now says what default_preset indexes. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
test_unified_offline held the loader, the decoder and the C-API context at once. For the 2.5 GB f32 file that is three copies in memory, and a ctest run on a shared host was killed for memory. Each stage now runs in its own scope, so one copy is alive at a time. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
NeMo encodes with length = floor(samples / hop) frames. The mel tensor holds one more frame (center padding), and the per-feature normalization zeroes it. The encoder still attended to that zero frame, so the offline encoder output drifted from NeMo by up to 0.03 on a German clip, though the decoded tokens stayed the same. A new GGUF key, parakeet.unified.valid_length, asks the mel frontend to keep only the frames NeMo passes as length. It is written true for unified checkpoints and absent (off) for every other GGUF, so existing models take the same code path as before. The CPU and GPU mel frontends both apply it, so the batch and chunked paths agree. With the key on, the production encoder output matches NeMo to 2.4e-7 (English) and 6.0e-7 (German) at f32. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
test_unified_offline now feeds the production mel frontend output to the encoder, with no manual truncation. It asserts the trimmed frame count and an encoder error below 1e-5, and that the gate is on. The negative test checks that a file without the key loads with the gate off. The parity notes record the remaining mel difference (2e-4 in the highest bins on the English clip) and its likely cause. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
parakeet_capi_stream_begin_lang and parakeet_capi_vad_stream_begin allocated the handle before the constructors that can throw (the streaming session, the streaming mel, the Silero stream). When one threw, the handle leaked. The handle is now held by a unique_ptr until the begin succeeds, then released to the caller. The diarization stream begin already held its object in a unique_ptr before the throwing step, so it was not affected. The streaming mel now throws on a per_feature config. The diarization path builds one, so the note records why the shipped checkpoint is safe: its model_config.yaml has normalize NA. A per_feature diarization GGUF is refused with an error. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
…ied keys With parakeet.unified.valid_length, a clip shorter than one hop has floor(samples/hop) = 0 frames, as NeMo's length is 0. The trim helper returned early for zero frames, so the clip kept one frame. It now returns zero frames, on the CPU and the GPU frontends. The encoder returns an empty output for zero frames, so an empty or sub-hop clip gives no text and no crash, with and without timestamps. test_unified_offline checks this on the real checkpoint. The unified block of the loader called gguf_get_val_* on keys whose type it had not checked. A wrong type aborted the process. Each key's type is now checked first, and a mismatch is a load error. The array element type is checked too. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
scripts/unified_kvs.py holds the writer for the unified-checkpoint keys and needs only the standard library, so it can be tested without NeMo. The converter imports it. The training windows (att_chunk_context_size) are now validated: a scalar part is accepted as a one-element list, and a missing part, a wrong length, or a non-integer entry raises a ValueError that names the key. Before, a scalar part failed with a TypeError. The presets are still the card's hardcoded values. The checkpoint's config does not carry them (att_context_size is [-1,-1]), and the module says where they come from. tests/python/check_unified_kvs.py exercises the writer with a recording writer. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
test_unified_synthetic writes small KV-only GGUFs (with the filterbank and window tensors the mel kernel reads) and needs no checkpoint, so it runs in CI and is never skipped. It checks: - the trimmed mel frame count is floor(S/hop) for S = 0, 1, hop-1, hop, hop+1 and k*hop plus an offset, on the CPU and GPU frontends, and the trimmed matrix equals the first columns of the untrimmed one; - with the gate off, the frame count is 1 + S/hop, and a non-unified style ignores the key; - StreamingMel refuses a per_feature config and StreamingEncoder refuses a non-streaming config, both with an exception; - a wrong-typed unified key is a load error. The stream_begin refusal without a leak needs a loaded model, so it is covered only by test_unified_offline, which needs a checkpoint. It was not run under a sanitizer. Assisted-by: Claude:claude-haiku-5-5 [Claude Code]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds offline transcription for
nvidia/nemotron-labs-unified-asr-0.6b: a prompt-conditioned RNNT model with NeMo'schunked_limited_with_rcattention and a 4095-piece vocabulary. Offline inference uses full attention. Buffered streaming is not part of this PR and will follow separately.What changed
parakeet.unified.*block for thechunked_limited_with_rcstyle and no streaming keys. Other attention styles are unchanged.[L,C,R]triples,C >= 1, default index in range).streaming.presentstays false.stream_beginandparakeet-cli transcribe --streamrefuse unified models with a clear error. Before this change the model was routed into the cache-aware streaming path.StreamingEncoderandStreamingMelchecked their preconditions withassert, which Release builds compile out. These are now real checks that throw.parakeet.unified.valid_length(default off). When it is set, the mel frontend keeps onlyfloor(samples / hop)frames, the count NeMo passes as the encoderlength. The converter writes it only for unified checkpoints, so every other GGUF takes the same code path as before. The CPU and GPU frontends both apply it.parakeet-cli infoprints the unified fields.test_unified_offline(mel, encoder, prompt kernel, RNNT token ids, text, and the C-API refusal) andtest_unified_load_negative(malformed presets, and the gate defaulting off). The NeMo prompt baseline now also stores the mel.Verification
Reference: NeMo main
cd5e4fbd36ba098d0c78b4552f6f9592ad34de93, CPU, float32, batch 1. The reference decodes the prompt-conditioned encoder output withrnnt_decoder_predictions_tensor. Stages use the production frontend and encoder, f32 weights.WER against the NeMo reference text, offline, for
enandde: 0.0000 for f32, f16 and q8_0. Theautoprompt and other languages were not measured.Existing checkpoints convert to the same bytes as before. The sha256 matched before and after this change for
nvidia/parakeet-tdt_ctc-110m(f32) andnvidia/parakeet_realtime_eou_120m-v1(f16). The model-dependent suite for those two checkpoints ran before thevalid_lengthchange and matched the pre-change result: 41 passed, 0 failed, the rest skipped for missing fixtures. Thevalid_lengthpath is off for them.Commands:
Model-free suite: 54 tests, all pass (8 skip for missing fixtures).
Known issues
stream_*entry points refuse unified models until the buffered driver lands.valid_lengthkey was added with a metadata-copy step, and q8_0 withparakeet-cli quantize. Converting a checkpoint with this branch writes the same key.autoand the other prompt languages. The scene stream runs the unified model offline per segment; it was not tested.🤖 Generated with Claude Code