Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

localvqe-c

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.

Build

Requires only CMake ≥ 3.16 and a C11 compiler:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j

Produces 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.

Run

# 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.npy

mic 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.

Performance

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); set LVQE_THREADS to override (LVQE_THREADS=1 for 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 vectorized expf; 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.

C API

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.

Layout

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

Testing

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-failure

test/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.

Numerical parity

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

docker build -t localvqe-c .
docker run --rm -v /path/to/LocalVQE/ggml:/ref:ro localvqe-c   # runs ctest

Verified dependency-free on linux/amd64 and linux/arm64.

License

Apache 2.0, matching upstream LocalVQE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages