A pure-C, CPU-only port of LocalVQE — a compact neural model for joint acoustic echo cancellation, noise suppression, and dereverberation of 16 kHz speech. The upstream engine is C++ on top of GGML; this is a from-scratch C reimplementation of the CPU F32 inference path. No external dependencies — only the C standard library and libm. No GGML, no GPU, no BLAS (optional). Builds on arm64/x86_64, macOS/Linux.
Targets the v1.2 / v3 model (localvqe-v1.2-1.3M-f32.gguf): STFT-256 analysis,
CausalGroupNorm + SiLU, n_fft 512, hop 256, dmax 64, S4D bottleneck. The legacy v1
(ELU/BatchNorm) and v1.1 (ReLU6) graph paths are implemented but only v3 is exercised.
Requires only CMake ≥ 3.16 and a C11 compiler:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jProduces build/liblocalvqe.a, the build/localvqe CLI, and test binaries. On macOS
the result links only libSystem; on Linux only libc/libm.
The built-in matmul is the default because the only matmuls are small matrix-vector
products (the 512×512 DCT heads) — BLAS makes no measurable difference here (the heavy
compute is the hand-rolled conv2d). To link a tuned CBLAS anyway (Apple Accelerate on
macOS, OpenBLAS on Linux via apt install libopenblas-dev), configure with
-DLOCALVQE_USE_BLAS=ON.
# WAV in/out (16 kHz mono PCM16):
./build/localvqe model.gguf --in-wav mic.wav ref.wav --out-wav enhanced.wav
# float32 .npy in/out (for bit-level comparison against the reference engine):
./build/localvqe model.gguf --in-npy mic.npy ref.npy --output enhanced.npymic is the microphone signal; ref is the far-end reference (what plays through the
speakers, for echo cancellation). Both must be 16 kHz mono. With a silent/zero ref the
model still runs noise suppression + dereverb.
Hot paths are tuned for CPU while staying portable C (no intrinsics, no external deps):
- conv2d runs as im2col + a register-blocked GEMM (4 output channels per col-row read), with the inner reduction structured so the compiler vectorizes it.
- output channels are split across a small pthread pool (no OpenMP/libomp).
Thread count defaults to
min(4, ncores); setLVQE_THREADSto override (LVQE_THREADS=1for single-threaded). Tiny convs run inline to avoid dispatch cost. - CausalGroupNorm is a fused two-pass kernel; broadcast add/mul drop the
per-element modulo; concat and pad are
memcpy-based; SiLU uses an inlinable vectorizedexpf; weight lookups go through an O(1) hash.
Per-hop processing time (model resident, each project's own bench harness):
| 1 thread | 4 threads | |
|---|---|---|
| this port (Apple M5 Max) | 2.3 ms/frame (6.8× RT) | 1.5 ms/frame (11.0× RT) |
| this port (Apple M2) | 3.9 ms/frame (4.1× RT) | 2.3 ms/frame (7.0× RT) |
| original (C++/GGML, Apple M2) | 4.5 ms/frame (3.6× RT) | 1.6 ms/frame (9.3× RT) |
Single-threaded the port is slightly faster than the GGML engine; the GGML build scales better with threads (it parallelizes the whole graph and ships tuned SIMD kernels, whereas only the larger convs are threaded here). Both are comfortably realtime (16 ms budget/frame). Output is bit-identical across thread counts. For reference, the un-optimized first pass was ~23 ms/frame — the im2col rewrite alone was the bulk of the ~6× speedup.
src/localvqe.h exposes an opaque-handle API: localvqe_new, localvqe_process_f32 /
_s16 (batch), localvqe_process_frame_f32 (one 256-sample hop, streaming),
localvqe_reset, localvqe_set_noise_gate, localvqe_free.
src/lv_tensor.{c,h} minimal tensor engine (ggml-compatible semantics; mul_mat→CBLAS)
src/gguf.{c,h} F32 GGUF v3 reader
src/wav.{c,h} 16-bit PCM mono WAV I/O
src/npy.{c,h} float32 .npy I/O
src/model.{c,h} hparams + named weights
src/graph.{c,h} per-frame forward pass + streaming history
src/api.c C API (windowing, overlap-add, noise gate)
src/cli.c command-line tool
test/ regression test, npy compare, NumPy reference
ctest runs a load smoke test and an end-to-end regression against the reference
engine's committed fixture (LocalVQE/ggml/tests/fixtures/). Point CMake at the
reference checkout with -DLOCALVQE_REF_DIR=/path/to/LocalVQE/ggml (defaults to a
sibling checkout). Tests skip cleanly (exit 77) if the model/fixtures are absent.
ctest --test-dir build --output-on-failuretest/numpy_ref.py is an independent, memory-faithful NumPy reference of the forward
pass — useful for block-level debugging; it agrees with the C engine to ~5e-7.
The port matches the reference ggml engine's regression fixture to max-abs 8.4e-4
(the engine's own tolerance is 1e-2). On long, loud clips the output tracks the engine
at correlation ~0.99 but can differ by a few percent: the ggml CPU backend uses
SIMD-approximate exp in SiLU/softmax while this port uses correctly-rounded libm,
and the marginally-stable S4D bottleneck (pole ≈0.99) integrates that small per-frame
difference over time. This is a function-approximation difference, not a precision bug
— a float64 build of the reference agrees with the float32 C engine to 2.7e-7.
docker build -t localvqe-c .
docker run --rm -v /path/to/LocalVQE/ggml:/ref:ro localvqe-c # runs ctestVerified dependency-free on linux/amd64 and linux/arm64.
Apache 2.0, matching upstream LocalVQE.