# Fun-ASR-Nano on llama.cpp / GGUF Run **Fun-ASR-Nano** entirely on the [llama.cpp](https://github.com/ggml-org/llama.cpp) / ggml stack — **CPU, edge, a single binary, no Python at runtime**. This is to Fun-ASR what [whisper.cpp](https://github.com/ggml-org/whisper.cpp) is to Whisper. ## Why this exists Fun-ASR-Nano normally runs on PyTorch / vLLM (GPU). That is great for a server serving many requests, but it cannot run where there is no GPU and no Python. This runtime ports the model to **ggml + GGUF**, so Fun-ASR-Nano can run: - on a laptop / phone / Raspberry Pi / edge box, offline, CPU-only; - embedded directly into a C/C++ application (one static binary); - with quantized weights (Q8 / Q4), shrinking the model to ~1.3 GB total. | | vLLM (existing) | this runtime (llama.cpp) | |---|---|---| | target | GPU server, high QPS | CPU / edge / embedded | | deps | Python + CUDA + PyTorch | none (C/C++ single binary) | | weights | HF fp16/bf16 | GGUF, quantized | | best for | online service, batch | offline, on-device | ## Architecture Fun-ASR-Nano = **SenseVoice SAN-M encoder (70 layers) + adaptor + Qwen3-0.6B LLM**. The whole pipeline runs in C++: ``` audio.wav (16k mono) │ kaldi 80-mel fbank + LFR (C++) ▼ features [T, 560] │ SAN-M encoder + adaptor (ggml) ── funasr-encoder.gguf ▼ audio embeds [T', 1024] │ keep first fake_token_len frames (low-frame-rate) ▼ [ prefix tokens | audio embeds | suffix tokens ] │ Qwen3-0.6B, embeds injected via llama_decode (llava/mtmd style) ── qwen3-0.6b.gguf ▼ transcription ``` The audio embeddings are fed into the LLM through `llama_decode`'s embedding-input path — exactly how llava/mtmd inject vision embeddings. ## Quickstart **1. Build** (drop the examples into a llama.cpp checkout): ```bash git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp cp -r /path/to/runtime/llama.cpp/funasr-cli examples/ echo 'add_subdirectory(funasr-cli)' >> examples/CMakeLists.txt cmake -B build -DGGML_NATIVE=ON -DLLAMA_CURL=OFF cmake --build build -j --target llama-funasr-cli ``` **2. Convert weights to GGUF** (one-time; needs the checkpoint, e.g. `FunAudioLLM/Fun-ASR-Nano-2512`): ```bash # LLM half — Qwen3-0.6B is natively supported by llama.cpp python llama.cpp/convert_hf_to_gguf.py /Qwen3-0.6B-vllm \ --outfile qwen3-0.6b-f32.gguf --outtype f32 build/bin/llama-quantize qwen3-0.6b-f32.gguf qwen3-0.6b-q8_0.gguf Q8_0 # smaller, recommended # audio half — SenseVoice encoder + adaptor python runtime/llama.cpp/export_encoder_gguf.py \ --model_pt /model.pt --out funasr-encoder.gguf # f32, 935 MB python runtime/llama.cpp/export_encoder_gguf.py \ --model_pt /model.pt --out funasr-encoder-f16.gguf --wtype f16 # 469 MB ``` **3. Transcribe:** ```bash build/bin/llama-funasr-cli \ --enc funasr-encoder.gguf -m qwen3-0.6b-q8_0.gguf \ -a audio.wav --chunk 15 ``` Expected output (one of the benchmark clips): ``` 我想问我在滨海新区有房我一直没有照顾孩子但是我想要抚养权...你觉得这是正常的想法吗 [done] 7.40s ; chunk=15s ``` ## Models & sizes | file | dtype | size | |---|---|---| | funasr-encoder.gguf | f32 | 935 MB | | funasr-encoder-f16.gguf | f16 (matmul weights) | 469 MB | | qwen3-0.6b-f32.gguf | f32 | 3.0 GB | | qwen3-0.6b-q8_0.gguf | Q8_0 | 805 MB | | qwen3-0.6b-q4km.gguf | Q4_K_M | 484 MB | Fully-quantized config (f16 encoder + Q8 LLM) ≈ **1.3 GB**, edge-friendly. ## Accuracy & validation Validated against the PyTorch reference on the 184-file benchmark: - **Encoder + adaptor (ggml) vs PyTorch:** cosine **1.000000**, max_abs_diff **5e-3** (f32). - **kaldi fbank (C++) vs torchaudio:** cosine **1.000000**. - **End-to-end CER, identical conditions (f32 LLM, 15 s chunking):** C++ macro 17.41% / micro 11.68% vs PyTorch macro 17.42% / micro 11.70% → aggregate CER matches to **0.02%**; the port is faithful. - Best practical config (Q8 LLM + 15 s chunking): **micro-CER 9.51%** (production VAD-segmented reference is ~8.2%; the gap is fixed-window vs VAD, not the port). ## Tips & gotchas - **Use `--chunk 15`** for long audio. Decoding a whole 60 s clip in one segment is out-of-distribution and makes greedy decoding loop; 15 s windows fix it (micro-CER 29% → 9.5%). - **Low-frame-rate truncation** is required: only the first `fake_token_len` adaptor frames are real audio tokens. The CLI does this automatically; feeding all frames makes the LLM repeat. - **Use bf16/fp32, avoid fp16 for the audio path** — the adaptor output has large magnitude (std ≈ 28, |max| ≈ 1187); fp16 can overflow. The GGUFs here are f32/f16 weights with f32 activations, which is safe. - **WAV input** currently assumes 16 kHz mono PCM16. Resample first if needed. - Q8 quantization slightly *helps* greedy stability (quant noise regularizes away from repetition loops), so Q8 is a good default. ## Implementation notes - FSMN depthwise memory is an exact f32 shift-accumulate (avoids the F16-only, upstream-flagged `ggml_conv_1d_dw`). - LayerNorm eps = 1e-5; sinusoidal position encoding depth = input feature dim (560), positions start at 1; encoder input pre-scaled by sqrt(512). - Prompt is fed as tokens via `llama_tokenize(parse_special=true)` (prefix = 18 tokens, matching the HF tokenizer), so no Python embedding table is needed. ## Files ``` funasr-cli/ integrated binary: WAV → transcription funasr-encoder/ encoder+adaptor only (ggml) — validation/debugging funasr-embd/ LLM decode from precomputed embeds — validation/debugging export_encoder_gguf.py export the audio encoder + adaptor to GGUF ``` ## Roadmap - True FSMN-VAD segmentation (replace fixed windows; closes the last ~1.3% CER). - Arbitrary WAV formats / resampling; encoder Q8 quantization; single packaged GGUF.