Skip to content

community_models: VibeASR I8_S VAE encoder - #440

Closed
XsquirrelC wants to merge 7 commits into
0xShug0:mainfrom
XsquirrelC:vibeasr-vae-encoder
Closed

community_models: VibeASR I8_S VAE encoder#440
XsquirrelC wants to merge 7 commits into
0xShug0:mainfrom
XsquirrelC:vibeasr-vae-encoder

Conversation

@XsquirrelC

@XsquirrelC XsquirrelC commented Sep 4, 2026

Copy link
Copy Markdown

Stacked on #438 (the I8_S/I2_S types and fused ops). This branch contains #438's three commits plus one; please review/merge #438 first — only community_models: VibeASR I8_S VAE encoder (a631fab) is new here.

Ports the audio VAE encoder from Microsoft's VibeASR.cpp, which runs the VibeVoice acoustic/semantic tokenizers on INT8 weights and INT8 activations end to end.

Relation to the existing vibevoice_asr family

CONTRIBUTING asks not to duplicate an already-listed family, so to be explicit: this is the same model audio.cpp already ships as vibevoice_asr, and this PR does not duplicate that family's loader, session, connector, or decoder. What is new is the numeric pipeline — I8_S weights with INT8 activations through the fused ops from #438, where the core family runs F32/Q8_0 activations in F32. Upstream's decoder is BitNet-style ternary I2_S; that half is not ported here (two follow-up PRs: the I2_S matmul kernel, then the decoder plus loader and session).

It stays an additive community_models/vibeasr entry rather than a weight path inside vibevoice_asr, because the two share no graph code: every activation here is I8_S and every node is one of the fused CPU-only 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 — is data and conventions, and this entry follows vibevoice_asr on all of it. The trade-off is written up in docs/community_models/vibeasr.md.

What is in this PR

  • tools/community_models/convert_vibeasr_vae.py — remaps GGUF tensor type ids. Upstream's fork put I2_S/I8_S at 36/37, which upstream ggml already spent on the retired IQ4_NL_4_4/IQ4_NL_4_8 slots; audio.cpp registers them at 42/43. On-disk layout is identical, so the tool rewrites the 4-byte type field per tensor info and copies everything else byte for byte (offsets, data section, and KV block untouched). --list / --check modes for inspection.
  • include/engine/community_models/vibeasr/{assets,vae_encoder}.h, src/community_models/vibeasr/{assets,vae_encoder}.cpp — geometry derived from the tensor table (which block tensors exist, what shape each weight is) rather than GGUF metadata, then the encoder graph: 7 stages, strides {1,2,2,4,5,5,8} (3200 samples/frame), depths 3-3-3-3-3-3-8, channels 32→2048, causal convs, ConvNeXt blocks with layer scale, latent head (64 acoustic / 128 semantic), connector out at 1536.
  • tests/vibeasr/test_vibeasr_vae_encoder.cpp — parity probe, registered in ctest with SKIP_RETURN_CODE 125 so a checkout without the 703 MB package skips instead of failing.
  • Docs: per-model page with the upstream backlink, plus rows in README, docs/community_models/models.md, and a pointer from the vibevoice_asr section of docs/asr.md.

No loader and no CLI family: without the decoder there is no session to register, so check_loader_catalog_sync.py has nothing new to advertise (run anyway, clean).

Build

cmake -B build -DCMAKE_BUILD_TYPE=Release -DENGINE_BUILD_MODEL_TESTS=ON
cmake --build build -j$(nproc)

Run

python3 tools/community_models/convert_vibeasr_vae.py \
    --input vibeasr-vae-encoder-i8_s.gguf \
    --output models/vibeasr/vae_encoder-i8_s.gguf

./build/bin/test_vibeasr_vae_encoder \
    --model models/vibeasr/vae_encoder-i8_s.gguf \
    --audio assets/asr_validation/librispeech/librispeech_test_clean_6930-75918-0000.wav \
    --reference-acoustic ref_acoustic.f32 \
    --reference-semantic ref_semantic.f32 \
    --threads 8

