Skip to content

Add Parakeet-TDT 0.6B v3 (FastConformer-TDT ASR) - #111

Merged
0xShug0 merged 45 commits into
0xShug0:mainfrom
dleiferives:main
Jul 28, 2026
Merged

Add Parakeet-TDT 0.6B v3 (FastConformer-TDT ASR)#111
0xShug0 merged 45 commits into
0xShug0:mainfrom
dleiferives:main

Conversation

@dleiferives

@dleiferives dleiferives commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Native port of nvidia/parakeet-tdt-0.6b-v3,
loaded directly from the Transformers-compatible model.safetensors checkpoint
rather than the .nemo archive. Offline (full-context) transcription, CPU and
CUDA.

Frontend: 16kHz -> 128 mel bins, preemphasis=0.97, NeMo per-feature normalization
Encoder:  3-stage Conv2D subsampling (8x) -> 24-layer FastConformer -> 1024-dim
Decoder:  2-layer LSTM predictor + joint network (TDT: 5 duration classes [0..4])

Validation

Tier 1 — golden transcription (CI)

ctest -R parakeet_golden_transcription_test, wired into ENGINE_BUILD_TESTS,
skips cleanly when the model isn't installed. Asserts the decoded text of a
checked-in LibriSpeech test-clean clip matches NeMo's exactly.

Tier 2 — numerical parity against NeMo (manual)

tests/parakeet_tdt/parity/ compares mel features, an isolated encoder layer,
and full encoder output against real NeMo activations captured via forward
hooks (torch 2.13.0+cu130, nemo 2.7.3). Current results, on both backends:

stage CPU CUDA threshold
mel_features 1.000000 1.000000 >= 0.999
layer_0 1.000000 1.000000 >= 0.999
enc_out 0.972258 0.972258 >= 0.97

enc_out's looser bound is float32 summation order across a ~2M-node graph,
not a correctness gap — the README explains why, and it was verified by
chaining 24 isolated single-layer builds, which holds cosine >= 0.999999 the
whole way. That 0.972258 is identical to the value measured before the
optimization pass
, which is the strongest available statement that none of
the performance work changed the math: it is checked against NeMo directly
rather than against our own earlier output.

This tier found true bugs that the golden tests missed

Tier 3 — multilingual accuracy (new)

tests/parakeet_tdt/multilingual/ — 120 FLEURS clips across 24 of the 25
supported languages, 21.8 min of audio. Raw transcripts for every clip and
weight type are checked in under multilingual/results/ so the numbers can be
re-derived without re-running or re-downloading.

This exists because one clip in one language is not enough to clear a
quantization: q8_0 is byte-identical on the golden clip, and across 24
languages it changes 8.3% of transcripts.

Backends

Both, on the same machine, agreeing numerically:

CPU (12 threads) CUDA (GTX 1650 Max-Q)
encoder compute, 7.4s clip ~1140 ms ~124 ms
peak host RSS (f32 / q8_0) 3843 / 1078 MiB 1818 MiB
peak VRAM 2642 of 4096 MiB

enc_out agrees between CPU and CUDA to cosine 0.99999988 (rel RMS
3.5e-06, mel bit-identical) — float32 accumulation noise between different
kernels. Checked with the parity dump's --backend flag.

Worth noting: nemo_asr cannot load this model on this card at all
PyTorch's overhead on top of the weights exhausts the 4 GB budget. The native
path runs in 2642 MiB.

Performance

Reference clip (7.435s), Intel i7-9750H (6C/12T) + GTX 1650 Max-Q:

implementation settings CPU CUDA
NeMo / PyTorch (the Python reference) as shipped 857.7 ms (8.7x real-time) OOM — won't load in 4 GB
this port untuned: 8 threads, f32 ~1245 ms (6.0x) ~169 ms (44.0x)
this port tuned: 12 threads, q8_0 ~715 ms (10.4x) ~131 ms (56.8x)

Untuned, this port is slower than PyTorch on CPU (~1245 ms vs 857.7 ms).
Tuned it wins by ~1.2x, and on this GPU there is no comparison to make: the
PyTorch reference cannot load the model in 4 GB, while the native path runs it
in 2642 MiB. "Untuned" is just the two defaults — --threads 8, and
matmul_weight_type native, which is f32 for this checkpoint.

Measured with a drift-cancelling harness (ABBA ordering, 4 passes, median of 5
iterations per run, machine idle) — this laptop swings 20-30% between cold and
thermally saturated, so sequential before/after timing is not trustworthy at
this scale. Tuned is 1.72x default on CPU, 1.31x on CUDA.

The largest single win was not a kernel or precision change — it was a
correctness bug. The encoder graph runs at whatever capacity it was built for,
and encode() zero-pads up to that capacity, so a session prepared with a long
audio contract paid the long price on every short clip:

7.4s clip, graph built for encoder compute
7.4s (matched) 1018 ms
60s (oversized) 10928 ms
60s contract, fixed 1140 ms — 9.6x

And it was simultaneously wrong. Zero input doesn't stay zero (the subsampling
convs have biases), and the attention mask was unconditionally all-zeros, so
real frames attended to padded garbage. On the reference clip that silently
dropped an entire sentence:

matched    "...turning away her eyes. It is certainly very like the old portrait."
oversized  "...turning away her eyes."

Fixed in two commits (mask padded keys; rebuild rather than run oversized), and
the same fix carries to CUDA at ~7.5x.

Three further CPU optimizations are bit-exact — verified identical across
all 95,232 enc_out floats: attention operands read as strided views instead
of copies, constant scales folded into weights (exact, both are powers of two),
and a redundant transpose pair removed.

Weight precision

parakeet_tdt.matmul_weight_type accepts native|f32|f16|bf16|q8_0.

speed identical to f32 absolute WER
native (f32) 1.00x 100% 0.1338
f16 ~1.00x 99.2% 0.1336
bf16 1.14x 99.2% 0.1334
q8_0 1.79x 91.7% 0.1331

q8_0 changes 8.3% of transcripts but absolute WER does not move. The churn is
mostly formatting (T RexT-Rex), and where words change it goes both
directions — it is sometimes right where f32 was wrong. It costs
reproducibility against an f32 baseline, not quality. It also cuts peak RSS
3.6x, which may matter more than the speed on constrained machines.

native stays the default as the only setting that reproduces exactly.

Build and run

python3 tools/model_manager.py install parakeet_tdt --models-root models

