Repository navigation
tools/parakeet: native Parakeet-TDT ASR (Phonon-2 five-value) behind eliza_inference_asr_*, with streaming - #73
Merged
lalalune merged 12 commits intoOct 14, 2026
Conversation
Adds a CPU-only FastConformer encoder + TDT decoder for
FermionResearch/Phonon-2 (five-value QAT re-quantisation of
nvidia/parakeet-tdt-0.6b-v3, weights CC-BY-4.0) and serves it through the
existing eliza_inference_asr_* surface, including the streaming session.
- convert_phonon2_to_gguf.py: pinned archive (HF revision 7e153e40, sha256)
to GGUF; five-value linears kept exact as 4-bit codes in kernel order,
int6 tables as exact int8 + scales; --verify re-expands every record
bit for bit against the container.
- int8 kernels: SMMLA (i8mm) and SDOT (dotprod) tiers in their own
translation units behind HWCAP/sysctl checks, plus a scalar reference;
all tiers bit-identical (test-parakeet-kernels).
- encoder/decoder in plain C++ with a persistent worker pool; fp16
relative-position key cache; per-token int8 activations.
- streaming: pause-delimited windowed re-decode with local agreement.
- libelizainference: ELIZA_ENABLE_PARAKEET fold; mmap_acquire("asr")
picks a parakeet-tdt GGUF under <bundle>/asr/ (ELIZA_ASR_BACKEND
overrides); real TDT word times; asr_stream_supported() is 1 while the
backend is loaded. No new symbols, ABI stays v17.
- tests: kernel tiers, reference parity (dump_parakeet_reference.py
fixtures), FFI end to end; parakeet-bench + bench_parakeet.py for
RTFx / first partial / RSS / WER.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otes Streaming windows now start in pauses between tokens (the model, the dense reference included, drops or invents words when a window starts mid-word); re-decoded committed tokens are matched case-insensitively. Portability: no M_PI / GCC builtins on MSVC, NEON paths off under MSVC, bench is POSIX only. Documents the parakeet backend semantics in the ABI header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tivation modes The relative-position keys are cached per layer, so they are now computed with residual (two-pass) int8 activations at no run-time cost. The parity test documents its two modes: the shipped int8-activation path (encoder relative L2 ~0.07-0.2, transcripts gated) and PK_DEBUG_F32_ACTS=1, which checks the graph itself against the dense reference (L2 <= 0.005). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for the key cache When the CPU is oversubscribed (an LLM decoding next to ASR on a phone), a spinning caller or worker can starve a preempted peer; both now yield after a short spin. PK_DEBUG_F32_ACTS skips the second (residual) pass of the position-key cache, which the fp32 diagnostic already covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps both the Paradee/misaki and the Parakeet CMake hooks; the parakeet backend note moves under the ABI v18 changelog entry (no new symbols). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gates Streaming (pk-stream): - PK_STREAM_DEBUG logged raw token pieces and tentative text; it now logs window geometry and token counts only. - The last word before a pause waited for the next word, which pinned the segment start, so the window grew through silence (a 3 s utterance followed by 40 s of quiet re-decoded windows up to 41 s, 1.5 s each). The newest word now commits once nothing follows it for right_context. - With nothing uncommitted and only quiet audio since the last decode, the decode is skipped and the window keeps the newest 1.5 s of the pause. Same clip: 22 decodes / 2.2 s of compute instead of 32 / 13.5 s, and no words invented on short windows of near-silence. - A window past max_window with no token keeps its newest half. - Partial/finish truncate on a UTF-8 boundary. FFI (eliza-asr-parakeet.inc): - asr_stream_feed raced partial/finish on the stream's audio buffer when called from another thread (the JNI shape). Feed now only resamples into a pending buffer under a per-stream mutex; partial/finish drain it under asr_mutex. Feed after finish fails. - The model generation counter is atomic (contexts bump it under different mutexes). - eliza_free_asr checked ctx for NULL after dereferencing it. Loader: check GGUF value types before gguf_get_val_* (which abort on a mismatch), and reject empty/negative TDT durations (a negative duration looped forever) and blank_id != vocab. Position-key cache: grow by computing only the new rows. Rows are independent, so the cache is bit-identical to a full rebuild; a stream with a growing window no longer rebuilds all 24 layers at every 64-frame step. Parity gate: an empty fixture set now fails, and a pooled WER against the reference transcripts (PK_GATE_REF_WER, default 0.02) bounds how far the utterances that differ may drift. FFI test: transcripts are no longer printed; adds a feeder thread racing partial polls, feed-after-finish and a tiny-buffer partial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds
tools/parakeet, a native CPU runtime for FermionResearch/Phonon-2, and serves it through the existingeliza_inference_asr_*surface in libelizainference, including the streaming session (asr_stream_open/feed/partial/finish).Phonon-2 is nvidia/parakeet-tdt-0.6b-v3 (FastConformer + TDT, English) with every encoder linear retrained to five values per row (
{-hi,-lo,0,+lo,+hi}). It downloads as 164 MB. There is no ONNX, MLX, Core ML or Python at run time, and no new ABI symbols and no version bump. The base is now at v18, and the behaviour notes sit under that changelog entry.Licence (flagged, not a blocker).
parakeet.notice, also available frompk_model_notice()).What is in it
Converter.
convert_phonon2_to_gguf.pyreads the pinnedphonon-2.bps.tar.zstand checks it againstpins.json(HF revision7e153e40…, sha256 of every input and archive member).fv5-pair16-v1). The GGUF is mmapped and used in place.--verifyre-expands every record bit for bit against the container, checks that all 723 records are consumed, and checks that the mel filterbank matches librosa's. The output is 330 MB, deterministic for the same inputs.Kernels. A per-8-row panel is decoded with one
TBLper 16 weights, then multiplied with:SMMLA(i8mm), 8x8 tiles;SDOT(dotprod), on straight and half-swapped token pairs;Each NEON tier is its own translation unit with its own
-march, entered after anHWCAP/HWCAP2orsysctlcheck. All tiers are bit-identical.Model. The encoder and decoder are plain C++ on a persistent worker pool. On Android the pool pins to the big cores; on Apple it uses QoS user-interactive.
Streaming. Each decode covers a pause-delimited window.
libelizainference (
src/eliza-asr-parakeet.incplus small#ifdef ELIZA_ENABLE_PARAKEEThooks).mmap_acquire("asr")loads a GGUF under<bundle>/asr/whosegeneral.architectureisparakeet-tdt. Otherwise the Qwen3-ASR path runs unchanged.ELIZA_ASR_BACKEND=qwen3|parakeetoverrides the choice.asr_transcribe_timedreturns real TDT word times.asr_stream_supported()is 1 while a context has the backend loaded.*io_n_tokens = 0), because the vocabulary is not the text model's.asr_cancelis polled between encoder layers and decode steps.mmap_evict("asr")frees the model, and an open stream then fails cleanly.ELIZA_ASR_USE_MMAP) applies.pk_*symbols are not exported.Review round (4efc306)
An adversarial review found these problems, now fixed:
Transcript text in logs.
PK_STREAM_DEBUGprinted raw token pieces and tentative text. It now prints window geometry and token counts only. The FFI test no longer prints transcripts either.Feed/partial race.
asr_stream_feedcalled from an audio thread racedpartial/finishon the stream's buffer (use-after-free on vector growth). Feed now only resamples into a pending buffer under a per-stream mutex and never waits for a decode;partial/finishdrain it underasr_mutex. Feed after finish fails. The new concurrent test (a feeder thread racing partial polls) passes under ThreadSanitizer with no reports.Windows grew through pauses. The last word before a pause waited for the next word and pinned the segment start. A 10 s utterance followed by 40 s of quiet re-decoded windows up to 41 s (about 1.5 s of compute each). Now:
right_contextpasses with no further token;max_windowwith no token keeps its newest half.On the same clip that is 22 decodes and 2.2 s of compute instead of 32 and 13.5 s, and the stream no longer invents words in the pause. An intermediate fix that cut idle windows short produced "Okay. Yeah. Yeah." on 1.4 s noise windows, because the feature normalisation is per window. Skipping decodes on quiet audio avoids that.
Loader.
gguf_get_val_*aborts on a type mismatch, so value types are now checked first. An empty or negative TDT duration table is rejected (a negative duration looped forever), and so isblank_id != vocab.FFI. The model generation counter is now atomic, since contexts bump it under different mutexes.
eliza_free_asrdereferencedctxbefore its NULL check. Partials truncate on a UTF-8 boundary.Parity gate. An empty fixture set passed; it now fails. A pooled WER against the reference transcripts (
PK_GATE_REF_WER, default 0.02) bounds how far the utterances that differ may drift, on top of the "90% identical" gate.Speed. The position-key cache now computes only the new rows when it grows. The result is bit-identical (rows are independent, and every transcript is unchanged), and a stream with a growing window no longer rebuilds all 24 layers every 64 frames.
Fresh numbers (this head, same subsets, Apple M4 Max as an ARM CPU proxy, not phone evidence; host load average 30–58)
These differ from the table below (4.14 / 67/78). That table came from an older head; these come from this head and the previous one, run back to back.
At 4 threads the wall-clock RTFx was 21–72x on LibriSpeech and 26–66x on commands, with the median first-1 s decode at 17–31 ms. The spread comes from contention on the shared host.
Streaming, 1 thread, CPU.
Parity.
PK_DEBUG_F32_ACTS, at most 0.0007.Android NDK r29 (
android-28), build evidence only.arm64-v8aandx86_64build cleanly, includinglibelizainference.so,parakeet_liband the parakeet tests. No warnings come from the new code..socontainssmmlaandsdotand exportseliza_inference_asr_stream_*.pk_*symbols are exported.Measurements (earlier head)
All figures come from an Apple M4 Max used as an ARM CPU proxy. This is not phone evidence. The host was shared and heavily loaded (load average 60–280 on 16 cores from other jobs). That makes wall-clock numbers unreliable, so single-thread speed uses process CPU time, minimum of 2–3 repeats.
The engines were run back to back on the same inputs: the 78-utterance LibriSpeech subset (610 s, test-clean + test-other, seed 1234) and 20 synthetic voice commands (30.5 s). WER uses the Open ASR Leaderboard normaliser.
reference_transformers.py, WER 4.26)Comparison with earlier evaluation runs on the same subset.
Multi-thread. I have no meaningful numbers from this host. At 4 threads the load average was 85–215, so each thread got a fraction of a core.
The pool now yields after a short spin, so a preempted worker is not starved. Scaling has to be measured on a phone's big cores.
Streaming (defaults, 1 thread, 16 long LibriSpeech utterances + 20 commands).
ELIZA_ASR_STREAM_{RIGHT,HOP,MAX_WINDOW}_MStrade cost against accuracy. For example,hop=800gives about 0.25x real time with stream WER 5.45.Silence. 3 s of digital silence and of -60 dB noise both transcribe to an empty string, with no VAD needed. Qwen3-ASR returns "The." on silence.
Kernel throughput (one P-core, CPU time).
Tests
test-parakeet-kernels: dotprod and i8mm are bit-identical to scalar for the panel decode, every tile width, the matvec and the whole GEMM driver across thread counts. The GEMM matches an exact five-value product within 3e-4. Passing.test-parakeet-parity model.gguf fixtures/: the fixtures come fromdump_parakeet_reference.py, which runs Fermion's own dense fp32 reference.PK_DEBUG_F32_ACTS=1(same graph and weights, fp32 activations), run on all 26 fixtures: encoder relative L2 is at most 0.0007 on LibriSpeech and at most 0.0105 on commands, mean frame cosine at least 0.99996, and 26/26 transcripts are identical. The remainder comes from the fp16 key cache and the int8 grid of the five-value levels. This confirms the graph matches the reference, so the int8 path's deviation is activation quantisation alone.eliza-inference-test-parakeet-asrruns through the real libelizainference: probe 0 → 1 → 0 across acquire and evict; batch equals timed equals expected; word count matches a whitespace split and word times are monotonic; a 48 kHz stream gives partials and a final transcript;asr_cancelfrom another thread returnsELIZA_ERR_CANCELLED; the evicted-stream error and re-acquire both work. Passing. It skips unlessELIZA_TEST_PARAKEET_BUNDLE/_PCMare set.eliza-inference-test-abi: passes, unchanged.android-28) build evidence only.elizainference,parakeet_liband the parakeet tests build for arm64-v8a and x86_64. The arm64libelizainference.socontains thesmmlaandsdotpaths and exports theasr_stream_*symbols. Nothing was run on an emulator or a device.CI. Python type-check, flake8 Lint, model-naming and the wasm build pass.
ubuntu-cpu,ggml-ci-*,ubuntu-24-webgpu,ubuntu-latest-rpcandwindows-latest-hipfail. The base head79b9437fails the same jobs (plus more), so these failures are not from this change.-Wall -Wextra -Wpedantic -Wcast-qual -Wmissing-declarations -Wextra-semi;ELIZA_ENABLE_PARAKEETunder GCC 15.Not done
Device evidence. RTFx at 1 thread and on the big cores, first partial, RSS and thermals on a Pixel, plus microphone acceptance. Host numbers do not stand in for these.
eliza-side wiring. This needs:
voice-model-versions.json);phonon-2-fv5.gguf, 330 MB, sha256dc552eb7…, byte-reproducible and pinned inpins.json. Its compressed size was not measured; the information content is that of the 164 MB release;ElizaVoiceNative);The Qwen3-ASR Q8_0 pair it would replace maps about 1.0 GB.
x86_64 speed. The scalar tier runs at about 0.6x real time, which is slow on x86_64 emulators. An AVX2 tier would fix that.
Activation precision. Per-group activation scales would bring the int8 path closer to the reference, at about 10–15% speed. WER already equals Fermion's engine and beats the dense reference on this subset, so I left it out.
Coordination. #64 (Paradee), #63 (misaki) and #56 (ABI v18) have merged since this branch started. The current base is merged in, both sets of CMake hooks are kept, and the host FFI tests pass on the merged tree.
🤖 Generated with Claude Code