Model path: models/vibeasr/vae_encoder-i8_s.gguf (703 MB, both branches). No model-manager package id — the upstream weights are not published as an audio.cpp package. Reference dumps are raw F32 [frames][dim] row-major from upstream's own vae_encode_acoustic / vae_encode_semantic.

Parity

3.505 s LibriSpeech clip, 17 frames × 1536 per branch, CPU backend:

Branch max abs mean abs cosine
acoustic 1.478 (12.1% of range) 0.0930 (0.76% of range) 0.99238739
semantic 2.526 (9.5% of range) 0.1804 (0.68% of range) 0.98475210

Layer by layer, stage 0 is bit-exact — every int8 byte and every scale — which is what actually pins the layouts, causal padding, kernel padding, and 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 compounds because every later stage requantizes.

Bit-exactness is not reachable: audio.cpp stores each per-tensor scale as a multiplier (amax/127, dequantize by multiplying) and upstream stores its reciprocal (127/amax, dequantize by dividing) — equal to within the last float bit, which is enough to flip a value sitting on a rounding boundary. Upstream also rounds ties to even in its vector body but away from zero in its scalar tail, so no single convention reproduces it.

For calibration, nudging one input sample by one int8 step and re-running upstream against itself moves its own output by cosine 0.99592 (acoustic) / 0.98700 (semantic) — the graph amplifies a single LSB about as far as the two implementations differ. So the probe gates on mean-abs-relative ≤ 2% and cosine ≥ 0.98; tighter would be testing rounding luck.

Also in this branch: #438's i8_s_fused_ops_test gains a case for the in-band scale surviving ggml_cont(ggml_permute(...)). The encoder flips activations between channel-major and length-major constantly, and ggml_compute_forward_dup was copying the payload without the scale — values stayed right while everything downstream was off by an arbitrary factor (that was the difference between cosine 0.26 and 0.99 end to end).

Backend and performance

CPU only — the fused I8_S ops have no CUDA or Metal kernels, and the probe pins the backend to CPU. Release build, 24-core AMD EPYC 7V13, 3.505 s clip, both branches:

Threads acoustic semantic both RTF
8 276 ms 270 ms 546 ms 0.156
1 1657 ms 1600 ms 3257 ms 0.929

Peak RSS 1.31 GB against 703 MB of weights: BackendWeightStore stages each tensor before upload, so load briefly holds roughly two copies. Graph arena 64 MB by default. No VRAM.

Tests

ctest --test-dir build -j8   # 100% tests passed, 0 failed out of 66 (2 pre-existing skips)
python3 tools/check_loader_catalog_sync.py   # ok

test_vibeasr_vae_encoder passes with the package present (2.41 s) and skips without it. i8_s_fused_ops_test passes at 1 and 4 threads; I confirmed the new cont(permute) case fails if the scale propagation is removed.

Known limitations

  • Encoder only. The ternary I2_S decoder is not ported, so nothing transcribes through this path yet. Encoder output is the LM input, so the halves are independently reviewable but only useful together.
  • CPU only, offline only (upstream's encoder is causal so streaming is implementable, but the state machine is not ported).
  • Bit-exact parity with upstream is out of reach by design, as above.
  • No release GGUF package or WebUI catalog entry; convert locally from upstream weights.

Upstream: https://github.com/microsoft/VibeASR.cpp

@0xShug0

0xShug0 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

I went with an additive community_models/vibeasr entry so the encoder is reviewable on its own.

@XsquirrelC A dedicated family is the cleaner way to go.

@XsquirrelC

Copy link
Copy Markdown
Author

Closing in favour of #448. You asked for two PRs in microsoft/VibeASR.cpp#10 — one for the additive ggml changes, one for the model integration — so this four-PR stack has been reorganized into exactly that: #447 (ggml) and #448 (model). Same code, no functional change; sorry for the churn.

@XsquirrelC XsquirrelC closed this Sep 4, 2026
@XsquirrelC
XsquirrelC deleted the vibeasr-vae-encoder branch September 4, 2026 08:05
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