cmake --build build/<preset> --target parakeet_warm_bench
build/<preset>/bin/parakeet_warm_bench \
    --model models/parakeet-tdt-0.6b-v3 \
    --audio tests/parakeet_tdt/assets/2086-149220-0033.wav \
    --backend cpu --threads 12

Package id parakeet_tdt, model dir models/parakeet-tdt-0.6b-v3.
tools/check_loader_catalog_sync.py reports catalog and loaders in sync.

Known limitations

  • Offline only. This checkpoint ships att_context_style="regular",
    att_context_size=[-1,-1] — unlimited bidirectional attention, not NeMo's
    chunked_limited streaming style. setup_streaming_params() doesn't error
    but produces a degenerate config (~5.8s chunks, ~10000-frame cache) with none
    of the real benefit. NVIDIA's maintainers point at a different architecture
    for streaming. Real support needs a purpose-trained checkpoint; this repo
    already has nemotron_asr for that.
  • Per-language WER in the study is 5 clips each — enough to show no language
    falls off a cliff under quantization, not publishable per-language figures.
  • CUDA numbers come from a 35 W GTX 1650 Max-Q, compute capability 7.5, which
    is below the 8.0 threshold where ggml enables CUDA graph capture. Several
    optimizations measured as no change on CUDA; that is this card, not the
    optimization. Worth re-measuring on Ampere or newer.

Docs

  • docs/community_models/parakeet_tdt.md — user-facing (162 lines)
  • docs/reports/parakeet_tdt_performance.md — full methodology, profiling, and
    the optimizations tried and rejected with numbers, so they don't get
    re-attempted
  • tests/parakeet_tdt/{parity,multilingual,bench}/README.md — harnesses

dleiferives and others added 30 commits July 25, 2026 09:57
Initial port of nvidia/parakeet-tdt-0.6b-v3 — a 600M-param
FastConformer-TDT
ASR model supporting 25 European languages with auto-detection.

Files under community_models/parakeet_tdt/:
- assets:    config parsing (Transformers-format), tokenizer loading
- weights:   NeMo safetensors key mapping, BN fold for 24-layer
depthwise conv
- frontend:  Mel filterbank frontend (16kHz, 128 bins, preemphasis=0.97)
- encoder:   24-layer FastConformer, NeMo-style Conv2D subsampling,
             relative position attention with untied biases, xscaling
- decoder:   2-layer LSTM predictor + joint (joint_enc(1024→640),
             decoder_projector(640→640), joint_head(640→8198))
- session:   offline session with prepare/run
- loader:    IVoiceModelLoader, family="parakeet_tdt"
- headers:   include/engine/community_models/parakeet_tdt/

Wiring: model_specs/parakeet_tdt.json, registry.cpp registration,
CMakeLists.txt engine_runtime sources + parakeet_warm_bench target.

The model loads and runs end-to-end on both CPU and CUDA (GTX 1650:
~180ms vs ~1570ms CPU). Encoder graph builds and produces 93-frame
1024-dim output. Decoder produces all-blank output due to a suspected
safetensors reader bug returning wrong data for tensors at offsets
beyond ~40 MB — BN statistics and depthwise conv weights in the
48 MB region read different values than direct HTTP range fetches,
while embedding weights at 1.6 MB read correctly.
…tend

The C++ frontend computed log-mel features (preemphasis -> STFT -> mel
filterbank -> log) but never normalized them, emitting raw log-mel values
(mean ~-11.8, std ~4.1). NeMo's AudioToMelSpectrogramPreprocessor for this
model is configured with normalize: "per_feature", which subtracts the
per-mel-bin mean and divides by the per-mel-bin unbiased std (computed over
the valid, unpadded time frames only, with a 1e-5 epsilon added to the std)
before the features ever reach the encoder. Skipping this step fed the
subsampling conv stack input at roughly the wrong scale by orders of
magnitude, and that error compounded through the conv/linear layers,
producing runaway-magnitude activations by the time they reached the
transformer encoder layers.

Confirmed against NeMo's own normalize_batch() (features.py): with the fix,
our mel features match NeMo's bit-for-bit in direction (cosine similarity
1.0) and to 7 decimal places in scale, and the subsampling stage's linear
output now matches NeMo's to 4 significant figures (previously off by
~500x). This does not fully fix decoding on its own — a separate bug
remains in the transformer encoder layers — but it eliminates a major,
independently-verified source of divergence from the reference model.

Bing Bong
The FastConformer encoder layer's depthwise conv was padded causally
(all padding on the left, via pad_causal_1d(x, kernel-1)), which is wrong
for this model's full-context (non-streaming) configuration. NeMo's
ConformerConvolution.depthwise_conv is a CausalConv1D, but for the
full-context encoder it is constructed with an explicit integer
padding=conv_context_size=(kernel_size-1)//2, and CausalConv1D treats an
int padding argument as SYMMETRIC left/right padding, not causal-only left
padding (only a `None` or explicit [left, right] argument produces
asymmetric/causal padding, and neither is used here). Padding causally
instead shifted every frame's receptive field, corrupting the residual
stream through all 24 encoder layers.

Isolated a single encoder layer's conv module against hook-captured NeMo
reference activations (same weights, same input) to localize this: with
causal padding the module's output was nearly uncorrelated with NeMo's
(cosine similarity 0.22); switching to symmetric padding raised that to
0.92, uniformly across all 93 frames (not concentrated at the sequence
edges, which rules out an off-by-one/boundary-only explanation and points
at the padding mode itself).

Combined with the prior frontend normalization fix, the model now
transcribes the reference test clip correctly end to end: "Well, I don't
wish to see it any more, observed Phoebe, turning away her eyes. It is
certainly very like the old portrait." — matching the NeMo reference
transcription exactly.

Bing Bong
Two follow-ups from the recent Parakeet TDT correctness review
(tmp/TODO.md items 1-2):

- decoder.cpp: remove an unconditional fprintf(stderr, "DECODE_DIAG ...")
  that fired on every single decode() call (frame 0 of every request),
  left over from the earlier blank-output debugging session. It also
  shadowed the outer `cfg` local with a redundant re-declaration.

- assets.cpp: stop silently swallowing all processor_config.json parse
  errors behind a bare `catch (...)` that only restored the preemphasis
  default, leaving sample_rate/feature_size/n_fft/win_length/hop_length
  silently on their struct defaults if anything about that file failed to
  parse. Now: skip parsing (and use defaults) only when the file is
  genuinely absent (resources.has_file(...) == false); if it's present but
  fails to parse or doesn't match the expected schema, that exception now
  propagates instead of being swallowed. Verified against three cases:
  file present and well-formed (unchanged behavior), file missing entirely
  (still falls back to defaults, though in practice resource-bundle file
  matching already makes this case unreachable today), and file present
  but malformed (previously silent, now throws
  "missing required json key: feature_extractor" as expected).

