community_models: VibeASR INT8/ternary ASR pipeline (2/2) - #448
Open
XsquirrelC wants to merge 7 commits into
Open
community_models: VibeASR INT8/ternary ASR pipeline (2/2)#448XsquirrelC wants to merge 7 commits into
XsquirrelC wants to merge 7 commits into
Conversation
This was referenced Sep 4, 2026
Owner
|
@XsquirrelC Thanks for the update! I review PRs in a queue so #447 has been merged early. Could you rebase and upload the missing model? We’re good to go after that. |
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.
Per your request in microsoft/VibeASR.cpp#10, split into two PRs: #447 is the additive ggml side, this one is the model integration. Stacked on #447 — this branch contains it, so review #447 first; the three commits from
community_models: VibeASR I8_S VAE encoderonward are what's new here.This replaces #440 and #446, which are now closed — same code, reorganized so the ggml and model halves are cleanly separated.
Result:
--task asr --family vibeasrtranscribes end to end on CPU.Why this is a separate family and not a weight path in
vibevoice_asrWorth addressing head on, because audio.cpp already ships this model:
vibevoice_asrin the core tree is the same architecture — same acoustic/semantic causal ConvNeXt tokenizers, same connectors, same Qwen2 decoder. What VibeASR.cpp adds is a different numeric pipeline, not a different model:vibevoice_asr(core)GGML_TYPE_I8_S, one F32 scale per tensorGGML_TYPE_I2_S, 993 MB for a 1.5B decoderIt stays separate because the two share no encoder graph code: every activation there is I8_S and every node is one of the CPU-only fused ops, so folding it in would put a second, mutually exclusive graph builder and a second backend policy behind one family's loader. The reuse that is worth having — tokenizer vocabulary, prompt layout, feature-injection order, audio normalization — is data and conventions, and this follows
vibevoice_asron all of it, including using its exact audio front end rather than upstream's. Happy to fold it in as a weight path instead if you'd rather; the graph code is the only obstacle and it is contained in two files.The decoder reuses the framework outright
modules::QwenCausalDecoderModulebuilds it unchanged. Every projection in it goes throughLinearModule, which is a bareggml_mul_maton a 2-D flattened activation, so a ternary weight dispatches on type with nothing to opt into — that is what theggml_mul_matI2_S support in #447 buys. The published checkpoint leaves the embedding table at Q6_K and the output projection at F16 and both load through the framework's normal path; only the 196blk.N.*projection weights need a dtype/shape check plusstore.make_tensor(..., GGML_TYPE_I2_S, ...).Geometry comes from the LM GGUF's KV block (28 layers, hidden 1536, intermediate 8960, 12 heads over 2 KV heads, RMSNorm eps 1e-6, RoPE theta 1e6). The checkpoint has no
qwen2.attention.key_length, sohead_dimfalls back toqwen2.rope.dimension_count, cross-checked againsthead_dim * head_count == embedding_lengthand againsttoken_embd.weight's actual shape rather than trusted. The encoder's geometry is derived from the tensor table instead — which block tensors exist, what shape each weight has — the same way upstream does it, since the checkpoint's KV block does not describe the VAE.Two graphs per session: a prefill graph sized to the prompt, and a single-step decode graph over a static KV cache. Speech features are injected with
ggml_set_rowsover theEmbeddingModuleoutput, so the embedding lookup and the<|speech_pad|>overwrite are one pass instead of a host-side gather. Both encoder branches emit 1536-wide features — decoder hidden size — and the reference sums them element-wise.Faithful-to-upstream details that are easy to get wrong
parse_special = false. The GGUF vocabulary still carries Qwen2.5's original text for those slots while the embedding rows are the ones VibeVoice trained, so tokenizing the literal text lands on the wrong rows.<|im_start|>assistant\nheader and the session strips that leading triple, asasr_server.cppdoes. Without the strip the transcript isassistant\nConcord returned….ceil(samples / 3200)pad tokens but prefills onlymin(pads, frames), so emitting exactlyframespads yields the same sequence.Packaging
The published package is two GGUFs — 703 MB encoder, 993 MB decoder — plus the tokenizer. A
--modeldirectory with more than one GGUF is rejected byrequire_selected_source, and a single component GGUF has no embedded spec, so this uses the conventionminimax_h3already documents:--model <one component>.gguf --model-spec-override model_specs. No framework change, and no merged GGUF that would diverge from what upstream publishes. Say the word if you'd rather have a single mergedmodel.ggufwith an embedded spec and I'll add the packaging step.tools/community_models/convert_vibeasr_gguf.py(renamed fromconvert_vibeasr_vae.py, since the same fix serves both halves) rewrites the 4-byte type field per tensor info and copies everything else byte for byte — data offsets, data section, and KV block untouched. Flags:--in-place,--output,--list,--check.Parity
Four LibriSpeech clips, greedy on both sides, against VibeASR.cpp's own
asr_infer --greedyon the same two GGUFs:Concord returned to its place amidst the tents.The english forwarded to the french baskets of flowers, …(43 tokens)Don't cry, he said. I was obliged to come.I'm from the cut or lying off the coast.I'm from the cutter lying off the coast.Three of four match token for token. The fourth diverges because these clips are 16 kHz and the resamplers differ: this port uses audio.cpp's
vibevoice_asrfront end (soxr, plus themax_abs > 1clamp), upstream uses naive linear interpolation and no clamp. The reference transcript isI AM FROM THE CUTTER LYING OFF THE COAST, so this port is the one that gets it right, but the point is that the difference is the front end and not the graph. A clip already at 24 kHz skips resampling and does not have this failure mode. Both divergences are commented at the point they happen.Encoder-level, against reference F32 dumps from VibeASR.cpp's own
vae_encode_acoustic/vae_encode_semantic, 3.505 s clip at its native 16 kHz, 17 frames × 1536:Stage 0 is bit-exact, every int8 byte and every scale — that is what pins the layouts, the causal padding, the kernel padding, and the weight mapping. The first divergence is 5 of 1,794,560 elements one int8 step apart at an identical scale, entering stage 1, and it grows because each remaining stage requantizes.
Bit-exactness is not reachable and the tolerances say so, which I'd rather state than paper over. The scale convention differs in the last float bit (multiplier vs reciprocal, see #447), and upstream rounds ties to even in its vector body but away from zero in its scalar tail, so no single convention reproduces it. To calibrate: nudging one input sample by one int8 step and re-running VibeASR.cpp against itself moves its own output by cosine 0.99592 / 0.98700 — the graph amplifies a single LSB about as far as the two implementations differ. The probe gates on mean-abs-relative ≤ 2% and cosine ≥ 0.98; anything tighter would test rounding luck.
One more:
output_format=jsonreturns an empty transcript on short single-speaker clips, because the model emits an immediate end-of-turn. I checked upstream on the same input before assuming it was my bug —asr_infer --prompt-format jsonis also empty. Documented, not worked around.Performance
Release, gcc, x86-64 AVX2, 24 vCPU EPYC 7V13. 3.505 s clip resampled to 24 kHz: 26 speech frames, 72-token prompt, 13 generated tokens.
RTF 0.367 at 8 threads, so within the community-model bar. The encoder dominates — it runs twice, once per branch, over raw samples rather than tokens. Decode is ~18 ms/token at 8 threads.
Peak RSS 2.20 GB against 1.70 GB of weights, because
BackendWeightStorestages each tensor before upload. Graph arenas are 64 MB (encoder) + 256 MB (prefill) + 256 MB (decode) and all three are session options (vibeasr.encoder_graph_arena_mbetc.). Memory is stable across requests — the graphs are built once per session and reused.Tests
tests/vibeasr/test_vibeasr_asr.cpp(test_vibeasr_asr) loads through the real registry, creates an{Asr, Offline}session, and requires raw equality with the reference transcript, punctuation and casing included; a normalized compare runs only to localize a failure.tests/vibeasr/test_vibeasr_vae_encoder.cpp(test_vibeasr_vae_encoder) checks shape, finiteness, and frame count, and with--reference-acoustic/--reference-semanticreports the parity numbers above and fails outside the tolerances.Both exit 125 (SKIP) when the checkpoint is not installed, so neither needs weights in CI. Full
ctestwith the checkpoints present: 70/70 pass, 0 failures (2 pre-existing parakeet skips for missing assets).python3 tools/check_loader_catalog_sync.pyreports runtime loaders,model_specs, andmodel_manager_v2in sync.Reproduce:
Docs
docs/community_models/vibeasr.mdcovers the architecture, both parity sections, performance, the packaging convention, and the limitations (CPU only, offline only, greedy only, text only).README.md,docs/community_models/models.md, anddocs/asr.mdget the family row and the cross-reference tovibevoice_asr.Upstream
src/vae.cpp,src/lm.cpp,src/asr_server.cpp,utils/prompt_builder.h)vibevoice_asr