moondream/parakeet-redux is a Parakeet TDT 0.6B v3 derivative whose encoder
linear layers are ternary: every weight is -1, 0 or +1 times a per-group scale.
Its sibling moondream/parakeet-ultra has the same shapes but ordinary F16
weights, so it needs no special support and loads like any other v3 GGUF.
In the encoder the ternary layers are the two FFN pairs (linear1, linear2
in each of feed_forward1 and feed_forward2), the attention projections
(linear_q/k/v/out/pos) and the two pointwise convolutions, which is 11
layers per block and 264 in the 24 block encoder. Norms, biases, the
depthwise conv, the subsampling stack, the prediction net and the joint stay
in ordinary float tensors.
Produce it with scripts/convert_hf_parakeet_to_gguf.py --ternary keep (see
docs/conversion.md). For each ternary linear <base> the file holds:
| Tensor | Type | Content |
|---|---|---|
<base>.qweight |
I8 | the upstream bytes, N rows of ceil(K/5) bytes (5 base-3 digits per byte) |
<base>.scales |
F16 | one scale per row and 128 input columns, shape N x K/128 |
and no <base>.weight. Two KVs mark the file: parakeet.ternary.present
(bool) and parakeet.ternary.group_size (u32, 128). The loader reads them into
ParakeetConfig::ternary. A packed Redux GGUF is 213.3 MB on disk, against
1441.0 MB for the v3 F16 file.
- At load time
ternary_preparerepacks everyqweightonce into a kernel layout with 2 bits per weight (the same size as the upstream bytes). Rows are grouped in blocks of 16, padded with zeros at the end. For each block and each run of 16 columns there are 64 bytes: byte 4*i + j holds the codes of row i at columns 4p + j of the run, for p = 0..3, in its four bit pairs. The kernels keep one output row per vector lane, so a group sum for 16 rows comes out in one vector with no horizontal reduction. The exact layout is inTernaryWeightinsrc/ternary.hpp. Nothing is dequantized per call. The repack decodes the upstream bytes with a 256 entry table (five trits per byte) and weights are repacked in parallel on up to min(cores, 8) threads.PARAKEET_REPACK_THREADS=Noverrides the count; 1 forces the serial path. The output is byte-identical to the earlier scalar code, whichtests/test_ternary.cppkeeps asrepack_reference. Measured on packed Redux (parakeet-cli bench,load_ms,taskset -c 0-7, 5 runs, load average 2.3 to 3.0): 866 ms min / 868 ms median before, 74 ms min / 76 ms median after; withPARAKEET_REPACK_THREADS=1, 204 ms min / 208 ms median. In-process, 264 weights of 4096x1024 and 1024x4096 take 1414 ms (old code) against 210 ms (table, one thread) and 29 ms (8 threads);bench_ternary repackreproduces this. - At run time the activations of each ternary linear are quantized to int8 per token (one float scale and one int32 sum per 128-column group). The scalar version defines the bytes; the AVX-512 and AVX2 versions write the same bytes.
- The layer is a pair of ggml custom ops: one quantizes the activations, one
runs the integer matmul and applies the scales. The dot product uses
code * int8sums with the constant offset removed through the group sums. - The kernel is picked once per process: AVX-512 VNNI, then NEON dot product,
then AVX2, then a scalar reference. All kernels are bit-identical to the
scalar reference.
PARAKEET_TERNARY_KERNEL=scalar|avx2|vnni|neonforces one (an unavailable name falls back to automatic selection with a log line). The activation quantizer is picked by CPU features only.
Reference path. Upstream's reference path for Redux is weight-only: the ternary
weights are expanded and the activations stay in floating point. The int8
activation quantization in steps 2 and 3 is our own scheme, chosen so the
integer kernels can run, and it is not part of the upstream model. The
reference for this engine is therefore --ternary dequant (ordinary F16 or
Q8_0 weights expanded from the ternary ones). The packed path is checked
against it: same transcripts on the test fixtures and 64 differing words in
22,922 on the long talks (see the long-form section), but it is not
bit-identical to it and is not a reproduction of any upstream runtime. We did
not run or inspect Moondream's own runtime (Photon).
- A packed tensor anywhere in the file (any name ending in
.qweight) whileparakeet.ternary.presentis false is refused at load, and so is the flag set with no packed tensors. Every tensor name is scanned, not only layer 0. - CPU only. Loading a packed GGUF with a GPU backend active fails with a
message; re-convert with
--ternary dequantto use a GPU. - The kernel is chosen at run time. x86-64 with AVX2 or AVX-512 VNNI and aarch64 with the
dot-product extension (dotprod) get SIMD kernels. MSVC builds, Windows on ARM and aarch64 without
dotprod select the scalar kernel (about 1 GMAC/s); the load logs a warning when that happens.
Re-convert with
--ternary dequanton such machines. - The loader keeps the original packed tensors resident next to the repacked planes, so a packed model uses more weight memory than its file size (roughly twice for Redux, not measured).
- No cache-aware streaming.
StreamingEncoderrejects packed GGUFs; use a--ternary dequantfile for streaming. parakeet-cli quantizenever touches ternary tensors. Running it on a packed file copies.qweight(I8) and.scales(F16) verbatim.
The numbers come from this repository's standard build. Its CMake configure
applies the in-tree ggml patches from third_party/ggml-patches
(scripts/apply_ggml_patches.sh); the ggml submodule pin itself is unchanged.
Results on an unpatched ggml were not measured.
Machine: AMD Ryzen 9 9950X3D (16 cores, Zen 5, AVX-512 VNNI), CPU backend,
2026-09-29. The end-to-end numbers were taken at commit f228376, the microbench
at 8b66c63 (which only changes the compile target of the activation
quantizer). Ultra has the same architecture and shapes as v3, so ultra-q8_0
(converted from the HF weights with --dtype q8_0) stands in for a Q8_0 v3.
The dequantized Redux was also converted to Q8_0 the same way. Every timed
command was serialized with flock /tmp/pk-bench.lock.
taskset -c 0-7 build/examples/cli/parakeet-cli bench --model <gguf> \
--manifest benchmarks/librispeech_manifest.tsv --decoder tdt --threads 8 --json <out.json>
100 utterances, 901.1 s of audio, one utterance at a time. RTF is total audio
over the summed per-utterance proc_ms of the JSON, median of 3 full passes.
WER is against the manifest text after lowercasing and replacing every
character other than a-z and the apostrophe with a space; it is from a quick
script, not validate_vs_nemo.py, and is identical across passes. The kernel
rows set PARAKEET_TERNARY_KERNEL; "automatic" leaves it unset, which picks
vnni here.
| Model | Form | RTF pass 1 / 2 / 3 | Median RTF | WER |
|---|---|---|---|---|
| parakeet-redux | packed ternary, automatic (vnni) | 74.8 / 75.6 / 78.0 | 75.6 | 1.96% |
| parakeet-redux | packed ternary, avx2 | 59.6 / 59.0 / 58.8 | 59.0 | 1.96% |
| parakeet-redux | dequantized F16 | 46.6 / 46.1 / 45.6 | 46.1 | 1.92% |
| parakeet-redux | dequantized Q8_0 | 42.7 / 42.3 / 42.7 | 42.7 | 1.96% |
| parakeet-ultra | Q8_0 | 42.5 / 43.1 / 44.2 | 43.1 | 1.71% |
| parakeet-redux | packed ternary, previous kernel (b4164da) | 40.3 / 40.2 / 40.1 | 40.2 | 1.96% |
The last row is the kernel before the current one (one row per call, 256-bit vectors, scalar activation quantization), built from commit b4164da and run in the same session. The transcripts of the old and the new kernel are identical for all 100 utterances, as expected from bit-identical kernels.
transcribe --decoder tdt on benchmarks/audio/diverse/i_have_a_dream.wav
(180 s), whole process wall time including model load, median of 5 runs:
taskset -c 0-7 build/examples/cli/parakeet-cli transcribe --model <gguf> \
--input benchmarks/audio/diverse/i_have_a_dream.wav --decoder tdt --threads 8
RTF is 180 s divided by the median.
| Model | Form | Size on disk | 8 threads (cores 0-7) | RTF | 16 threads (cores 0-15) |
|---|---|---|---|---|---|
| parakeet-redux | packed ternary, vnni kernel | 213.3 MB | 7.93 s | 22.7 | 7.45 s |
| parakeet-redux | packed ternary, avx2 kernel | 213.3 MB | 8.52 s | 21.1 | 8.01 s |
| parakeet-redux | dequantized F16 | 1441.9 MB | 8.73 s | 20.6 | 7.75 s |
| parakeet-redux | dequantized Q8_0 | 941.5 MB | 9.04 s | 19.9 | 8.11 s |
| parakeet-ultra | Q8_0 | 941.5 MB | 9.04 s | 19.9 | 8.29 s |
| parakeet-ultra | F16 | 1441.9 MB | 8.63 s | 20.9 | 7.83 s |
| parakeet-tdt-0.6b-v3 | F16 | 1441.0 MB | 32.79 s | 5.5 | 29.29 s |
The v3 F16 row read its model from a network share, and its wall time includes that model load, so it is not comparable with the other rows.
The scalar kernel is a reference only (116.39 s for this clip in one run at commit c8499e6, with an earlier and faster form of the reference).
taskset -c 2 build/tests/bench_ternary 4096 1024 200 10
taskset -c 2 build/tests/bench_ternary 1024 4096 200 10
taskset -c 2 build/tests/bench_ternary 1024 4096 1000 10
The ggml rows are ggml_mul_mat with an F32 activation matrix on one thread,
so they include ggml's own activation quantization; the ternary rows time only
the matmul kernel. bench_ternary times ternary_quant_rows on its own line.
| N x K, T | scalar | ternary avx2 | ternary vnni | ggml Q8_0 | ggml F16 | ternary quant |
|---|---|---|---|---|---|---|
| 4096 x 1024, T=200 | 1.04 | 184.12 | 546.86 | 87.51 | 121.21 | 0.014 ms |
| 1024 x 4096, T=200 | 1.05 | 184.38 | 553.21 | 91.28 | 129.36 | 0.060 ms |
| 1024 x 4096, T=1000 | 1.05 | 183.27 | 492.23 | 89.85 | 125.12 | 0.408 ms |
Kernel columns are GMAC/s. For comparison, the previous kernel (b4164da) measured 81.91, 87.01 and 82.57 GMAC/s (vnni) and 78.14, 78.63 and 66.79 (avx2) with the same commands. The quantization of the activations took 1.405 ms for K=4096, T=200 before it was vectorized, about as long as the new matmul. Repeated runs of the same commands moved the vnni numbers by up to about 10 percent (the T=1000 shape measured between 492 and 542 over five runs).
- The vnni kernel does 490 to 550 GMAC/s on one core, 6.0 to 6.7 times the
previous kernel and 3.9 to 6.3 times ggml's Q8_0 and F16 mul_mat (546.86/87.51 = 6.25 at the top, 492.23/125.12 = 3.93 at the bottom). At a
clock of about 5 GHz (not measured) that is about 110 int8 multiply-adds
per cycle, close to two 512-bit
vpdpbusdper cycle. The gain comes from keeping one output row per vector lane (no horizontal reduction per group), 512-bit vectors, 12 independent int32 accumulators, and unpacking the 2-bit weights once per 4 activation rows. - Per utterance on LibriSpeech, packed ternary is now the fastest form: median RTF 75.6 against 46.1 for the same model in F16 and 43.1 for Ultra Q8_0, 1.6 to 1.8 times faster. The three passes of each row are within 5 percent of each other, well inside that margin. WER is unchanged at 1.96 percent.
- On the 180 s clip the gain is small: 7.93 s against 8.73 s (F16) and 9.04 s (Q8_0), 9 to 12 percent less time. The linears are a smaller share of this run, which includes model load and attention over a long sequence.
- The avx2 kernel (184 GMAC/s, 59.0 RTF) is also faster than the ggml F16 and Q8_0 paths. It accumulates a whole group in int16, which is exact here.
- Moondream reports 113x for its own Photon runtime with these ternary weights on 8 Zen 5 cores, against 45x for parakeet.cpp Q8_0. We did not run Photon; our 75.6 is not measured under the same conditions and is not a comparison with that number.
- The NEON kernel uses the same layout and is bit-identical to the scalar
reference under
qemu-aarch64; its speed on real ARM hardware has not been measured. - The v3 F16 row is 3.8 times slower than Ultra and Redux F16 with identical shapes on the long clip. That gap was not investigated.
Transcripts on tests/fixtures/speech.wav are identical across the scalar,
avx2 and vnni kernels and the dequantized F16 and Q8_0 Redux, and equal the
reference transcript in AGENTS.md.
Ultra and Redux carry a small voice-activity head (vad_head.proj, vad_head.ctx, vad_head.out) on the
subsampler output: 1x1 conv 1024 to 128, conv k=5 128 to 128, 1x1 conv 128 to 1, sigmoid, one probability per
80 ms frame. The checkpoint does not document the activations or whether ctx is residual.
Wiring in use: SiLU after proj, SiLU after ctx, no residual, then out and a sigmoid. This is the wiring
Moondream's public behaviour describes. It is described by behaviour only; no upstream code was copied and no
proprietary kernel was inspected.
History. The first version of this engine had to infer the wiring from the shapes and the probe below, and chose ReLU
after both convs. The probe could not tell ReLU from SiLU (the pre-activations are large, so the two are almost the
same function), which is why the change to SiLU moves the numbers by less than 0.01 in the median and not at all in
the AUC (table below). VadVariant (src/vad_head.hpp) keeps the other choices as debug options
(parakeet-cli vad-probe --variant N: 0 is the default; bit 0 ReLU after proj, bit 1 residual, bit 2 ReLU after ctx;
the old default is variant 5).
How the probe worked. Test data: three clips of 12 LibriSpeech utterances each (the first 36 of
benchmarks/librispeech_manifest.tsv, joined with no inserted silence), jfk.wav, and the first 120 s of
i_have_a_dream.wav. Labels came from the model's own TDT word timestamps: a speech frame (80 ms) has its center inside a
word span widened by 0.04 s; a pause frame lies inside an inter-word, leading or trailing gap of at least 0.4 s, minus a
0.16 s margin at each end. Counts: 4289 speech frames, 479 pause frames, 218 ignored, identical for both models. (An
earlier probe with digital silence and white noise clips was discarded: zero padding wrecks the per-feature mel
normalization of the speech part.) Features were dumped once per clip from the F16 Ultra and the dequantized F16 Redux.
Grid, 108 variants: tap (subsampler output, final encoder output) x input treatment (none, per-frame LayerNorm without affine, per-frame L2 normalize times sqrt(1024)) x activation after proj (none, ReLU, SiLU) x activation after ctx (none, ReLU, SiLU) x ctx residual (no, yes). Each is scored by the median p on speech frames, the median p on pause frames and the ROC AUC of p separating the two.
Top rows (speech median, pause median, AUC). All are the subsampler tap with no input treatment:
| proj act, ctx act, residual | Ultra | Redux | mean AUC |
|---|---|---|---|
| ReLU, ReLU, no (the first default, variant 5) | 0.999, 0.437, 0.930 | 1.000, 0.197, 0.948 | 0.939 |
| ReLU, SiLU, no | 0.999, 0.437, 0.930 | 1.000, 0.197, 0.948 | 0.939 |
| SiLU, ReLU, no | 0.999, 0.437, 0.930 | 1.000, 0.197, 0.948 | 0.939 |
| SiLU, SiLU, no (default now) | 0.999, 0.437, 0.930 | 1.000, 0.197, 0.948 | 0.939 |
| ReLU, ReLU, yes | pause median 1.000 (collapses) | 1.000, 0.200, 0.959 |
The SiLU rows tie with ReLU because the pre-activations are huge and saturated. No variant met the strict rule (AUC of at least 0.90, speech median at least 0.8, pause median at most 0.2, on both models); the SiLU default misses it only on the Ultra pause median (0.437), like the ReLU variant did.
Refuted. The final encoder output as the tap scores AUC 0.17 to 0.67 for every treatment and activation, so it is at chance or inverted. LayerNorm and L2 input normalization do not help: the medians collapse to about 0.5 for both classes with equal or lower AUC.
Decision: subsampler tap, no input treatment, SiLU after proj, SiLU after ctx, no residual (the default of VadVariant).
The probe does not pick this wiring; it is chosen because it matches Moondream's public behaviour, and the probe only
confirms that it is consistent with the data (it scores the same as the ReLU variant and the other tap and treatments
stay refuted).
Caveats:
- Pause detection is weak. Pause frames with p below 0.5: 58 percent on Ultra, 93 to 94 percent on Redux (the same for gaps of 0.8 s and 1.5 s or more). Speech frames above 0.5: 92.8 percent on Ultra, 92.9 to 94.6 percent on Redux.
- In our probe Ultra needs no residual (with it the pause median is 1.000) and Redux scores slightly better with one (AUC 0.959 against 0.948). We follow the public description (no residual) for both.
- The wiring comes from Moondream's public behaviour, not from a comparison of outputs with their runtime. We did not run that runtime, so the per-frame probabilities have not been compared with it.
- The default threshold is 0.5 for both models, as in Moondream's behaviour. In our probe the best thresholds differ
(Ultra about 0.5 to 0.7, Redux about 0.3).
--vad-thresholdoverrides it. - Long-form validation is in the sections below. The segmenter falls back to a midpoint or hard cut when it finds no pause, so a weak pause response degrades gracefully.
segment_by_vad (src/vad_segmenter.cpp) turns the per-frame probabilities into segments of at most 30 s. The rules
follow Moondream's public behaviour, written from the description and not from their code.
- Speech is a frame with p >= 0.5 (
--vad-threshold, default 0.5 for both models). - Smoothing: speech gaps shorter than 0.1 s are filled, then speech runs shorter than 0.1 s are removed. With 80 ms frames that fills a one-frame dip and removes a one-frame blip.
- A pause is a silent run of at least 0.2 s (
--vad-min-pause). - Audio of at most 30 s (
--vad-max-seg) is not cut and goes through whole, with or without speech. - Longer audio is cut from the front. For a segment that starts at s the cut is the midpoint of the last pause that lies fully inside [s + 1 s, s + 30 s]. If there is none, it is the midpoint of the last pause whose midpoint lies in that range. Otherwise it is a hard cut at s + 30 s. A cut is never closer than 1 s to the start of the segment.
- A segment without speech is dropped, including a trailing remainder. Long audio with no speech at all gives an empty transcript. Kept segments are ordered and disjoint but need not touch.
- The VAD runs on blocks of 120 s, each with its own mel normalization, and the probabilities are joined on the 80 ms grid. A trailing block shorter than 5 s is folded into the previous block. The head's conv sees zero padding at each block edge, so the one or two frames at an edge are slightly less reliable.
Behaviour changes against the first version of this segmenter (the one that produced the measurements in the sections below up to the re-measurement):
| before | now | |
|---|---|---|
| minimum pause | 0.32 s | 0.2 s |
| earliest cut inside a segment | 8 s (fallback), last third first | 1 s |
| which pause | the longest in the last third of the window, else the longest in the window | the last one fully inside, else the last one with its midpoint inside |
| smoothing | none | bridge gaps under 0.1 s, drop runs under 0.1 s |
| segments without speech | kept and decoded | dropped; no speech at all gives an empty transcript |
| VAD input | the whole clip, one mel normalization | 120 s blocks, one mel normalization each |
| VAD head activations | ReLU, ReLU | SiLU, SiLU |
Audio of 30 s or less is unchanged (same code path, transcripts identical). The C-API function
parakeet_capi_transcribe_path_json_vad and the CLI options keep their names; only their defaults changed.
SegmenterOpts gained bridge_sec and min_speech_sec (additive; no ABI change).
The full reference, with the Silero detector that uses the same entry points, is in vad.md. This section describes the head.
The head also runs without transcribing, like the VAD of whisper.cpp. Three entry points return the same JSON
(src/vad_json.cpp):
parakeet-cli vad --model ultra.gguf --input audio.wav [--mode speech|segments] [--probabilities] \
[--threshold 0.5] [--min-pause 0.2] [--min-speech 0.1] [--max-segment 30] [--threads N]
char* parakeet_capi_vad_pcm_json(parakeet_ctx* ctx, const float* samples, int n_samples,
int sample_rate, const char* options_json);
char* parakeet_capi_vad_path_json(parakeet_ctx* ctx, const char* wav_path, const char* options_json);
options_json is NULL, "" or a flat object with the keys threshold, min_pause, min_speech, max_segment, mode
and probabilities. The result is freed with parakeet_capi_free_string; on error it is NULL and
parakeet_capi_last_error has the message (model has no VAD head for a model without the head, such as v3).
{"mode":"speech","duration":21.370,"frame_sec":0.080,"backend":"cpu",
"segments":[{"start":2.400,"end":6.560},{"start":7.040,"end":9.200}],
"probabilities":[0.0123, ...]} // only with "probabilities":true; one value per 80 ms frame from t = 0
Two kinds of segment are available:
"speech"(the default) is the speech regions: the frames with p >= threshold after the smoothing of rule 2, with regions separated by a silence shorter thanmin_pausemerged. It works for audio of any length, is never capped, and never includes silence. Use it to find where speech is, to gate a recorder, or to feed another engine."segments"is whattranscribe --vaddecodes (segmenter rules 4 to 6): pieces of at mostmax_segmentseconds cut at pauses, with no-speech pieces dropped. Audio of at mostmax_segmentseconds comes back as one segment[0, duration]even when it holds no speech. Use it to reproduce or to plan a transcription.
Times are on the original timeline: other sample rates are resampled to 16 kHz and the times stay in seconds of the input.
Backend rules are the same as for --vad: the head runs on the context's compute backend (the pool of
parakeet_capi_set_concurrency when set), a packed Redux file is CPU only, and the JSON reports the device in backend.
The call is safe from several threads, on one context or several. It is additive to the C-API: parakeet_capi_abi_version
is unchanged. The head is a pause detector with the limits listed above (AUC 0.93 and 0.95): expect soft edges of a
frame or two (80 to 160 ms), and tune threshold for your audio.
--vad cuts long audio at pauses found by the VAD head and transcribes each segment on its own (see "Segmenter rules"
above). Without it, the model sees the whole clip in one pass.
The tables in this section and in the TED-LIUM section were measured with the first version of the segmenter and the ReLU head (threshold 0.5, min pause 0.32 s, max segment 30 s, min segment 8 s). The re-measurement with the rules above is in "Re-measurement with the Moondream-style segmenter" at the end of the long-form validation.
How it was measured. scripts/make_longform.py joins the first 90 utterances of benchmarks/librispeech_manifest.tsv
into three clips of 30 utterances each (218 to 354 s each), with a known reference
(the joined manifest texts). Utterances are separated by Gaussian noise at about -55 dBFS (fixed seed) rather than
digital zeros, because digital silence distorts the per-feature mel normalization. Three sets: a 0.45 s gap, a 0.16 s gap
(below the 0.32 s minimum pause on purpose), and no inserted gap (only the natural utterance edges). These are synthetic
long-form clips built from LibriSpeech read speech, not TED-LIUM or other real long recordings.
scripts/eval_vad_longform.py runs parakeet-cli transcribe --decoder tdt --threads 8 under taskset -c 0-7 with and
without --vad and scores with scripts/asr_metrics.py wer (case and punctuation normalized). CPU pinning and the shared bench lock are applied by the caller through the script's --prefix option, not by the
script. The command used:
python3 scripts/eval_vad_longform.py --model <gguf> --dir <longform dir> --glob 'longform_0p45_*.wav' \
--prefix 'flock /tmp/pk-bench.lock taskset -c 0-7'
(--threads 8 is the script default; sweep runs add --skip-plain --vad-arg=--vad-threshold=0.3 and similar.) The models are the
Ultra F16 GGUF and the packed ternary Redux GGUF (--ternary keep, native kernel).
WER per clip, plain single pass vs --vad (percent, three clips per set, then the mean):
| Model | Gap | Clip 0 | Clip 1 | Clip 2 | Mean |
|---|---|---|---|---|---|
| Ultra F16, plain | 0.45 s | 0.47 | 2.39 | 2.29 | 1.71 |
Ultra F16, --vad |
0.45 s | 0.47 | 2.21 | 2.39 | 1.69 |
| Ultra F16, plain | 0.16 s | 0.31 | 2.58 | 2.18 | 1.69 |
Ultra F16, --vad |
0.16 s | 0.62 | 2.21 | 2.50 | 1.78 |
| Ultra F16, plain | none | 0.31 | 2.39 | 2.18 | 1.63 |
Ultra F16, --vad |
none | 0.62 | 2.39 | 2.50 | 1.84 |
| Redux packed, plain | 0.45 s | 0.62 | 2.58 | 2.72 | 1.97 |
Redux packed, --vad |
0.45 s | 0.47 | 2.58 | 2.72 | 1.92 |
| Redux packed, plain | 0.16 s | 0.62 | 2.39 | 2.83 | 1.95 |
Redux packed, --vad |
0.16 s | 0.31 | 2.03 | 2.72 | 1.69 |
| Redux packed, plain | none | 0.62 | 2.03 | 2.83 | 1.83 |
Redux packed, --vad |
none | 0.62 | 2.21 | 2.29 | 1.71 |
Reading. The change in mean WER from plain to --vad runs in both directions and stays within about 0.2 points:
Ultra -0.02, +0.09 and +0.21 (0.45 s, 0.16 s, no gap); Redux -0.05, -0.26 and -0.12. The single largest loss (+0.21, Ultra,
no gap) is marginally over the 0.2-point investigation limit. The Redux gains and the Ultra losses are the same size, so
neither direction is a real effect on three clips per set: VAD segmentation does not change WER meaningfully on these
clips. One clip is 645 to 919 words, so 0.1 point is about one word. Inspecting the worst Ultra clip (no gap, clip 0, 10 segments) shows every cut lands between words, none
inside one, and the differing words are spelling variants (tail/tale, honour/honor) and rare names that flip
between the two runs. The segment boundaries there were 20.32, 48.48, 70.00, 98.48, 127.12, 156.56, 179.92, 204.16 and
229.28 s. The result is a wash, not a win: on these clips VAD does not measurably help or hurt accuracy. Its benefit is
bounded memory and time on audio of any length (the 30 s cap), not a lower WER.
Parameter sweep on Ultra F16 with the first segmenter version (mean --vad WER, percent, one variable at a time from its defaults):
| Setting | 0.45 s gap | 0.16 s gap | no gap | mean of the three |
|---|---|---|---|---|
| threshold 0.3 | 1.69 | 1.67 | 1.89 | 1.75 |
| threshold 0.5 (default) | 1.69 | 1.78 | 1.84 | 1.77 |
| threshold 0.7 | 1.69 | 1.83 | 1.89 | 1.80 |
| min pause 0.16 s | 1.69 | 1.78 | 1.84 | 1.77 |
| min pause 0.32 s (default) | 1.69 | 1.78 | 1.84 | 1.77 |
| min pause 0.64 s | 1.95 | 1.79 | 1.85 | 1.86 |
Threshold 0.3 is the only setting ahead of the default. On Redux packed it gives 1.97, 1.65 and 1.63 (mean 1.75) against 1.92, 1.69 and 1.71 (mean 1.77) for the default. Both gains are 0.02 points, under half a word per clip set, and Redux gets worse on the 0.45 s gap set, so the defaults stay. A min pause of 0.64 s is worse on the 0.45 s gap set for Ultra (1.95); probably a longer required pause makes the segmenter fall back to hard cuts more often, which was not measured. A min pause of 0.16 s changes nothing here.
Known limits:
- A 30 s window with no usable pause makes a hard cut, which can land inside a word.
- Offline only: no streaming with
--vad. - The VAD head detects pauses weakly (see the caveats above), so many cuts are hard cuts or land at the model's best guess.
- Ternary kernels are CPU only.
- The figures in this section are from synthetic LibriSpeech clips (218 to 354 s); real talks are in the multilingual and long-form section below.
Everything above is English (LibriSpeech and synthetic long clips built from it). Ultra is a 25-language model, and the
upstream cards report FLEURS and TED-LIUM long-form results, so this section measures our engine on both. Nothing was
tuned, no src/ code changed, and every number below comes from the commands listed here. The scoring, the subsets and
the caveats matter for how to read the tables, so read those first.
- Scoring uses
normalizefromscripts/asr_metrics.pyon both reference and hypothesis (NFKC, lowercase, punctuation replaced by a space, whitespace collapsed). It is the same for every model. It is NOT the Open ASR Leaderboard normalizer (no number spelling, no text-normalizer for other languages), so the absolute numbers here are not comparable with the upstream model cards. What matters is the direction and size of the differences between our models on the same subset. - Upstream's own figures, from their pipeline on the full test splits, quoted only as theirs: FLEURS average 11.62 for v3, 9.55 for Ultra and 10.56 for Redux; TED-LIUM long-form 2.71 for v3, 1.94 for Ultra and 2.51 for Redux.
- FLEURS: the first 50 utterances of the
testsplit of each language in dataset (streaming parquet) order, no shuffling and no filtering. The reference is the dataset'sraw_transcription(natural case and punctuation). 25 languages, 1250 utterances, 26784 reference words, 14580.6 s of audio. Audio is resampled to 16 kHz mono int16. Because each language has about 1000 words, one word is about 0.1 point, so differences under roughly 1 point on a single language are noise. WER per language is a corpus WER (total edits over total reference words); the mean is the unweighted mean over the 25 languages. - The config ids all exist as listed: bg_bg, hr_hr, cs_cz, da_dk, nl_nl, en_us, et_ee, fi_fi, fr_fr, de_de, el_gr, hu_hu, it_it, lv_lv, lt_lt, mt_mt, pl_pl, pt_br, ro_ro, ru_ru, sk_sk, sl_si, es_419, sv_se, uk_ua.
- TED-LIUM:
distil-whisper/tedlium-long-form,testsplit, 11 full talks (8905 s in total, 2.5 hours). The reference is thetextcolumn with tags such as<unk>removed. One of the 11 is a 5.5 s clip with 24 words (DanBarber_2010_S103), so it moves a per-talk mean by a lot; means are given with and without it. - Models: v3 F16 (
tdt-0.6b-v3-f16.gguf), Ultra F16, Redux packed ternary (--ternary keep, native kernel) and Redux dequantized F16 (--ternary dequant, isolates the int8 activation and packed kernel path). All with--decoder tdt.
| FLEURS config | v3 F16 | Ultra F16 | Redux packed | Redux dequantized F16 |
|---|---|---|---|---|
| bg_bg | 11.24 | 9.23 | 10.37 | 10.98 |
| cs_cz | 11.93 | 9.61 | 10.62 | 9.71 |
| da_dk | 17.96 | 16.18 | 16.44 | 16.71 |
| de_de | 4.75 | 3.67 | 4.92 | 4.83 |
| el_gr | 36.14 | 33.01 | 32.13 | 32.29 |
| en_us | 5.30 | 4.65 | 6.23 | 6.04 |
| es_419 | 3.10 | 2.38 | 3.42 | 3.65 |
| et_ee | 17.83 | 15.21 | 12.47 | 13.34 |
| fi_fi | 13.06 | 9.77 | 11.84 | 12.21 |
| fr_fr | 4.23 | 4.45 | 10.31 | 10.09 |
| hr_hr | 11.80 | 8.90 | 9.67 | 9.19 |
| hu_hu | 16.18 | 12.69 | 18.48 | 18.08 |
| it_it | 1.88 | 2.11 | 3.54 | 3.69 |
| lt_lt | 22.03 | 18.57 | 20.30 | 19.98 |
| lv_lv | 23.56 | 19.08 | 14.39 | 14.61 |
| mt_mt | 21.20 | 16.59 | 15.46 | 15.29 |
| nl_nl | 7.94 | 7.15 | 10.86 | 10.41 |
| pl_pl | 8.71 | 7.69 | 11.54 | 11.43 |
| pt_br | 4.55 | 3.99 | 5.58 | 5.34 |
| ro_ro | 12.73 | 11.31 | 12.56 | 12.90 |
| ru_ru | 5.68 | 5.11 | 8.14 | 7.85 |
| sk_sk | 9.22 | 7.01 | 9.42 | 9.22 |
| sl_si | 24.12 | 19.00 | 20.81 | 20.17 |
| sv_se | 17.39 | 14.87 | 14.57 | 14.07 |
| uk_ua | 6.81 | 5.43 | 8.09 | 7.55 |
| Mean over 25 languages | 12.77 | 10.71 | 12.09 | 11.99 |
Reading:
- Ultra beats v3 on 23 of 25 languages, mean 10.71 against 12.77 (2.07 points lower on average). It loses slightly on fr_fr (4.45 against 4.23) and it_it (2.11 against 1.88), which are within noise at this sample size. The size of the gain is close to upstream's (11.62 to 9.55), which is a useful sign that the conversion did not lose anything, but the two pipelines differ so this is a direction check, not a reproduction.
- Redux is better than v3 on 13 languages (mostly Baltic, Slavic, Uralic and Greek: bg, cs, da, el, et, fi, hr, lt, lv, mt, ro, sl, sv) and worse on the rest; the mean is 12.09 against 12.77. It loses to Ultra on 20 of 25. The largest gaps to Ultra are fr_fr (10.31 against 4.45, +5.9), hu_hu (+5.8), pl_pl (+3.9), nl_nl (+3.7), ru_ru (+3.0), uk_ua (+2.7) and sk_sk (+2.4). French and Polish match what the upstream card says Redux gives up. Redux is ahead of Ultra on el_gr, et_ee, lv_lv, mt_mt and sv_se, with lv_lv (14.39 against 19.08) and et_ee (12.47 against 15.21) the biggest. I did not check why; it may be a difference in the training mix.
- Int8 activations. Packed Redux (12.09) against dequantized Redux F16 (11.99) is +0.10 points on the mean, with per-language differences from -0.87 to +0.91 and no direction (packed is lower on 9 languages, higher on 16). With about 1000 words per language that is noise. On this data the packed ternary path costs no measurable WER against the dequantized weights, and the speed benefit is in the speed section above.
- el_gr is high for every model (32 to 36). The FLEURS Greek references and the model outputs likely disagree on normalization details (accents, final sigma, number forms); I did not investigate, and it does not change the comparison between models on the same references.
Plain is one single pass over the whole talk; VAD is --vad with the options of the first segmenter version
(threshold 0.5, min pause 0.32 s, max segment 30 s, min segment 8 s; ReLU head). The --vad option needs the VAD head, which v3
does not have (model has no VAD head), so v3 has only the plain column. The segment count was not recorded.
| Talk (length) | v3 plain | Ultra plain | Ultra VAD | Redux packed plain | Redux packed VAD | Redux deq plain | Redux deq VAD |
|---|---|---|---|---|---|---|---|
| AimeeMullins (1249 s) | 6.65 | 3.44 | 3.41 | 4.18 | 4.34 | 4.08 | 4.21 |
| BillGates (1506 s) | 7.07 | 5.79 | 5.74 | 6.18 | 6.34 | 6.32 | 6.32 |
| DanBarber (834 s) | 5.75 | 5.46 | 4.90 | 6.47 | 6.79 | 6.59 | 6.83 |
| DanBarber_2010_S103 (5.5 s) | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 8.33 | 8.33 |
| DanielKahneman (1096 s) | 3.29 | 3.23 | 3.20 | 4.11 | 3.98 | 4.11 | 4.14 |
| EricMead (459 s) | 5.61 | 4.91 | 4.84 | 5.35 | 5.16 | 5.23 | 5.16 |
| GaryFlake (345 s) | 2.90 | 3.16 | 3.08 | 3.52 | 3.60 | 3.60 | 3.69 |
| JamesCameron (982 s) | 6.34 | 5.52 | 5.42 | 5.81 | 5.62 | 5.62 | 5.55 |
| JaneMcGonigal (1168 s) | 4.21 | 3.60 | 3.68 | 4.06 | 4.09 | 4.06 | 4.16 |
| MichaelSpecter (921 s) | 3.57 | 3.25 | 3.16 | 3.63 | 3.79 | 3.63 | 3.86 |
| RobertGupta (340 s) | 3.04 | 2.71 | 2.59 | 4.17 | 4.51 | 4.17 | 4.62 |
| Mean, all 11 | 4.40 | 3.73 | 3.64 | 4.32 | 4.38 | 5.07 | 5.17 |
| Mean, without the 5.5 s clip | 4.84 | 4.11 | 4.00 | 4.75 | 4.82 | 4.74 | 4.85 |
Reading:
- Ultra beats v3 on 9 of the 11 talks in a single pass (one tie on the 5.5 s clip) and on the mean (3.73 against 4.40; 4.11 against 4.84 without the short clip). It loses only on GaryFlake (3.16 against 2.90). Direction agrees with upstream (2.71 against 1.94), but our absolute values are about twice upstream's. Part of that gap is the normalizer: the references spell numbers out while the model writes digits, and in the Ultra plain hypotheses 218 of about 26800 tokens contain digits (none in the references), which alone accounts for at least 0.8 points. I did not run a number-aware normalizer, so the rest of the gap is not explained here.
- Redux packed with
--vad: 4.38 on all 11 (4.82 without the short clip), against Ultra VAD 3.64 (4.00) and v3 plain 4.40 (4.84). So Redux is about where v3 is on real talks and about 0.8 points behind Ultra, the same order as upstream (2.51 against 1.94 against 2.71). - Packed against dequantized Redux, both with
--vad: 4.38 against 5.17 on all 11, but 4.82 against 4.85 without the 5.5 s clip. The all-11 gap is the 24-word clip (0 against 2 errors); on the ten real talks packed and dequantized are the same. - VAD against plain. Ultra: VAD is better on 9 of 11 talks (one tie), mean 3.64 against 3.73 (4.00 against 4.11), gains of 0.03
to 0.56 points (the 0.56 is DanBarber), and worse on JaneMcGonigal by 0.08. Redux dequantized: VAD is slightly worse, 5.17 against 5.07
(4.85 against 4.74). Redux packed: plain 4.32 against VAD 4.38 (4.75 against 4.82 without the short clip), so plain is slightly better, by
0.07 points. The honest reading is that on real talks VAD segmentation is about neutral: a small gain for Ultra, a small loss for Redux,
all under about 0.15 points on the mean and inside per-talk noise. Peak RSS (from
/usr/bin/time -v) with--vadis 7 to 11 GB on the talks over 800 s, about the same as the single pass, and drops to about 4 GB only on the three shortest talks (340 to 459 s). So with the first segmenter VAD did not buy a memory win (it does now, see the re-measurement below). - Packed against dequantized Redux, plain single pass, after the long-audio fix (the seven talks that used to crash; packed run
at
--threads 8with/usr/bin/time -v): WER 4.92 packed against 4.91 dequantized on the mean of the seven (per talk within 0.2 points, packed lower on 2, higher on 2, equal on 3). Scored with the dequantized transcript as the reference, packed differs by 64 of 22922 words (0.28 percent; per talk 0.05 to 0.50 percent). So on real 14 to 25 minute talks the packed path matches the dequantized one to a few words per talk, and there is no measurable cost from int8 activations or the packed kernel. The packed plain runs all exited 0. Longest talk (BillGates, 1506 s): 82.8 s wall, 11.67 GB peak RSS, load average 20 at the start; the other six took 64 to 85 s except JamesCameron (318 s) and MichaelSpecter (294 s), which started at load 26 to 48; peak RSS 6.6 to 11.7 GB, similar to the F16 models. Load averages at start were 3.5 to 48, so wall times are indicative only. - Single-pass memory and time. Every single pass over a 5 to 25 minute talk finished on the F16 models (Ultra, dequantized Redux, v3), peak RSS 7 to 13 GB, so the O(T^2) attention above the 8192-frame local-attention threshold did not fail for any of these talks. Wall times were recorded but are not reported: the shared machine was heavily loaded during the runs (load average 40 to 90 from other jobs), so times vary by more than 10x between identical runs.
With redux-keep.gguf (packed ternary), plain single pass segfaulted after about 3 s on every talk longer than about
655 s (8192 encoder frames at 80 ms): AimeeMullins, BillGates, DanBarber, DanielKahneman, JamesCameron, JaneMcGonigal and
MichaelSpecter, exit code 139. The backtrace ended in RelPosAttention::build_graph_local_chunked.
Cause: above kLocalThreshold = 8192 frames the encoder switches to the local attention paths. Three of the five
attention linear lambdas (build_graph_batched_local, build_graph_local, build_graph_local_chunked) read
only <base>.weight, which a packed GGUF does not have, so ggml_mul_mat got a null tensor. The earlier synthetic clips
(218 to 354 s) never reach these paths. All five now go through one helper (attn_linear in
src/relpos_attention.cpp), which uses the packed kernel when <base>.qweight exists and throws
missing encoder weight <name> when neither form exists.
Verified after the fix on a 714 s clip (speech.wav repeated, 2112 words): packed and dequantized Redux both finish and
print byte-identical transcripts. tests/test_ternary_long.cpp forces the same paths on short audio
(PARAKEET_ATT_CONTEXT=64), for a single item and for a batch. The seven talks that crashed were then re-run
with the fixed build; their packed plain numbers are in the TED-LIUM table above.
The same 11 talks and scoring as above, run again after the segmenter and head changes ("Segmenter rules"). "Before" is
the PR head (cb6208d, first segmenter, ReLU head) and "now" is this branch, both built from source with the same
flags and run with parakeet-cli transcribe --decoder tdt --threads 8 --vad, one after the other for each talk. Models:
Ultra converted to Q8_0 (--dtype q8_0) and Redux packed (--ternary keep). Audio is streamed from the Hub and deleted
after the runs.
Load. The machine was shared and busy: the load average was 23 to 69 at the start of the runs. These runs were not measured on a quiet machine, so wall times are not reported; WER and peak memory do not depend on load.
WER, percent, --vad:
| Talk | Ultra before | Ultra now | Redux before | Redux now |
|---|---|---|---|---|
| AimeeMullins | 3.41 | 3.74 | 4.34 | 4.38 |
| BillGates | 5.83 | 5.70 | 6.34 | 6.18 |
| DanBarber | 4.90 | 5.18 | 6.79 | 6.27 |
| DanBarber_2010_S103 | 0.00 | 0.00 | 0.00 | 0.00 |
| DanielKahneman | 3.13 | 2.94 | 3.98 | 4.04 |
| EricMead | 4.84 | 4.84 | 5.16 | 5.35 |
| GaryFlake | 3.16 | 3.16 | 3.60 | 3.60 |
| JamesCameron | 5.32 | 5.19 | 5.62 | 5.58 |
| JaneMcGonigal | 3.68 | 3.70 | 4.09 | 4.14 |
| MichaelSpecter | 3.16 | 2.94 | 3.79 | 3.35 |
| RobertGupta | 2.59 | 2.37 | 4.51 | 5.07 |
| Mean, all 11 | 3.64 | 3.61 | 4.38 | 4.36 |
| Mean, without the 5.5 s clip | 4.00 | 3.98 | 4.82 | 4.80 |
Plain single pass for reference. The plain path did not change (transcripts are byte-identical between the two builds on the fixtures and on 8 clips of up to 180 s for three models), so plain numbers do not depend on the build. The earlier table has Ultra F16 plain at 3.73 (all 11) and 4.11 (without the short clip), and Redux packed plain at 4.32 and 4.75. The Redux packed plain runs repeated here on 9 of the 11 talks and gave the same WER as the earlier table on every one. Ultra Q8_0 plain was run on two talks only (AimeeMullins 3.54, RobertGupta 2.71; Ultra F16 earlier: 3.44, 2.71); the rest was stopped because the machine was overloaded.
Peak resident memory with --vad (GB, before / now, from /usr/bin/time -v):
| Talk | Ultra Q8_0 | Redux packed |
|---|---|---|
| AimeeMullins | 9.53 / 2.01 | 9.05 / 1.47 |
| BillGates | 11.30 / 2.05 | 10.83 / 1.51 |
| DanBarber | 6.67 / 1.93 | 6.17 / 1.39 |
| DanBarber_2010_S103 | 0.99 / 0.99 | 0.44 / 0.44 |
| DanielKahneman | 8.48 / 1.97 | 7.98 / 1.44 |
| EricMead | 4.10 / 1.86 | 3.58 / 1.32 |
| GaryFlake | 3.33 / 1.83 | 2.80 / 1.30 |
| JamesCameron | 7.69 / 1.95 | 7.20 / 1.42 |
| JaneMcGonigal | 8.97 / 1.99 | 8.48 / 1.45 |
| MichaelSpecter | 7.27 / 1.94 | 6.77 / 1.40 |
| RobertGupta | 3.29 / 1.83 | 2.76 / 1.29 |
Reading.
- WER. The mean moves by 0.02 to 0.03 points (Ultra 3.64 to 3.61, Redux 4.38 to 4.36 on all 11; 4.00 to 3.98 and 4.82 to
4.80 without the short clip). Per talk the change is between -0.52 and +0.56 points, in both directions (Redux RobertGupta +0.56, DanBarber
-0.52, MichaelSpecter -0.44; Ultra AimeeMullins +0.33, DanBarber +0.28), the same size as the noise between runs that
differ only in where the cuts fall. Against plain,
--vadnow is better than plain for Ultra by 0.12 points (3.61 against 3.73 from the earlier F16 table) and worse for Redux by 0.04 (4.36 against 4.32). The conclusion of the earlier sections stands: segmentation does not change accuracy in any direction we can resolve with 11 talks. - Memory. This changes a lot. With
--vadthe peak RSS is now 1.3 to 2.1 GB on every talk of 340 s or more, against 2.8 to 11.3 GB before. The old VAD ran the subsampler over the whole clip in one pass; the 120 s blocks bound that, and segments are at most 30 s. The earlier statement that--vadbuys no memory win applied to the first version only. The 5.5 s clip is unchanged (it takes the plain path). - The cuts differ from the first version (earlier threshold of 0.32 s and 8 s minimum, now 0.2 s and 1 s), so more and shorter segments are expected; the segment count was not recorded.
Pause detection with vad-probe, on the same 5 clips as the wiring probe (three 12-utterance LibriSpeech clips, jfk.wav,
first 120 s of i_have_a_dream.wav), same labels, F16-equivalent models Ultra Q8_0 and Redux packed. Frames with
speech labels: 4289 (Ultra) and 4065 (Redux); pause labels: 477 and 564 (labels come from each model's own TDT
timestamps, so the counts differ a little between models).
| Model | Head | Speech frames p >= 0.5 | Pause frames p < 0.5 | Median p, speech | Median p, pause | AUC |
|---|---|---|---|---|---|---|
| Ultra Q8_0 | ReLU (before) | 92.8% | 58.1% | 0.999 | 0.436 | 0.931 |
| Ultra Q8_0 | SiLU (now) | 92.8% | 58.1% | 0.999 | 0.434 | 0.931 |
| Redux packed | ReLU (before) | 94.6% | 92.9% | 1.000 | 0.187 | 0.959 |
| Redux packed | SiLU (now) | 94.6% | 92.9% | 1.000 | 0.187 | 0.959 |
The head change does not move pause detection (as expected from the saturated pre-activations), and the old default is
still available as vad-probe --variant 5. Ultra still separates pauses weakly (58% of pause frames below 0.5).
Timing check, not a result. Five interleaved runs of --vad on a 117 s clip (Ultra Q8_0, 4 threads), before / now, wall
seconds with the load average at the start: 19.35 (64.5) / 17.51 (61.2), 18.48 (53.9) / 13.28 (51.0), 16.78 (50.1) / 10.78
(49.5), 13.61 (48.0) / 11.04 (52.1), 11.03 (51.8) / 11.02 (47.0). This was not measured on a quiet machine (load above 45
throughout), so only the direction is usable: the new build is not slower.
An independent check that the Ultra conversion and engine reproduce the HF implementation. ParakeetForTDT is not in the
installed transformers 5.3.0, so this used a source checkout that has it (version string 5.10.0.dev0) through
PYTHONPATH. The HF checkpoint has no preprocessor or tokenizer files, so scripts/hf_reference_transcribe.py builds a
ParakeetFeatureExtractor(feature_size=128, sampling_rate=16000) (per-feature normalization is built in) and decodes
token ids with the piece table from our Ultra GGUF. The run is fp32 on CPU with greedy TDT decoding; ours is the F16 GGUF.
Because the token table comes from our GGUF, this checks the encoder, decoder and search, not the tokenizer.
Same first 20 test utterances of en_us, de_de and fr_fr (60 utterances, 1509 words). Ours scored against the HF
transcript as reference, after normalize, and after dropping the <unk> piece, which the HF side prints and we omit:
| Language | Identical after normalize | WER of ours vs HF |
|---|---|---|
| en_us | 20 of 20 | 0.00 |
| de_de | 18 of 20 | 0.45 |
| fr_fr | 18 of 20 | 0.48 |
| total | 56 of 60 | 0.33 |
The four differing utterances are single-word choices near a tie (for example Hirnschadens against Höhenschadens,
Laka against Lakas, vient against viant, and dix-sept against 17). The F16 weights in the GGUF against the fp32 weights in HF are a plausible cause, but I did not test that, for example by converting an
F32 GGUF.
Both implementations scored against the FLEURS references (same scorer as above, <unk> left in the HF text, where the
unk word counts as an error):
| Language | Ours (F16 GGUF) | HF transformers (fp32) |
|---|---|---|
| en_us | 3.64 | 3.64 |
| de_de | 3.56 | 3.78 |
| fr_fr | 5.13 | 6.09 |
| mean of the three | 4.11 | 4.50 |
The fr_fr gap on the HF side comes from <unk> pieces that the HF text prints (four in the 20 utterances) and we drop; it is a
decoding detail, not a model difference.
# subsets (stream from the Hub, no token; only 16 kHz wavs, references and manifests are written, under 1 GB in total)
python3 scripts/fetch_fleurs_subset.py --out /tmp/val/fleurs --n 50
python3 scripts/fetch_tedlium_longform.py --out /tmp/val/ted
# FLEURS WER for the four models (bench --json keeps the hypotheses per language)
python3 scripts/eval_manifest_wer.py --out /tmp/val/fleurs_res --threads 8 \
--model v3=<tdt-0.6b-v3-f16.gguf> --model ultra=<ultra-f16.gguf> \
--model redux=<redux-keep.gguf> --model reduxdeq=<redux-deq.gguf> /tmp/val/fleurs/*/manifest.tsv
# TED-LIUM plain and --vad, one command per model (records WER, wall time, peak RSS; a crash is recorded as FAIL)
python3 scripts/eval_longform_talks.py --model <gguf> --dir /tmp/val/ted --save /tmp/val/ted_res/<name>
# HF transformers reference for Ultra, then compare
PYTHONPATH=<transformers checkout>/src python3 scripts/hf_reference_transcribe.py \
--hf-dir <hf ultra dir> --gguf <ultra-f16.gguf> --out /tmp/val/spike_hf /tmp/val/spike/{en_us,de_de,fr_fr}/manifest.tsv
python3 scripts/compare_hyps.py /tmp/val/spike_res/ultra /tmp/val/spike_hf
The FLEURS run used --threads 2 for most of the sweep instead of 8, because the machine was heavily loaded and 8 spinning
threads were much slower than 2 there; greedy decoding does not depend on the thread count except for float summation
order, which was not checked for any change in a transcript.
Limits of this section:
- 50 utterances per language is a sample, not the full FLEURS test split; per-language numbers carry about a 1 point noise band.
- The normalizer is a plain one, so numbers, hyphenation and non-Latin scripts add errors that the leaderboard normalizer would remove; compare models against each other, not against upstream.
- The TED-LIUM set has 11 talks, one of them 5.5 s long; a per-talk difference under 0.2 points is not meaningful.
- Timing is not reported (loaded machine). The speed numbers are in the earlier speed section.
ctest --test-dir build -R ternary --output-on-failure # test_ternary (kernels vs scalar), no model needed
PARAKEET_TEST_GGUF_REDUX_KEEP=<redux --ternary keep gguf> \
PARAKEET_TEST_GGUF_REDUX_DEQ=<redux --ternary dequant gguf> \
ctest --test-dir build -R test_ternary_model --output-on-failure
build/tests/bench_ternary [N K T reps] # single thread: each kernel, the quantizer, ggml Q8_0 and F16 mul_mat