Rebuilt and reran parakeet_warm_bench against the reference clip: stderr
is now clean of DECODE_DIAG output and the transcription is unchanged
("Well, I don't wish to see it any more, observed Phoebe, turning away
her eyes. It is certainly very like the old portrait.").

Bing Bong
Follow-up from the recent Parakeet TDT correctness review (tmp/TODO.md
item 3, "Tier 1"). Neither of the two real bugs fixed in 4a10c48/3bf2d12
(missing frontend normalization, wrong conv padding mode) crashed or
produced an obviously malformed result — the model loaded, ran, and
produced plausible-shaped tensors throughout — so nothing in the existing
test suite would have caught either regression. This adds the cheapest
thing that actually would have: run the full offline pipeline against a
fixed clip and assert the decoded text matches the known-correct NeMo
reference transcription exactly.

- tests/parakeet_tdt/assets/2086-149220-0033.wav: a ~7.44s LibriSpeech
  test-clean clip (CC BY 4.0), with provenance documented in the
  accompanying assets/README.md.
- tests/parakeet_tdt/test_golden_transcription.cpp: loads the model, runs
  it against that clip, and asserts the output text is exactly "Well, I
  don't wish to see it any more, observed Phoebe, turning away her eyes.
  It is certainly very like the old portrait." — verified against the
  actual NeMo reference model output for this clip, not just eyeballed.
  Requires the real (multi-GB) model weights, which aren't present in a
  normal checkout/CI build; the test detects this at startup and exits
  125 (SKIP) rather than failing when the model or fixture audio is
  missing.
- CMakeLists.txt: wires the test into the existing ENGINE_BUILD_TESTS/
  ctest suite via the add_engine_unittest() helper, with
  SKIP_RETURN_CODE 125 configured so a missing-model environment shows as
  skipped rather than failed.

This is not a substitute for numerical layer-by-layer parity comparison
against NeMo (the "Tier 2" harness described in
tmp/todo-03-add-parity-regression-test.md, not yet built) — it only
catches regressions large enough to flip the final decoded text for this
one clip. It's deliberately cheap enough to run on every change, unlike a
NeMo-based numerical comparison which needs a NeMo install on top of the
model weights this test already needs.

Verified: `ctest -R parakeet_golden_transcription_test` passes (~7.4s on
CPU) against the current model checkout, and the skip path was confirmed
independently by running the binary against a nonexistent model path
(exits 125 as expected).

Bing Bong
The depthwise conv in build_fastconformer_conv_module was built with
use_bias=false, silently dropping weights.conv_dw_bias — the batch-norm
bias term fold_bn() computes and bakes into the depthwise conv
(-running_mean*scale + bn_bias). The scale/weight side of the BN fold was
applied correctly; only the additive bias term was being discarded.

This did not produce obviously wrong output — greedy/argmax decoding
turned out to be robust enough to this that the golden-clip transcription
was already correct without it — which is exactly why it went unnoticed
until a numerical (not just text-output) comparison against the real NeMo
model was built. Isolating the depthwise conv in a minimal single-op graph
and comparing against a hand-computed reference showed every output value
differed from the correct one by exactly the same constant offset, equal
in magnitude to the folded bias for that channel — a very clean signature
once found, but invisible from the top-level transcription alone.

Also exports build_encoder_layer (previously anonymous-namespace) via
encoder.h so a layer can be built and numerically verified in isolation
without duplicating the real production graph-building code — this is
what made isolating the bug to this one line tractable instead of only
being visible as an accumulated ~0.78 cosine similarity drift by the final
of 24 layers.

Verified: chaining 24 isolated single-layer builds — each fed the
previous one's real output, exactly like the production encoder does —
now stays at cosine similarity >= 0.999999 against the real NeMo model's
per-layer output through every one of the 24 layers (previously this
drifted down substantially by the later layers). Golden-clip transcription
remains correct on both CPU and CUDA.

Bing Bong
Adds the "Tier 2" numerical layer-by-layer comparison harness alongside
the existing golden-transcription test: a NeMo reference dump script, a
C++ dump tool built off the real production entry points (frontend
extraction, full encoder, and one isolated encoder layer via the newly
exported build_encoder_layer), and a numpy-only comparator that checks
cosine similarity and relative scale per stage.

This tool is what found and confirmed the encoder conv-module bias bug
fixed in 837382e — the golden-transcription test alone did not catch it,
since greedy decoding was robust enough to that particular corruption not
to change the final transcribed text on the one test clip. Comparing
actual activations, not just final decoded text, is a meaningfully
different and complementary check.

- tests/parakeet_tdt/parity/dump_nemo_reference.py: runs the real NeMo
  model with forward hooks, dumps mel features, pos_emb, every encoder
  layer's output, and the final encoder output as .npy files.
- tests/parakeet_tdt/parity/npy_io.h: minimal float32 .npy reader/writer
  (just enough for this tool; not a general-purpose npy library).
- tests/parakeet_tdt/parity/dump_cpp_reference.cpp: mirrors the NeMo dump
  using the real, unmodified C++ production entry points for mel features
  and the full encoder, plus one isolated single-layer build for granular
  comparison. Deliberately does not splice extra debug-output taps into
  the shared production graph — see the file's top comment for why (an
  extra "dead end" output tensor on that specific cached multi-thousand-
  node graph read back incorrect values during the original encoder
  debugging session, for reasons never fully root-caused; small isolated
  graphs and the unmodified production entry points both proved reliable
  and are what this tool sticks to).
- tests/parakeet_tdt/parity/compare_parity.py: numpy-only comparator,
  runnable without NeMo/torch installed.
- CMakeLists.txt: wires parakeet_parity_dump as a buildable tool under
  ENGINE_BUILD_TESTS (not registered as an add_test — it's a manual/
  pre-release check requiring a NeMo install on the Python side, not a
  CI gate).

Bing Bong
The parakeet_tdt loader was registered and working but had no installable
package entry (tools/check_loader_catalog_sync.py warned about this) and
wasn't listed anywhere in README.md or docs/community_models/models.md.

