# Canary-Qwen 2.5B NVIDIA's [`nvidia/canary-qwen-2.5b`](https://huggingface.co/nvidia/canary-qwen-2.5b) ported to transcribe.cpp. A NeMo SALM (Speech-Augmented Language Model): a 32-layer FastConformer audio encoder (`d_model=1024`, 16 heads) feeds audio embeddings into a Qwen3-1.7B causal LM (28 layers, `hidden_size=2048`, `intermediate_size=6144`) via audio-token injection at a sentinel position in the prompt. ## What it's for Offline English speech-to-text. Takes a 16 kHz mono WAV and produces a transcript via greedy decoding. English only; no translation, no explicit PnC toggle (SALM applies punctuation and capitalization implicitly when the audio supports it). See NVIDIA's [model card](https://huggingface.co/nvidia/canary-qwen-2.5b) for training data, intended use, and upstream evaluation. Licensed CC-BY-4.0. Ported from upstream commit [`b1469e1`](https://huggingface.co/nvidia/canary-qwen-2.5b/commit/b1469e1bba1cfe140205529c79c434ca47180960), pinned 2026-05-15. ## Input limits Accepts up to about **54 minutes** of 16 kHz mono audio per call — the 40,960-token Qwen3 decoder context is the binding limit. That ceiling bounds memory and is far longer than any normal clip; audio past it is rejected up front with `TRANSCRIBE_ERR_INPUT_TOO_LONG` rather than silently truncated. Lowering `--n-ctx` lowers the limit, and `transcribe_session_get_limits()` reports the exact per-session value. See the [input-length contract](../input-limits.md). ## Download | Quantization | Download | Size | WER (LibriSpeech test-clean) | | --- | --- | ---: | ---: | | BF16 | [canary-qwen-2.5b-BF16.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-BF16.gguf) | 4.73 GB | 1.63% | | F16 | [canary-qwen-2.5b-F16.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-F16.gguf) | 4.73 GB | 1.63% | | Q8_0 | [canary-qwen-2.5b-Q8_0.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-Q8_0.gguf) | 2.61 GB | 1.63% | | Q6_K | [canary-qwen-2.5b-Q6_K.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-Q6_K.gguf) | 2.06 GB | 1.63% | | Q5_K_M | [canary-qwen-2.5b-Q5_K_M.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-Q5_K_M.gguf) | 1.85 GB | 1.63% | | Q4_K_M | [canary-qwen-2.5b-Q4_K_M.gguf](https://huggingface.co/handy-computer/canary-qwen-2.5b-gguf/resolve/main/canary-qwen-2.5b-Q4_K_M.gguf) | 1.62 GB | 1.63% | WER measured on the full LibriSpeech `test-clean` split (2620 utterances) with the Whisper-style English text normalizer and jiwer 3.x. The same-machine NeMo SALM reference run (CPU torch, dither=0.0, greedy `model.generate`) lands at **1.61%** with 95% bootstrap CI [1.47%, 1.75%]; NVIDIA's published number is 1.60% (within the same CI). All six GGUF presets land at exactly 1.63% (`+0.02pp` over our same-machine REF run). The remaining `+0.02pp` C++ vs REF gap is BF16 weight-precision cascade noise: of 2620 utterances, only 21 (0.8%) differ post-normalizer, all classic small-margin token flips (homophones, word-boundary flips, function-word substitutions). ## Quick Start ```bash cmake -B build cmake --build build build/bin/transcribe-cli \ -m models/canary-qwen-2.5b/canary-qwen-2.5b-Q8_0.gguf \ samples/jfk.wav ``` If your audio is not already 16 kHz mono WAV, convert it first: ```bash ffmpeg -i input.mp3 -ar 16000 -ac 1 output.wav ``` CLI flags: - `-l en` (or omit): English is the only supported language; passing any other code returns `TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE`. - No `--task`, `--target-language`, or `--pnc` toggles. SALM is ASR-only, English-only, and applies PnC implicitly. ## Performance Cells are wall-clock latency (mean over 3 iterations after 1 warmup), with speedup over realtime in parentheses. Units: `ms` below 1 s, `s` above (2 decimal places). ### Apple M4 Max | Backend | Sample | Q8_0 | Q4_K_M | | ------- | ------------ | ---------------: | ---------------: | | Metal | jfk (11.0s) | 243.9 ms (45.1×) | 232.4 ms (47.3×) | | Metal | dots (35.3s) | 1.02 s (34.5×) | 911.7 ms (38.8×) | | CPU | jfk (11.0s) | 1.42 s (7.7×) | 1.06 s (10.4×) | | CPU | dots (35.3s) | 5.14 s (6.9×) | 4.00 s (8.8×) | macOS 26.4.1, transcribe.cpp `1b19945`. ### AMD Ryzen 7 PRO 4750U | Backend | Sample | Q8_0 | Q4_K_M | | ------- | ------------ | ---------------: | ---------------: | | Vulkan | jfk (11.0s) | 2.41 s (4.6×) | 2.11 s (5.2×) | | Vulkan | dots (35.3s) | 9.72 s (3.6×) | 8.48 s (4.2×) | | CPU | jfk (11.0s) | 4.73 s (2.3×) | 3.43 s (3.2×) | | CPU | dots (35.3s) | 18.42 s (1.9×) | 13.51 s (2.6×) | Fedora Linux 43, transcribe.cpp `51db32d`. Vulkan device: `AMD Radeon Graphics (RADV RENOIR)`. Benchmark reproduction: ```bash uv run scripts/bench/run.py \ --models canary-qwen-2.5b \ --quants q8_0,q4_k_m \ --samples jfk,dots \ --backends metal,cpu,vulkan \ --iters 3 --warmup 1 \ --name canary-qwen-2.5b-publication ``` ## Numerical Validation transcribe.cpp is validated tensor-by-tensor against NeMo SALM (`nemo.collections.speechlm2.SALM` 2.7.3) on `samples/jfk.wav` with the strict CPU backend, BF16 weights promoted to F32 at load time. All 16 checkpointed tensors fall within family tolerance, and the BF16 transcript matches the reference verbatim (`And so my fellow Americans ask not what your country can do for you ask what you can do for your country`). Tolerances are pinned in `tests/tolerances/canary_qwen.json` with a detailed `_comment` block naming the precision regime, the two implementation gotchas (BF16 mel filterbank in NeMo's preprocessor, forced F32 promotion of F16 depthwise conv kernels on CPU), and the mechanism behind every widened entry. Last validated at commit [`6f6c699`](https://github.com/handy-computer/transcribe.cpp/tree/6f6c699). | Field | Value | | --- | --- | | Reference | NeMo SALM 2.7.3 (`nvidia/canary-qwen-2.5b`) | | Dump script | `scripts/dump_reference_canary_qwen_nemo.py` | | Manifest | `tests/golden/canary_qwen/canary-qwen-2.5b.manifest.json` | | Tolerances | `tests/tolerances/canary_qwen.json` | | Command | `uv run scripts/validate.py all --family canary_qwen --variant canary-qwen-2.5b` | Selected tensors (observed on CPU, strict backend; see tolerance file for budgets): | Tensor | Shape | Max abs diff | Mean abs diff | Notes | | --- | --- | ---: | ---: | --- | | `enc.mel.in` | `[128,1101]` | `6.724e-01` | `5.242e-05` | NeMo preprocessor's BF16 fb/window matrices vs C++ F32 STFT | | `enc.pre_encode.out` | `[138,1024]` | `2.610e+02` | `5.795e-01` | Output of the conv subsampler; large extreme-bin spikes from the mel difference are absorbed here, then drained by the next LayerNorm | | `enc.block.0.out` | `[138,1024]` | `1.653e+01` | `2.448e-02` | First FastConformer block | | `enc.block.16.out` | `[138,1024]` | `2.528e+01` | `7.975e-02` | Mid-encoder | | `enc.block.31.out` | `[138,1024]` | `7.921e-01` | `1.875e-02` | Final FastConformer block | | `enc.final` | `[1024,138]` | `7.921e-01` | `1.875e-02` | Encoder output (transposed) | | `perception.proj.out`| `[138,2048]` | `3.520e+00` | `4.314e-02` | Audio→LM width projection | | `dec.token_emb` | `[15,2048]` | `0.000e+00` | `0.000e+00` | Pure embedding lookup | | `dec.audio_injected` | `[152,2048]` | `3.520e+00` | `3.917e-02` | Audio-tokens scattered into the prompt sequence | | `dec.block.0.out` | `[152,2048]` | `3.355e+00` | `4.365e-02` | First Qwen3 LM block | | `dec.block.14.out` | `[152,2048]` | `1.506e+01` | `1.190e-01` | Mid-LM | | `dec.block.27.out` | `[152,2048]` | `1.112e+02` | `9.333e-01` | Final LM block (accumulated) | | `dec.out_before_head`| `[152,2048]` | `1.566e+01` | `4.152e-02` | Pre-head hidden state | | `dec.logits_raw.gen0`| `[151936]` | `5.226e-01` | `6.571e-02` | Greedy step 0 logits | | `dec.logits_raw.gen8`| `[151936]` | `1.514e+00` | `2.116e-01` | Greedy step 8 logits (mid-generation, exercises KV cache write/read) | For the full porting writeup including the SALM trace, the audio-injection scatter contract, and the BF16-vs-F32 weight precision investigation, see [`docs/porting/families/canary_qwen.md`](../porting/families/canary_qwen.md). ## Reproduction ### Convert ```bash uv run --project scripts/envs/canary_qwen \ scripts/convert-canary-qwen.py nvidia/canary-qwen-2.5b \ --revision b1469e1bba1cfe140205529c79c434ca47180960 ``` ### Quantize ```bash uv run scripts/quantize-all.py models/canary-qwen-2.5b/canary-qwen-2.5b-BF16.gguf ``` ### Validate ```bash uv run scripts/validate.py all --family canary_qwen --variant canary-qwen-2.5b ``` ### Score WER ```bash PRESET=BF16 uv run scripts/wer/run.py \ --model models/canary-qwen-2.5b/canary-qwen-2.5b-${PRESET}.gguf \ --manifest samples/wer/test-clean.manifest.jsonl \ --out reports/wer/canary-qwen-2.5b-${PRESET}.librispeech-test-clean.jsonl uv run scripts/wer/score.py reports/wer/canary-qwen-2.5b-${PRESET}.librispeech-test-clean.jsonl ``` ### Score WER against the NeMo SALM reference ```bash uv run --project scripts/envs/canary_qwen \ scripts/wer/run_reference_canary_qwen_nemo.py \ --model nvidia/canary-qwen-2.5b \ --manifest samples/wer/test-clean.manifest.jsonl \ --out reports/wer/canary-qwen-2.5b-REF.librispeech-test-clean.jsonl uv run scripts/wer/score.py reports/wer/canary-qwen-2.5b-REF.librispeech-test-clean.jsonl ```