- tools/model_manager.py: add a parakeet_tdt ModelPackage entry. The repo
  (nvidia/parakeet-tdt-0.6b-v3) hosts both a Transformers-compatible
  safetensors checkpoint and a NeMo .nemo archive side by side; the
  package uses include_prefixes to fetch only the four files this loader
  actually reads (config.json, processor_config.json, tokenizer.json,
  model.safetensors), not the redundant multi-GB .nemo file. Verified the
  entry resolves against the real repo: `python3 tools/model_manager.py
  install parakeet_tdt` began fetching the correct files (config.json
  matched the existing local file's exact byte size; model.safetensors
  started downloading from the right repo) before being stopped partway
  through given the file size.
- docs/model_manager.md: add the package row to the recommended package
  table (required by the sync checker).
- docs/community_models/parakeet_tdt.md: new per-model doc following the
  existing community-model doc convention — build/run commands, what was
  validated and how (golden-transcription test + numerical parity
  harness), the two bugs found and fixed while getting there, and current
  known limitations (offline only, no streaming yet).
- README.md, docs/community_models/models.md: add the community-models
  table row.

python3 tools/check_loader_catalog_sync.py now passes with no warnings.

Bing Bong
Investigated adding NeMo cache-aware streaming support (causal conv with a
cross-chunk cache_last_time cache, chunked-limited attention with a
cache_last_channel KV cache) before writing any code, and found the actual
checkpoint doesn't support it in a meaningful way:

- nvidia/parakeet-tdt-0.6b-v3's encoder config has
  att_context_style="regular" and att_context_size=[-1, -1] — unlimited,
  fully bidirectional attention, not NeMo's "chunked_limited" streaming
  style.
- Calling NeMo's own encoder.setup_streaming_params() on this checkpoint
  doesn't error, but produces a degenerate configuration (~5.8 second
  chunks, a ~10000-frame attention cache) that wouldn't provide the
  latency or bounded-memory benefit real streaming is for.
- NVIDIA's own maintainers, asked directly about streaming with this
  model on its Hugging Face discussion page, pointed users at a
  different, dedicated cache-aware streaming architecture rather than
  confirming this checkpoint for the purpose.

Building a genuine chunked/causal streaming path against a model trained
with an unrestricted attention receptive field risks silently degrading
accuracy for a mode that wouldn't provide much practical benefit even
working correctly. This needs a different, purpose-trained checkpoint
(att_context_style="chunked_limited"), not more implementation effort
against this one.

This framework already has real streaming ASR: the nemotron_asr family
targets nvidia/nemotron-3.5-asr-streaming-0.6b, a same-size-class NVIDIA
checkpoint actually trained cache-aware with configurable chunk sizes
down to 80ms, and already implements IStreamingVoiceTaskSession. Pointed
to it from parakeet_tdt's known-limitations doc for anyone who lands here
specifically wanting streaming.

Bing Bong
pad_causal_1d, pad_causal_2d, and causal_conv_output_dim were leftover,
never-called scaffolding from an earlier attempt at streaming support
(compiler already flagged all three as unused). Confirmed streaming isn't
a viable follow-up for this specific checkpoint (see the preceding commit)
before removing them, so this isn't losing groundwork for later — it's
dead code for a direction that doesn't apply to this model. The one
function that was actually load-bearing, pad_symmetric_1d (the fix for
the encoder conv module's padding mode), is unaffected.

Verified: rebuilds warning-free, golden-transcription test and the
numerical parity harness both still pass unchanged.

Bing Bong
parakeet_warm_bench.cpp set ENGINE_TIMING_ENABLED/ENGINE_TIMING_FILE as
environment variables, but the logging subsystem doesn't read those env
vars anywhere in the codebase — every other *_warm_bench.cpp tool
(nemotron_asr, vibevoice_asr, qwen3_tts, etc.) wires up timing by calling
engine::debug::configure_logging(LoggingConfig{true, timing_path}) directly.
parakeet_warm_bench never did this, so its --timing-file output has been
silently empty since the tool was written — the frontend/encoder/decoder
breakdown was never actually available, only the top-level wall-clock
number.

Also fixed two stale/wrong keys in ordered_keys(): "parakeet.encoder_ms"
never matched what encoder.cpp actually logs
(debug::timing_log_scalar("parakeet_tdt.encoder_ms", ...) — note the
_tdt), and "parakeet.pre_encode_ms" is never emitted anywhere at all
(dead reference, presumably left over from a metric that was renamed or
removed). Replaced with the correct "parakeet_tdt.encoder_ms" and added
"parakeet_tdt.encoder.graph.compute_ms", which is what actually turned
out to matter for performance work (see the following commits): encoder
graph compute is ~93-96% of total wall time in every configuration
measured.

Bing Bong
…ights

Compared audio.cpp against the real NeMo/PyTorch reference on the same
machine (see docs/community_models/parakeet_tdt.md's new Performance
section for the full numbers and methodology). With the timing breakdown
now actually working (previous commit), encoder graph compute was clearly
~93-96% of total wall time in every configuration, so that's where
optimization effort actually matters — not the frontend or decoder.

Two changes, both using infrastructure that already existed (no new
inference code):

- Thread count: this machine is 6 physical / 12 logical cores; 8 threads
  (an arbitrary default) oversubscribes, 6 is consistently fastest, 12 is
  consistently worst. Core-count-dependent, not a universal number — sweep
  it on your own hardware.
- parakeet_tdt.matmul_weight_type=q8_0 (an existing but previously
  untested session option): ggml's CPU backend has heavily hand-tuned
  Q8_0 dot-product kernels; measured faster than F16/BF16 here, not just
  faster than F32.

Net: CPU 1247.5ms -> 830.9ms (1.60x), CUDA 167.4ms -> 132.1ms (1.27x),
transcription unchanged in every configuration. CPU with q8_0 is now
~3% faster than PyTorch's own CPU number on this clip.

Quantified the accuracy cost via the numerical parity harness rather than
just trusting "the transcription still matched": enc_out cosine similarity
vs the NeMo reference drops from 0.972258 (native F32) to 0.968028 (q8_0)
— comparable in size to the float32 accumulation noise already present
between an isolated single-layer graph and the full 24-layer production
graph, not a cliff, but a real, measured, non-zero cost. Only validated
against one clip, so this is documented as a strongly-recommended opt-in
session option, not changed as the default — that would need broader
validation first.

- dump_cpp_reference.cpp: add --matmul-weight-type so the parity harness
  can numerically compare any weight storage type against the NeMo
  reference, which is what produced the 0.972258 -> 0.968028 numbers
  above.
- docs/community_models/parakeet_tdt.md: new Performance section.
- tests/parakeet_tdt/parity/README.md: document the new flag.

Bing Bong
…re it as a net loss

Wires the FastConformer encoder's relative-position self-attention onto
ggml_flash_attn_ext_with_bias_mask (already used by
common_relative_attention.cpp's specialized_flash path, but unused by any
production model) via a new parakeet_tdt.encoder_flash_attention session
option, off by default.

Validated correct via the numerical parity harness (enc_out cosine 0.972325
vs. 0.972258 for the existing path, layer_0 unchanged) but measured
consistently slower end to end, not faster, on both CPU and CUDA, on both
the 7.4s reference clip and a synthetic 59.5s clip. Documented as an
opt-in for other hardware rather than removed, since it's now proven
correct infrastructure, but explicitly not recommended based on what was
actually measured here.

Bing Bong
…uant bucket

pointwise_conv1/pointwise_conv2 in the FastConformer conv module are
kernel_size=1 Conv1d layers — mathematically Linear layers, and run through
LinearModule/mul_mat in this port's encoder, not a real conv op. They were
being loaded under conv_weight_type (capped at f16, no q8_0) instead of
matmul_weight_type, silently excluding ~3*hidden^2 params/layer across all
24 layers from quantization when matmul_weight_type=q8_0 was requested.

Found by cross-referencing CrispStrobe/CrispASR's FastConformer/Parakeet
port, which documents hitting and fixing the identical quantizer
misclassification for the same class of weights. Verified via the
numerical parity harness (no change at the native default; q8_0 accuracy
unchanged within noise) and the golden-transcription test (exact match on
both CPU and CUDA). ~10% additional CPU wall-time reduction on top of the
existing q8_0 + thread-tuning result; no clear CUDA change.

Bing Bong
… layer

load_encoder_layer now reads the raw F32 q_proj/k_proj/v_proj rows and
concatenates them at load time into a single [3*hidden, hidden] weight
(row-major Linear layout concatenates directly along the output-feature
axis, no interleaving needed), populating the previously-dead qkv_weight
field on the shared AttentionWeights struct. build_encoder_layer now does
one Linear(hidden, 3*hidden) + slice instead of three separate
Linear(hidden, hidden) calls.

Inspired by CrispStrobe/CrispASR crediting the same fusion as part of
their FastConformer encoder win. Validated via the parity harness to be
numerically identical to the pre-fusion path at both native (cosine
0.972258) and q8_0 (cosine 0.967974) precision — a pure reorganization,
not a precision trade — and the golden-transcription test still matches
exactly on CPU and CUDA. No clear wall-time change measured on this
hardware (dispatch overhead isn't the bottleneck at this graph size here),
but it's zero-cost to carry and may pay off on other backends or under
batched/concurrent serving conditions not exercised by this benchmark.

Bing Bong
…graph

engine::runtime::optimize_graph (src/framework/runtime/graph_optimizer.cpp)
is a real, unit-tested graph rewrite pass — folds broadcast repeats into
consuming ops, elides pure-metadata reshape/view/cont chains, elides
no-ops — but had zero callers anywhere in the codebase: not
graph_executor.cpp, not any model family. Wired into
ParakeetEncoderRuntime::ensure_graph, called once per distinct input
length right after ggml_build_forward_expand, before allocation, so the
cost is amortized across every subsequent encode() call that reuses the
cached graph.

Effect on the reference clip's encoder graph: 2854 nodes -> 1425 (1323
metadata-only ops elided, 106 broadcast repeats folded). Validated via
the parity harness (enc_out cosine 0.972258, bit-identical to before) and
the golden-transcription test (exact match, both backends). Speed,
measured via an interleaved A/B using the optimizer's existing
ENGINE_GRAPH_OPTIMIZER=0/1 env toggle to cancel out this machine's
background-load drift: CPU q8_0 encoder time ~812ms -> ~726ms (~10%,
ahead in 5/6 paired runs). CUDA: no measurable difference, expected,
since GPU options intentionally skip metadata-only elision (the
scheduler-based backend still needs those nodes).

Bing Bong
Traced parakeet_warm_bench with nsys profile --trace=cuda: 16068
cudaLaunchKernel calls, 4265 cudaStreamSynchronize calls (47% of total CUDA
API time), and zero cudaGraphLaunch/cudaGraphInstantiate calls anywhere,
despite GGML_CUDA_GRAPHS=ON in this build and the encoder/decoder graphs
already satisfying the capture prerequisite (same cached ggml_cgraph*
reused across every call at a given shape).

Root cause traced into ggml_cuda_graph_set_enabled
(external/ggml/src/ggml-cuda/ggml-cuda.cu): ggml unconditionally disables
CUDA graph capture below compute capability 8.0 (Ampere). This machine's
GTX 1650 is Turing, compute capability 7.5. Not a bug or misconfiguration
in this codebase - the build already does everything right - but it
retroactively explains why flash attention and fused QKV (both already
documented above) measured no CUDA benefit here: they target exactly the
per-op dispatch overhead that graph replay exists to hide, which never
activates on this card. Documented as a reason to re-measure both on
Ampere-or-newer hardware rather than trust this machine's null result.

Bing Bong
Three copy-elimination changes in the encoder layer, all bit-exact:

- Q/K/V are read out of the fused QKV result as strided 4d views already in
  per-head order, rather than slicing the feature axis and forcing each
  slice contiguous. Those conts were not optional once you slice, because
  reshape_heads goes through ggml_reshape which asserts contiguity — so the
  old path copied all three projections in full every layer just to satisfy
  a reshape, and MatMulModule then re-materialized k and v anyway.

- The Transformer-XL relative shift is expressed directly as the strided
  view it mathematically is, replacing the generic concat + cont + slice +
  cont sequence (~1.1 MB of copying per layer, most of it discarded by the
  following slice). It also folds the trailing slice in for free. See the
  derivation above relative_shift_view.

- The convolution module's BTC->BCT->BTC transpose pair around
  pointwise_conv1 cancelled out exactly and is gone; only the depthwise
  conv actually needs BCT.

Encoder graph drops 1425 -> 1257 nodes and cont traffic 123 -> 84 MB.
Encoder output is bit-identical across all 95232 enc_out floats.

Measured on its own this is only ~0.3% of encode time on a 6-core i7-9750H
(drift-cancelling interleaved A/B, median of 6 passes): the encoder is
overwhelmingly GEMM-bound, with ~74% of cycles inside tinyBLAS. Kept
because it is free, removes work that scales with sequence length, and the
node-count reduction helps backends where dispatch is not free.

Bing Bong
Three constant scales ran as their own ggml_scale pass over the activations:
the 0.5 residual half-step at the end of each feed-forward branch (twice per
layer, 48 passes over [seq, 1024]) and RelPositionalEncoding's xscaling by
sqrt(d_model) after the subsampling projection. All three are folded into the
producing Linear's weights at load time instead, removing all 49 SCALE nodes
from the encoder graph (1257 -> 1208).

The fold is exact rather than approximate, because both constants are powers
of two. Scaling a float by a power of two only adjusts the exponent, and
every term of the dot product is scaled identically, so sum(0.5*w_i*x_i)
equals 0.5*sum(w_i*x_i) bit for bit. It also survives quantisation: q8_0
derives its block scale from max|w|, so halving every weight in a block
halves the block scale and leaves the stored integers untouched.

For the subsampling projection both weight and bias are scaled, since the
xscaling applied after the linear, not before it.

Verified bit-identical across all 95232 enc_out floats.

Bing Bong
ensure_graph() reuses any cached graph whose capacity merely exceeds the
request, and encode() zero-pads the input up to that capacity. Zero input
does not stay zero: every subsampling conv carries a bias, so the padded
tail reaches the encoder stack as nonzero garbage. The attention mask was
unconditionally all-zeros, so real frames attended to that garbage, and
because softmax normalizes across keys it rescaled every real frame's
attention.

Not theoretical: feeding a 7.4s clip to a graph prepared with a 60s audio
contract dropped an entire sentence from the transcription.

  matched    "...turning away her eyes. It is certainly very like the old portrait."
  oversized  "...turning away her eyes."

and this commit restores the full, correct text in the oversized case.

Masking is by key column only, never by query row, so the padded query rows
keep a full set of valid keys and no softmax row degenerates to all -inf
and produces NaN. Their outputs are discarded afterwards regardless.

Matched-size graphs are unaffected: no mask entries are written when
valid == capacity, and enc_out stays bit-identical across all 95232 floats.

Bing Bong
…ersized

The encoder graph always runs at the capacity it was built for, because
encode() zero-pads the input up to that capacity. ensure_graph() reused any
cached graph whose capacity merely exceeded the request, so a session
prepared with a long audio contract paid the long price on every short clip
that followed.

Measured on a 6-core i7-9750H, 7.4s clip:

  matched-capacity graph              1018 ms
  60s-capacity graph                 10928 ms   (10.7x)
  60s contract, with this change      1140 ms

Rebuilding is cheap by comparison — ~400 ms, and dominated by recomputing
the 24 per-layer positional projections; the allocation itself is ~0.4 ms.
So rebuilding wins outright on the very first call whenever the mismatch is
more than a few percent, and wins by more on every call after that.

A 10% oversize tolerance keeps a stream of clips whose lengths wobble
slightly from rebuilding every call, and caps the wasted compute at roughly
the same fraction.

enc_out stays bit-identical when the capacity already matched, and the
golden transcription test passes.

Bing Bong
…ology

Documents where the encoder's CPU time actually goes (~74% in llamafile's
tinyBLAS f32 GEMM), the measured GEMM ceiling per weight type at 1 and 6
threads, and the graph-capacity bug — both its 10.7x cost and the dropped
sentence it caused.

Also records two optimizations that looked obvious and measured as losses,
so they don't get re-attempted:

- Padding the token axis to clear tinyBLAS's fast-path guards really does
  speed the attention GEMMs up ~2x, but slows every weight GEMM ~6%, and the
  weight GEMMs dominate: ~233 ms spent to save ~91 ms.
- The single-core GEMM is not memory-bound at all — throughput is flat from
  0.25 MB to 32 MB of weights — so it is kernel-bound at ~22% of AVX2 peak
  inside vendored code, and shrinking weights only helps when it selects a
  genuinely faster kernel (q8_0 does, f16 does not).

Finally, documents the benchmarking method, because this machine drifts 20-30%
between cold and thermally saturated. The ABBA-with-mean estimator cancels
linear drift; the min() estimator an earlier harness used does not, and
produced a confident 5-8% "regression" that reversed once corrected.

Bing Bong
… numbers

Replaces the earlier hand-wave with per-op measurements at 1 and 6 threads,
and separates the two padding variants: padding the token axis (clear loss,
~233 ms spent to save ~91 ms) versus padding only the key axis (net ~+0.5%
at 6 threads, ~+1% at 1). Records why neither is worth doing, including that
changing AV's k dimension gives up the bit-exactness the other changes keep.

Bing Bong
The two existing tiers both run on a single English clip, which is not
enough to decide whether a weight-storage type is safe. Greedy decoding
absorbs a lot of numerical drift, and cosine similarity on enc_out is not a
transcription: q8_0 scores 0.9964 there and is byte-identical on the golden
clip, which reads as "free".

It is not. Across 35 FLEURS clips in 7 languages, q8_0 matched the f32
transcription exactly on only 91.4% of clips and cost +0.4 points of absolute
WER, concentrated in particular languages (Estonian 16.4% -> 18.4%) rather
than spread evenly. One clip in one language would have reported that as a
clean pass.

This harness pulls FLEURS test clips for the 24 European languages the model
supports that FLEURS covers, and reports per weight type: exact-match rate
against f32, WER against f32, absolute WER against the human reference, and a
per-language breakdown so concentrated damage is visible.

fetch_fleurs.py deletes each ~150-400 MB parquet right after extracting the
few clips it keeps, and records per-language progress, so it is resumable and
does not accumulate ~10 GB. compare_weight_types.py runs all clips for a type
in one process via --audio-sequence so the quantized weights are built once.

Bing Bong
Replaces "only validated against one clip" with an actual measurement: 120
FLEURS clips, 5 per language, across the 24 European languages the model
supports that FLEURS covers.

The result reframes q8_0. It changes 8.3% of transcripts, which the previous
single-clip evidence (byte-identical) completely missed — but absolute WER
against the human reference is flat, 0.1331 vs f32's 0.1338. The churn is
mostly cosmetic (T Rex -> T-Rex), and where real words change q8_0 is
sometimes right where f32 was wrong. So the 1.79x is not paid for in quality
on this corpus; it is paid for in reproducibility against an f32 baseline.

Also records bf16 as the low-thought option (1.14x, 99.2% identical), f16 as
pointless (no speedup, slower than f32 single-core), and corrects the thread
count guidance: 12 threads is 3.1% faster than 6, not "consistently worst".
That earlier claim was an artifact of sequential before/after timing on a
drifting machine and does not survive a controlled A/B; the GEMM
microbenchmark agrees with the corrected result.

Bing Bong
The weight-precision bullet still asserted that both reduced-precision types
measured slower than F32, which the 24-language study contradicts for BF16:
it is 1.14x faster. F16 remains a non-speedup (and is genuinely slower than
F32 single-core), so the underlying point — that the win tracks which ggml
kernels are well optimized rather than bit width — survives, but the specific
claim did not.

Bing Bong
… GPU caveat

Records the actual output rather than only the summary: every clip's
reference plus what native/f16/bf16/q8_0 each transcribed, 120 clips across
24 languages. That means the aggregate numbers can be re-derived, or
re-scored under a different WER normalization, without re-running the study
or re-downloading FLEURS. Includes the per-language table and samples of the
10 clips where q8_0 diverges.

Also states the test hardware precisely, and how far each half generalizes.
The CPU is an ordinary 6-core i7-9750H, so the CPU findings should carry to
comparable AVX2 machines. The GPU does not generalize: it is a GTX 1650 with
Max-Q Design — Turing, compute capability 7.5, 4 GB, and a 35 W power limit.
That is about the weakest CUDA device this model realistically runs on, and
its compute capability is below the 8.0 threshold where ggml enables CUDA
graph capture at all. The several optimizations that measured as "no change"
on CUDA are statements about this card, not about the optimizations, and the
docs now say so wherever those results appear.

All of native/f32/f16/bf16/q8_0 verified transcribing correctly end to end;
they were already supported, no code change needed.

Bing Bong
The model doc had grown to 489 lines, 4.2x the next largest doc in
docs/community_models/ (glm_tts 116, outetts 104, vietneu_tts 66), and someone
who just wanted to run the model had to scroll past benchmark methodology,
rejected experiments, and CUDA-graph hardware analysis to find it.

The repo already has the right shape for this: long writeups live in
docs/reports/ (glm_tts_validation.md, outetts_validation.md, and the existing
*_performance.md reports), with the community_models entry kept short. This
follows that split — 162 lines of user-facing doc, 405 lines of report — and
moves no content out of the tree, only between files.

Also removes references that only resolve on the machine this was developed
on, or in projects that are not this one:

- a comparison to a locally cloned copy of another Parakeet port
- the benchmark harness, which the methodology section cited by a path under
  the untracked tmp/ directory. Rather than drop the reference, the harness is
  now checked in under tests/parakeet_tdt/bench/ with a README explaining why
  the ABBA-with-mean estimator is required here and what breaks without it.
- a stale pointer in the golden-transcription test to an untracked planning
  note, repointed at the parity harness it actually meant

Third-party attribution is kept where it is genuine provenance: the
conv-pointwise weight misclassification and the fused-QKV idea were both found
by cross-referencing a public project, and citing that is honest. What is
removed is the passage that leaned on that project's own unmeasured benchmark
numbers to justify a decision here; that argument now stands on this repo's
own reasoning.

Bing Bong
The harness hardcoded BackendType::Cpu, so it could only ever answer "does
this match NeMo on CPU". It could not answer the question that actually
matters when a change touches tensor layout: does CPU still agree with CUDA.

That gap was live. The recent switch to reading attention operands as strided
ggml_view_4d views is verified bit-exact on CPU, where MatMulModule
materializes the operands — but CUDA runs different kernels over the same
graph, and nothing checked it. Transcription matching on both backends is
suggestive, not proof.

With --backend cuda, it is now checked directly: enc_out agrees between CPU
and CUDA to cosine 0.99999988 (relative RMS 3.5e-06, max absolute 2.6e-06),
which is float32 accumulation-order noise between different kernel
implementations, and mel features are bit-identical.

Bing Bong
Records how to use --backend to diff CPU against CUDA directly, what to
expect (float32 accumulation noise, not bit-exactness, because different
kernels sum in different orders), and the current measured agreement.

Bing Bong
Adds the backend/memory section the report was missing. Both backends
re-validated on the merged tree; CPU and CUDA agree on enc_out to cosine
0.99999988, which is what confirms the view-based attention rewrite is
correct under CUDA's kernels and not only under the CPU path where its
bit-exactness was measured.

Memory numbers were absent entirely and CONTRIBUTING asks for them. Two are
worth knowing: q8_0 cuts peak host RSS 3.6x (3843 -> 1078 MiB), a larger
practical difference than its 1.79x speed win on constrained machines; and
peak VRAM is 2642 of 4096 MiB, on a card where the PyTorch reference cannot
load the model at all.

The graph right-sizing fix also carries to CUDA at a similar factor: ~940 ms
to ~126 ms, about 7.5x, for the mismatched-capacity case.

Bing Bong
…pass

The parity evidence in these docs was inherited: bit-exactness measured
against an internal pre-change baseline, with the NeMo reference dump itself
lost to a reboot partway through. Regenerated it (torch 2.13.0+cu130, nemo
2.7.3) and re-ran against the current tree.

All three stages pass on CPU and CUDA. enc_out cosine is 0.972258 on both,
identical to the value measured before any of the optimizations in this
report — which is the strongest available statement that they changed no
math, since it is checked against NeMo directly rather than against our own
earlier output.

Bing Bong
The quoted CPU tuned figure (~750 ms) was inherited from a session that
predated the optimization pass, and this machine drifts 20-30% thermally, so
it was neither current nor safely comparable to the rows around it.

Re-measured with the drift-cancelling harness (ABBA, 4 passes, median of 5
iterations per run, machine idle):

  default (8 threads, f32)   ~1245 ms CPU / ~169 ms CUDA
  tuned (12 threads, q8_0)    ~715 ms CPU / ~131 ms CUDA
  ratio                          1.72x / 1.31x

The default rows land within a couple percent of the historical figures
(1247.5 / 170.6), which is a useful check that the older table still means
something. The CPU tuned row is the one that moved: ~715 ms rather than
~750 ms, i.e. 10.4x real-time rather than 9.9x.

Also labels the historical table as a progression whose rows were each
measured in their own session, so it is not read as row-to-row comparable.

Bing Bong
@0xShug0

0xShug0 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@dleiferives Thank you for your contribution! I'd appreciate it if you could update the spec to follow the current model_specs_v1/ format, but without adding a schema version number. The existing code will simply ignore the new v1 fields, so this won’t break anything. This is just to make the eventual migration easier.

The rows were labelled "default settings" and "q8_0 + tuned threads", which
does not make clear that both are *this port* and only the first row is the
Python reference — the question "is default the Python one?" came up reading
it, so the labelling was doing real damage on the table a reader hits first.

Splits implementation and settings into separate columns, and states the
comparison the table exists to make: untuned this port is slower than PyTorch
on CPU (~1245 ms vs 857.7 ms), tuned it wins by ~1.2x, and on this GPU there
is no comparison because PyTorch cannot load the model in 4 GB at all.

Also spells out what "untuned" means, so it cannot be read as a deliberately
handicapped baseline: --threads defaults to 8 and matmul_weight_type defaults
to native, which is f32 for this checkpoint.

Bing Bong
Requested in review. parakeet_tdt was the only family present in
model_specs/ but missing from model_specs_v1/ (34 of 35), so this closes
that gap ahead of the eventual migration.

Deliberately carries no version/schema_version field, per the review note —
none of the existing 34 v1 specs has one either.

Nothing existing changes behaviour:

- model_specs/parakeet_tdt.json is left exactly as it was. Every other family
  keeps its minimal legacy spec (family + sources) alongside the enriched v1
  file, and this matches that.
- The `sources` block in the v1 spec is byte-identical to the legacy one, so
  the resource resolution the loader actually performs is unchanged.
- Verified empirically rather than assumed: running the loader against a spec
  file containing the full v1 field set transcribes correctly, confirming the
  extra fields are ignored.
- `tools/model_manager_v2.py --specs-dir model_specs_v1 list` now lists
  parakeet_tdt, where before it was absent.

Modelled on nemotron_asr (the closest analogue — NVIDIA ASR loaded from a
safetensors checkpoint) with the community-model package shape used by
glm_tts/outetts/vietneu_tts: status "community", no package_defaults, and the
Hugging Face snapshot declared per-package. Top-level key set now matches
those peers exactly.

Also fixes a real gap the migration surfaced: `encoder_flash_attention` is
accepted by the session but was never advertised in the loader's option list,
so it was undiscoverable. session.cpp, loader.cpp and the v1 spec now declare
the identical set of seven session options.

Bing Bong
@dleiferives dleiferives changed the title In progress: Add community_models/parakeet_tdt: NVIDIA Parakeet-TDT 0.6B v3 ASR Add Parakeet-TDT 0.6B v3 (FastConformer-TDT ASR) Jul 27, 2026
@dleiferives

dleiferives commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@dleiferives Thank you for your contribution! I'd appreciate it if you could update the spec to follow the current model_specs_v1/ format, but without adding a schema version number. The existing code will simply ignore the new v1 fields, so this won’t break anything. This is just to make the eventual migration easier.

Yeah, of course. Just implemented & updated pr body to denote work and results more accurately.

@dleiferives
dleiferives marked this pull request as ready for review July 27, 2026 17:00
@0xShug0

0xShug0 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

@dleiferives You can check my old impl of https://github.com/0xShug0/audio.cpp/tree/preview/parakeet_tdt_old. That implementation was done before the 0.1 release and isn't very polished, but it may still be useful.

@0xShug0

0xShug0 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@dleiferives Please check #128. The example is Confucius4-TTS.

@dleiferives

Copy link
Copy Markdown
Contributor Author

Awesome, that is super helpful thank you.

Reference: 0xShug0/audio.cpp preview/parakeet_tdt_old

Adapted: decoder emission-frame capture, persistent predictor-state lifecycle, and token-piece-to-word timestamp merging
Reference: 0xShug0/audio.cpp preview/parakeet_tdt_old

Adapted: bounded left-center-right window scheduling, center-only incremental decoding, retained predictor state, partial transcript merging, and long-form window reuse
Reference: 0xShug0/audio.cpp preview/parakeet_tdt_old

Adapted: continued bounded-window streaming lifecycle work; framework event delivery and public option naming were rewritten for the current API
Use the schema-v1 audio_chunk_threshold_sec contract from issue 0xShug0#128 and enforce the spec's duration minima in the runtime parser.
# Conflicts:
#	CMakeLists.txt
#	src/framework/runtime/registry.cpp
@dleiferives

Copy link
Copy Markdown
Contributor Author

Implemented the requested Parakeet follow-up and aligned it with the schema-v1 flow described in #128, using Confucius4-TTS as the reference shape.

What changed:

  • model_specs/parakeet_tdt.json is now the single source of truth and declares schema_version: 1, metadata, tasks/modes, package sources, and the complete option contract.
  • Removed the duplicate model_specs_v1 entry and the repetitive Parakeet loader.cpp / loader.h path. The loader factory now lives beside the session and uses the generic spec-backed loader.
  • Normalized the old chunk setting to audio_chunk_threshold_sec, matching the naming rules in Using Model Spec v1 for New Models #128, and enforce the declared duration minima at runtime.
  • All declared enum options are now validated centrally across both offline and streaming session creation, so an invalid value cannot be silently ignored merely because it belongs to the other mode.
  • Added timestamp-aware TDT decoding, word-span assembly, bounded-window long-form processing, and buffered left/center/right streaming with incremental partial/final transcript delivery.
  • The streaming behavior is described accurately as buffered/pseudo-streaming: this checkpoint uses unlimited bidirectional attention and is not a native cache-aware streaming model.
  • After the latest upstream merge, Parakeet is also registered as a first-class selectable model in the new composite build system; the generated registry includes its spec-backed loader correctly.

I reviewed preview/parakeet_tdt_old as suggested and adapted the useful pieces—emission-frame timing, persistent predictor state, bounded window scheduling, center-only incremental decoding, and transcript merging—to the current framework APIs. The relevant commits explicitly cite that branch where implementation ideas were carried forward.

Validation on the merged tree:

  • model_spec_system_test passed
  • tdt_decoder_duration_loop_test passed
  • parakeet_golden_transcription_test passed
  • parakeet_streaming_transcription_test passed
  • Focused CPU build passed with AUDIOCPP_MODEL_SET=custom and AUDIOCPP_MODELS=parakeet_tdt
  • Full composite configuration includes parakeet_tdt, and the loader/catalog synchronization check passes

The main schema/streaming work is in 5ad45e3 through 0a93eb4; f1e1333 resolves the latest upstream composite-build merge while preserving the Parakeet registration.

@0xShug0
0xShug0 merged commit a4f4a45 into 0xShug0:main Jul 28, 2026
4 checks passed
@0xShug0

0xShug0 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

@dleiferives Merged! Thank you for contributing the new model!

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.

2 participants