funasr-mlx 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- funasr_mlx-0.1.0/.gitignore +14 -0
- funasr_mlx-0.1.0/AGENTS.md +13 -0
- funasr_mlx-0.1.0/LICENSE +26 -0
- funasr_mlx-0.1.0/OPTIMIZATION.md +154 -0
- funasr_mlx-0.1.0/PKG-INFO +255 -0
- funasr_mlx-0.1.0/PROGRESS.md +220 -0
- funasr_mlx-0.1.0/README.md +220 -0
- funasr_mlx-0.1.0/docs/port-notes/__init__.py.md +79 -0
- funasr_mlx-0.1.0/docs/port-notes/_layout_state.py.md +27 -0
- funasr_mlx-0.1.0/docs/port-notes/_strict_load.py.md +61 -0
- funasr_mlx-0.1.0/docs/port-notes/api.py.md +55 -0
- funasr_mlx-0.1.0/docs/port-notes/awq.py.md +40 -0
- funasr_mlx-0.1.0/docs/port-notes/calibrate.py.md +31 -0
- funasr_mlx-0.1.0/docs/port-notes/campplus.py.md +35 -0
- funasr_mlx-0.1.0/docs/port-notes/checkpoint_stream.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/checkpoints.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/cli.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/comments.md +2399 -0
- funasr_mlx-0.1.0/docs/port-notes/ct_punc.py.md +71 -0
- funasr_mlx-0.1.0/docs/port-notes/download.py.md +58 -0
- funasr_mlx-0.1.0/docs/port-notes/emotion2vec.py.md +67 -0
- funasr_mlx-0.1.0/docs/port-notes/frontend.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/fsmn_vad.py.md +73 -0
- funasr_mlx-0.1.0/docs/port-notes/fun_asr_nano.py.md +27 -0
- funasr_mlx-0.1.0/docs/port-notes/functions.md +8731 -0
- funasr_mlx-0.1.0/docs/port-notes/glm_asr.py.md +49 -0
- funasr_mlx-0.1.0/docs/port-notes/glm_asr_thinker.py.md +55 -0
- funasr_mlx-0.1.0/docs/port-notes/gptq.py.md +78 -0
- funasr_mlx-0.1.0/docs/port-notes/kv_cache.py.md +37 -0
- funasr_mlx-0.1.0/docs/port-notes/llama.py.md +46 -0
- funasr_mlx-0.1.0/docs/port-notes/llm_asr.py.md +52 -0
- funasr_mlx-0.1.0/docs/port-notes/memory.py.md +34 -0
- funasr_mlx-0.1.0/docs/port-notes/moss_transcribe.py.md +49 -0
- funasr_mlx-0.1.0/docs/port-notes/paraformer.py.md +43 -0
- funasr_mlx-0.1.0/docs/port-notes/paraformer_streaming.py.md +36 -0
- funasr_mlx-0.1.0/docs/port-notes/pipeline.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/pseudo_streaming.py.md +27 -0
- funasr_mlx-0.1.0/docs/port-notes/quant.py.md +35 -0
- funasr_mlx-0.1.0/docs/port-notes/qwen3.py.md +58 -0
- funasr_mlx-0.1.0/docs/port-notes/qwen3_asr.py.md +64 -0
- funasr_mlx-0.1.0/docs/port-notes/qwen3_asr_thinker.py.md +64 -0
- funasr_mlx-0.1.0/docs/port-notes/rope.py.md +52 -0
- funasr_mlx-0.1.0/docs/port-notes/runtime.py.md +19 -0
- funasr_mlx-0.1.0/docs/port-notes/sanm.py.md +83 -0
- funasr_mlx-0.1.0/docs/port-notes/sensevoice.py.md +43 -0
- funasr_mlx-0.1.0/docs/port-notes/server.py.md +43 -0
- funasr_mlx-0.1.0/docs/port-notes/splice.py.md +64 -0
- funasr_mlx-0.1.0/docs/port-notes/whisper.py.md +64 -0
- funasr_mlx-0.1.0/pyproject.toml +43 -0
- funasr_mlx-0.1.0/src/funasr_mlx/__init__.py +3 -0
- funasr_mlx-0.1.0/src/funasr_mlx/api.py +92 -0
- funasr_mlx-0.1.0/src/funasr_mlx/checkpoint_stream.py +138 -0
- funasr_mlx-0.1.0/src/funasr_mlx/checkpoints.py +857 -0
- funasr_mlx-0.1.0/src/funasr_mlx/cli.py +275 -0
- funasr_mlx-0.1.0/src/funasr_mlx/download.py +147 -0
- funasr_mlx-0.1.0/src/funasr_mlx/frontend.py +250 -0
- funasr_mlx-0.1.0/src/funasr_mlx/llm_asr.py +306 -0
- funasr_mlx-0.1.0/src/funasr_mlx/memory.py +323 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/__init__.py +1 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/_layout_state.py +45 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/_strict_load.py +147 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/campplus.py +941 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/ct_punc.py +796 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/emotion2vec.py +947 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/fsmn_vad.py +1177 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/fun_asr_nano.py +211 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/glm_asr.py +143 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/glm_asr_thinker.py +80 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/__init__.py +14 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/awq.py +377 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/calibrate.py +179 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/gptq.py +701 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/kv_cache.py +57 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/llama.py +136 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/quant.py +297 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/qwen3.py +168 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/rope.py +35 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/llm/splice.py +38 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/moss_transcribe.py +160 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/paraformer.py +898 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/paraformer_streaming.py +406 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/qwen3_asr.py +186 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/qwen3_asr_thinker.py +85 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/sanm.py +789 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/sensevoice.py +451 -0
- funasr_mlx-0.1.0/src/funasr_mlx/models/whisper.py +980 -0
- funasr_mlx-0.1.0/src/funasr_mlx/pipeline.py +196 -0
- funasr_mlx-0.1.0/src/funasr_mlx/pseudo_streaming.py +103 -0
- funasr_mlx-0.1.0/src/funasr_mlx/runtime.py +82 -0
- funasr_mlx-0.1.0/src/funasr_mlx/server.py +97 -0
- funasr_mlx-0.1.0/tests/test_alignment_asr_static.py +775 -0
- funasr_mlx-0.1.0/tests/test_alignment_assets.py +77 -0
- funasr_mlx-0.1.0/tests/test_alignment_comparison.py +89 -0
- funasr_mlx-0.1.0/tests/test_alignment_data.py +141 -0
- funasr_mlx-0.1.0/tests/test_alignment_inputs.py +44 -0
- funasr_mlx-0.1.0/tests/test_alignment_utilities_static.py +481 -0
- funasr_mlx-0.1.0/tests/test_alignment_whisper_static.py +84 -0
- funasr_mlx-0.1.0/tests/test_asr.py +1514 -0
- funasr_mlx-0.1.0/tests/test_awq.py +220 -0
- funasr_mlx-0.1.0/tests/test_calib_manifest.py +407 -0
- funasr_mlx-0.1.0/tests/test_calibrate.py +270 -0
- funasr_mlx-0.1.0/tests/test_campplus.py +350 -0
- funasr_mlx-0.1.0/tests/test_checkpoint_stream.py +29 -0
- funasr_mlx-0.1.0/tests/test_checkpoints.py +63 -0
- funasr_mlx-0.1.0/tests/test_cli.py +120 -0
- funasr_mlx-0.1.0/tests/test_ct_punc.py +666 -0
- funasr_mlx-0.1.0/tests/test_emotion2vec.py +741 -0
- funasr_mlx-0.1.0/tests/test_emotion_reference.py +109 -0
- funasr_mlx-0.1.0/tests/test_frontend.py +134 -0
- funasr_mlx-0.1.0/tests/test_fsmn_vad.py +651 -0
- funasr_mlx-0.1.0/tests/test_glm_asr_tower.py +111 -0
- funasr_mlx-0.1.0/tests/test_gptq.py +657 -0
- funasr_mlx-0.1.0/tests/test_gptq_acceptance.py +342 -0
- funasr_mlx-0.1.0/tests/test_memory.py +91 -0
- funasr_mlx-0.1.0/tests/test_model_reference.py +167 -0
- funasr_mlx-0.1.0/tests/test_moss_transcribe.py +133 -0
- funasr_mlx-0.1.0/tests/test_pipeline.py +302 -0
- funasr_mlx-0.1.0/tests/test_prepare_large_alignment.py +136 -0
- funasr_mlx-0.1.0/tests/test_quantization.py +945 -0
- funasr_mlx-0.1.0/tests/test_reference_parity.py +97 -0
- funasr_mlx-0.1.0/tests/test_runtime.py +179 -0
- funasr_mlx-0.1.0/tests/test_small_alignment_launcher.py +29 -0
- funasr_mlx-0.1.0/tests/test_whisper.py +581 -0
- funasr_mlx-0.1.0/tools/alignment_asr.py +2068 -0
- funasr_mlx-0.1.0/tools/alignment_utilities.py +1197 -0
- funasr_mlx-0.1.0/tools/alignment_whisper.py +211 -0
- funasr_mlx-0.1.0/tools/awq_calibrate.py +215 -0
- funasr_mlx-0.1.0/tools/benchmark_speed.py +236 -0
- funasr_mlx-0.1.0/tools/build_alignment_inputs.py +85 -0
- funasr_mlx-0.1.0/tools/build_calib_manifest.py +386 -0
- funasr_mlx-0.1.0/tools/compare_alignment_arrays.py +203 -0
- funasr_mlx-0.1.0/tools/convert_llm_checkpoints.py +238 -0
- funasr_mlx-0.1.0/tools/gptq_acceptance.py +538 -0
- funasr_mlx-0.1.0/tools/gptq_calibrate.py +683 -0
- funasr_mlx-0.1.0/tools/gptq_decode_bench.py +432 -0
- funasr_mlx-0.1.0/tools/inspect_alignment_weights.py +67 -0
- funasr_mlx-0.1.0/tools/prepare_alignment_assets.py +349 -0
- funasr_mlx-0.1.0/tools/prepare_alignment_data.py +371 -0
- funasr_mlx-0.1.0/tools/prepare_large_alignment.py +87 -0
- funasr_mlx-0.1.0/tools/release_pack.py +195 -0
- funasr_mlx-0.1.0/tools/run_small_alignment.py +119 -0
- funasr_mlx-0.1.0/tools/setup_small_alignment.py +55 -0
- funasr_mlx-0.1.0/tools/zh_extended_manifest.py +58 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Development Rules
|
|
2
|
+
|
|
3
|
+
Reference repository: `/Users/baicai1145/repos/funasr` (read-only).
|
|
4
|
+
|
|
5
|
+
- Port inference semantics from the exact FunASR source, including masking, layout, padding, epsilon, and checkpoint key names. No PyTorch inference dependency.
|
|
6
|
+
- Absolute memory ceiling is 12,000,000,000 bytes. Leave headroom: use small synthetic dimensions only. Never load/download full weights or run MLX/PyTorch numerical tests without lead approval. All expensive verification is serialized by the lead through the memory guard.
|
|
7
|
+
- Agents implement and use syntax/static checks only unless explicitly authorized. Maximum two concurrent implementation agents. Do not launch other agents.
|
|
8
|
+
- Runtime uses `mlx.core` / `mlx.nn`; keep MLX imports lazy at the package root and CLI. Float32 reductions where needed, float16 optional weights, bounded input lengths. No global model cache.
|
|
9
|
+
- Preserve checkpoint-compatible parameter names and provide explicit tensor-layout conversion rules. Loading must be strict; never silently skip mismatched keys.
|
|
10
|
+
- Tests use tiny configurations and bounded inputs, never network access. Report what has actually been verified; architecture tests are not real-checkpoint acceptance.
|
|
11
|
+
- Temporary artifacts belong in `.tmp/`. Do not write to `/tmp`.
|
|
12
|
+
- Scope edits to assigned files, commit changes, and report commit SHA and remaining gaps.
|
|
13
|
+
- Use `read`/`grep`/`find` for retrieval and `edit`/`write` for manual changes.
|
funasr_mlx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 funasr-mlx contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
The Software contains derivative works vendored from mlx-lm
|
|
16
|
+
(https://github.com/ml-explore/mlx-lm), Copyright (c) 2023-2024 Apple Inc.,
|
|
17
|
+
licensed under the Apache License, Version 2.0; see the module docstrings in
|
|
18
|
+
src/funasr_mlx/models/llm/ for scope.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Optimization Backlog — 17 Items by Distance-to-Roofline
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-10-04 · **Host:** MacBook Air M4 (16 GB) · **Batch size:** 1 everywhere
|
|
4
|
+
|
|
5
|
+
> **Post-mortem addendum (2026-10-04, whisper sprint).** Two items below were
|
|
6
|
+
> closed as MEASURED-AND-REJECTED, and the lead's original attribution for both
|
|
7
|
+
> was wrong twice. Recorded so the same traps are not re-run:
|
|
8
|
+
>
|
|
9
|
+
> 1. **Encoder elementwise fusion (#13 tiny / large enc rows)**: NOT real. The
|
|
10
|
+
> 1.65x came from (a) a timing-protocol artifact — `for KK: r=fn()` + one
|
|
11
|
+
> `mx.eval(r)` evaluates only the last lazy graph (wall time FLAT in KK while
|
|
12
|
+
> "per-iter" falls 1/KK; real slice cost ~3.0-3.4 ms, not 210 us), and (b) a
|
|
13
|
+
> hand-rolled 3-kernel GELU, while the model uses fused `mxnn.GELU()`.
|
|
14
|
+
> Honest paired A/B: tiny -1.3% p=0.29, large-v3 -0.06% p=0.90. Encoder is
|
|
15
|
+
> already at 73% of machine fp16 peak and FASTER than the sum of its parts.
|
|
16
|
+
> The 13-15 ms target was below the hardware floor (36.7 GFLOP / 2.44 TFLOPS
|
|
17
|
+
> = 15.0 ms). Evidence: reports/whisper-encoder-fusion.md.
|
|
18
|
+
> 2. **Decode KV-append replacement (whisper decode rows)**: NOT worth it. The
|
|
19
|
+
> "52% of layer" claim was a sum-of-parts fallacy (isolated concat 0.17-0.30
|
|
20
|
+
> ms summed into block attribution; inside a real block the append is ~0.1 ms
|
|
21
|
+
> of ~1.1 ms). A numerically exact prealloc/slice-assign buffer was built and
|
|
22
|
+
> reverted: 300-pair A/B -0.6%, CI95 [-0.158,+0.087], p=0.603. Ceiling for ANY
|
|
23
|
+
> append mechanism: 0.116/0.079 ms (10%/7% of block); slice-assign captured
|
|
24
|
+
> negative. Remaining decode cost is per-op dispatch count + the lm_head
|
|
25
|
+
> bandwidth floor — no cache representation removes it.
|
|
26
|
+
> Evidence: reports/task-3-*.md.
|
|
27
|
+
>
|
|
28
|
+
> **Methodology rules that fell out** (apply to every future probe):
|
|
29
|
+
> - eval INSIDE the timed region, every iteration; never rebind-and-drop.
|
|
30
|
+
> - Probe with the model's REAL ops (`mxnn.GELU`), never hand-rolled stand-ins.
|
|
31
|
+
> - Never sum isolated op costs into a stage attribution — inter-op overlap
|
|
32
|
+
> hides/shows costs (real chain beat parts-sum in both directions here).
|
|
33
|
+
> - Paired in-process A/B is the only valid comparison; absolutes drift with
|
|
34
|
+
> thermal (observed: same probe 4.73 vs 5.97 ms across sessions).
|
|
35
|
+
> - A 2.5%/p=0.055 result that fails to replicate at n=300 is noise; demand
|
|
36
|
+
> >=5% with CI excluding zero before shipping.
|
|
37
|
+
|
|
38
|
+
## Measurement basis
|
|
39
|
+
|
|
40
|
+
- **Ceilings are measured on this machine, not spec numbers**
|
|
41
|
+
(`.tmp/probe_hardware.py`, best-of-10):
|
|
42
|
+
- fp16 GEMM **2.58 TFLOPS** @ 4096³ (2.30 @ 8192³)
|
|
43
|
+
- fp32 GEMM **2.27 TFLOPS** → fp16 has only a **1.14×** compute edge on this host
|
|
44
|
+
- DRAM bandwidth **92 GB/s**
|
|
45
|
+
- Ridge point ≈ 28 FLOP/byte: decode (intensity ~1.3) is memory-bound → judge
|
|
46
|
+
by **MBU**; towers/prefill are compute-bound → judge by **MFU**.
|
|
47
|
+
- Latency: `tools/benchmark_speed.py` + `.tmp/bench_fp16_llm.py` /
|
|
48
|
+
`.tmp/bench_fp16_small.py`, current post-SDPA code, warmup + 20-30 repeats.
|
|
49
|
+
- FLOPs/bytes are analytic (2·params·tokens + attention T² terms; weights read
|
|
50
|
+
once per call), not kernel-traced — the *gap*, not the percentage, is the signal.
|
|
51
|
+
- Full tables: `.tmp/ceiling_analysis.json`, `.tmp/ceiling_analysis_small.json`.
|
|
52
|
+
|
|
53
|
+
## The 17 items, worst gap first
|
|
54
|
+
|
|
55
|
+
| # | Model | Stage | dtype | t (ms) | MFU% | MBU% | **Gap** | Bound | RTF context (fp16) |
|
|
56
|
+
|---|-------|-------|-------|--------|------|------|---------|-------|---------------------|
|
|
57
|
+
| 1 | fsmn-vad | forward | fp16 | 3.4 | 3.2 | 0.6 | **31.2×** | launch latency | 0.0010 (968×) |
|
|
58
|
+
| 2 | whisper-tiny | decode/step | fp16 | 5.2 | 0.3 | 7.2 | **13.9×** | bandwidth | 3.0 ms/step effective |
|
|
59
|
+
| 3 | moss | adaptor | fp16 | 2.0 | 10.7 | 5.7 | **9.4×** | mixed | (tower unmeasured, see #17) |
|
|
60
|
+
| 4 | campplus | forward | fp16 | 15.0 | 11.7 | 1.1 | **8.5×** | launch latency | 0.0046 (219×) |
|
|
61
|
+
| 5 | glm-asr-nano | decode/step | fp16 | 244.4 | 0.5 | 14.2 | **7.1×** | bandwidth | RTF 1.53 — not realtime |
|
|
62
|
+
| 6 | glm-asr-nano | prefill | fp16 | 306.0 | 15.0 | 11.4 | **6.7×** | compute | ↑ |
|
|
63
|
+
| 7 | qwen3-asr-0.6b | decode/step | fp16 | 98.9 | 0.6 | 16.5 | **6.1×** | bandwidth | RTF 0.605 (1.7×) |
|
|
64
|
+
| 8 | whisper-tiny | decode/step | fp32 | 4.0 | 0.4 | 18.7 | **5.3×** | bandwidth | fp32 faster than fp16 |
|
|
65
|
+
| 9 | emotion2vec | forward | fp16 | 60.3 | 23.7 | 6.0 | **4.2×** | compute | — |
|
|
66
|
+
| 10 | qwen3-asr-0.6b | prefill | fp16 | 127.0 | 30.5 | 13.0 | **3.3×** | compute | ↑ #7 |
|
|
67
|
+
| 11 | glm-asr-nano | tower | fp16 | 243.0 | 41.8 | 5.7 | **2.4×** | compute | ↑ #5 |
|
|
68
|
+
| 12 | moss | prefill | fp16 | 101.0 | 42.4 | 13.1 | **2.4×** | compute | RTF 0.132 (7.6×) |
|
|
69
|
+
| 13 | sensevoice | enc+ctc | fp16 | 42.5 | 35.4 | 12.5 | **2.8×** | compute | fp32 is 28.8 ms |
|
|
70
|
+
| 14 | paraformer | enc+dec | fp16 | 43.2 | 37.8 | 11.7 | **2.6×** | compute | fp32 is 27.8 ms |
|
|
71
|
+
| 15 | emotion2vec | forward | fp32 | 47.1 | 30.4 | 7.7 | **3.3×** | compute | fp32 faster |
|
|
72
|
+
| 16 | qwen3-asr-0.6b | tower | fp16 | 41.0 | 77.1 | 9.9 | **1.3×** | compute | near ceiling ✅ |
|
|
73
|
+
| 17 | fun-asr-nano | decode+prefill | fp16 | 16.7/31.0 | 3.5 | **98.0** | **1.0×** | bandwidth | AT the roofline ✅ |
|
|
74
|
+
|
|
75
|
+
*(whisper-tiny encoder 1.6×, sensevoice/paraformer fp32 1.7-1.9×, ct-punc 1.5×
|
|
76
|
+
are already within 2× of their roofs and are not listed as work items.)*
|
|
77
|
+
|
|
78
|
+
## Grouped by fix, highest leverage first
|
|
79
|
+
|
|
80
|
+
### A. LLM decode path — fixes #5, #7 (and lifts #2's relatives)
|
|
81
|
+
**The single biggest win.** fun-asr-nano's decode runs at **98% of memory
|
|
82
|
+
bandwidth** (1.0×, item #17) on a **Qwen3-0.6B backbone with the identical
|
|
83
|
+
751.6 M params and identical 1.50 GB/step weight traffic as qwen3-asr**, yet
|
|
84
|
+
qwen3-asr takes 98.9 ms/step vs nano's 16.7 ms — a **6× gap that is pure call
|
|
85
|
+
path, not model size**.
|
|
86
|
+
- Suspect 1: `KVCache.update_and_fetch` does `self.keys[:, :, offset:n] = k`
|
|
87
|
+
slice-assign per step; if not in-place this rewrites the whole buffer
|
|
88
|
+
(28 layers × K/V × every step).
|
|
89
|
+
- Suspect 2: `.item()` / `mx.argmax(...).item()` forces a sync every token.
|
|
90
|
+
- Payoff if brought to nano's efficiency: qwen3-asr RTF 0.605 → ~0.35;
|
|
91
|
+
glm-asr-nano decode 244 → ~40 ms/step, RTF 1.53 → ~0.85 (**crosses into
|
|
92
|
+
realtime**). Verify with `mx.metal.set_wire_per_frame`-style profiling or by
|
|
93
|
+
timing a bare `model(x, cache=...)` loop.
|
|
94
|
+
|
|
95
|
+
### B. Drop fp16 for mixed-dtype models — fixes #2, #8, #13, #14, #15
|
|
96
|
+
On this host fp16 GEMM is only 1.14× fp32, so fp16 **cannot** pay for its
|
|
97
|
+
mixed-dtype promotion. Measured regressions vs native precision:
|
|
98
|
+
sensevoice −48%, paraformer −55%, emotion2vec −28%, whisper-tiny e2e −43%,
|
|
99
|
+
glm decode −18%, qwen3 decode −10%, moss −8%.
|
|
100
|
+
**Action:** default these families to fp32 (or bf16) weights; keep fp16 only
|
|
101
|
+
where bandwidth-bound (ct-punc: **+28% faster**, the sole winner, because its
|
|
102
|
+
971 MB embedding read halves).
|
|
103
|
+
Also note SDPA fusion is a **net negative under fp16** here: sensevoice −13%,
|
|
104
|
+
paraformer −24%, emotion2vec −11% vs the pre-SDPA fp16 numbers, while it helped
|
|
105
|
+
fp32 (−8%/−2%). Gate SDPA on dtype.
|
|
106
|
+
|
|
107
|
+
### C. Launch-latency-bound small models — #1, #3, #4
|
|
108
|
+
fsmn-vad (3.2% MFU, 31×) and campplus (11.7%, 8.5×) are far from any roof, but
|
|
109
|
+
their absolute cost (3.4 / 15 ms) is already 219-968× realtime — **the gap is
|
|
110
|
+
real yet the payoff is nil unless used in a tight loop** (streaming VAD at
|
|
111
|
+
100 fps × chunks). If ever needed: fuse the 24-70 sequential small GEMMs
|
|
112
|
+
(`mx.compile` measured +0.8% = noise; needs graph-level batching instead).
|
|
113
|
+
fsmn-vad's whole forward is smaller than one Metal dispatch batch.
|
|
114
|
+
|
|
115
|
+
### D. Remaining compute-bound stages — #6, #9, #10, #11, #12
|
|
116
|
+
All transformer forwards at 15-42% MFU. Causes are the documented M4 envelope
|
|
117
|
+
(sequential 560-1024-dim GEMMs below the compute-bound threshold) plus, for
|
|
118
|
+
GLM tower (#11), attention over T=201 with d=1280. Expected recoverable headroom
|
|
119
|
+
is ~2× at best; these are genuine hardware limits, not bugs — deprioritize
|
|
120
|
+
behind A and B.
|
|
121
|
+
|
|
122
|
+
### E. Measurement debt — #3 (moss tower), caveat on #5/#7
|
|
123
|
+
- MOSS's whisper encoder ran **outside the timed region** on a padded 30 s
|
|
124
|
+
window; only the 2 ms adaptor was timed. Its published RTF 0.132 is therefore
|
|
125
|
+
**optimistic**. Re-bench with the encoder inside.
|
|
126
|
+
- GLM prefill/decode were measured with `lowest_available` down to 1.76 GB
|
|
127
|
+
(below the 3 GB reserve; policy is advisory there) — numbers are valid but
|
|
128
|
+
were collected under memory pressure.
|
|
129
|
+
- ct-punc fp32 needs `process_limit ≥ 4 GB` (971 MB largest tensor vs
|
|
130
|
+
`process_limit/4` admission); first-ever fp32 run of that family.
|
|
131
|
+
|
|
132
|
+
## Non-items (already at/near the roof)
|
|
133
|
+
|
|
134
|
+
fun-asr-nano decode **1.0×** (98% MBU), its prefill 1.2× (84% MFU), qwen3 tower
|
|
135
|
+
1.3× (77%), whisper-tiny encoder 1.6× (61%), ct-punc fp16 1.5× (69% MBU).
|
|
136
|
+
**No optimization is possible on these without different hardware.**
|
|
137
|
+
|
|
138
|
+
## Reproduction
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
# ceilings
|
|
142
|
+
PYTHONPATH=src python3 -m funasr_mlx.runtime --root . --report .tmp/ph.json -- \
|
|
143
|
+
python3 .tmp/probe_hardware.py
|
|
144
|
+
# small families (fsmn-vad/campplus/whisper-tiny)
|
|
145
|
+
PYTHONPATH=src python3 -m funasr_mlx.runtime --root . --report .tmp/b.json -- \
|
|
146
|
+
python3 .tmp/bench_fp16_small.py --dtype float16 --repeats 20 --warmup 3 \
|
|
147
|
+
--out /Volumes/2T/funasr-mlx-alignment/results/bench-small-float16.json
|
|
148
|
+
# sensevoice/paraformer/emotion2vec/ct-punc (ct-punc fp32 needs ≥4GB process_limit)
|
|
149
|
+
python3 .tmp/run_small2.py
|
|
150
|
+
# LLM families (serialized)
|
|
151
|
+
python3 .tmp/run_bench_matrix.py
|
|
152
|
+
# this table
|
|
153
|
+
python3 .tmp/ceiling_analysis.py && python3 .tmp/ceiling_analysis_small.py
|
|
154
|
+
```
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: funasr-mlx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native MLX speech inference (SenseVoice, Paraformer, Whisper, Qwen3-ASR, GLM, MoSS, Fun-ASR-Nano) with W4A16 quantization, an OpenAI-compatible server and pseudo-streaming, for Apple Silicon
|
|
5
|
+
Author: baicai1145
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: apple-silicon,asr,funasr,mlx,qwen3,sensevoice,speech-recognition,whisper
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: mlx>=0.32.0
|
|
20
|
+
Requires-Dist: numpy>=1.26
|
|
21
|
+
Requires-Dist: psutil>=5.9
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: safetensors>=0.4
|
|
24
|
+
Requires-Dist: soundfile>=0.12
|
|
25
|
+
Provides-Extra: convert
|
|
26
|
+
Requires-Dist: huggingface-hub>=0.24; extra == 'convert'
|
|
27
|
+
Requires-Dist: torch>=2.2; extra == 'convert'
|
|
28
|
+
Provides-Extra: reference
|
|
29
|
+
Requires-Dist: librosa>=0.10; extra == 'reference'
|
|
30
|
+
Requires-Dist: torch>=2.2; extra == 'reference'
|
|
31
|
+
Requires-Dist: torchaudio>=2.2; extra == 'reference'
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# funasr-mlx
|
|
37
|
+
|
|
38
|
+
Native [MLX](https://github.com/ml-explore/mlx) inference ports of the
|
|
39
|
+
FunASR model zoo for Apple Silicon — memory-bounded, numerically verified
|
|
40
|
+
against the official references, with no PyTorch/runtime dependency at
|
|
41
|
+
inference time.
|
|
42
|
+
|
|
43
|
+
All 14 checkpoints covering 11 architectures from the FunASR README model
|
|
44
|
+
zoo are ported. Every port is validated layer-by-layer against its
|
|
45
|
+
official implementation (fp32, shared-input parity) and end-to-end on real
|
|
46
|
+
audio before it is marked done.
|
|
47
|
+
|
|
48
|
+
## Coverage & Verification
|
|
49
|
+
|
|
50
|
+
| Model | Family | Layer parity (fp32) | Real-audio e2e |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| SenseVoiceSmall | `sensevoice` | source-exact diffs | ✅ |
|
|
53
|
+
| Paraformer (offline) | `paraformer` | rel 1e-6 (GEMM noise) | ✅ CER ~0 |
|
|
54
|
+
| Paraformer streaming | `paraformer` | 4.0e-7 tail path | ⚠️ CER 0.156* |
|
|
55
|
+
| Whisper tiny | `whisper` | canonical full-forward | ✅ |
|
|
56
|
+
| Whisper large-v3 | `whisper` | enc 1.5e-4 / dec 2.3e-5 | ✅ RTF 0.058 |
|
|
57
|
+
| Whisper large-v3-turbo | `whisper` | enc 5.3e-4 / dec 2.6e-4 | ✅ RTF 0.061 |
|
|
58
|
+
| emotion2vec+large | `emotion2vec` | fairseq-exact CNN/pos | ✅ |
|
|
59
|
+
| ct-punc | `ct-punc` | conv-tap exact | ✅ |
|
|
60
|
+
| fsmn-vad | `fsmn-vad` | conv/cache exact | ✅ |
|
|
61
|
+
| CAM++ | `campplus` | source-exact | ✅ |
|
|
62
|
+
| Fun-ASR-Nano | llm | audio 4.6e-3 / LLM 3.0e-6 | ✅ 8/8 |
|
|
63
|
+
| Qwen3-ASR-0.6B | llm | audio 1.2e-5 / dec 1.4e-4 | ✅ 6/6 |
|
|
64
|
+
| GLM-ASR-Nano-2512 | llm | tower 2.0e-5 / llama 3.3e-6 | ✅ 7/7 |
|
|
65
|
+
| MOSS-Transcribe-Diarize | llm | enc 1.3e-3 / dec 4.7e-5 | ✅ 8/8 +timestamps+speakers |
|
|
66
|
+
|
|
67
|
+
\* Streaming Paraformer: single-layer diffs are 1e-6, but the chunked
|
|
68
|
+
streaming state machine amplifies fp32 noise until CIF fire positions
|
|
69
|
+
drift (MLX has no fp64). Use streaming for partial results and the
|
|
70
|
+
offline model for final transcripts.
|
|
71
|
+
|
|
72
|
+
The four LLM-based models (Fun-ASR-Nano, Qwen3-ASR, GLM-ASR-Nano,
|
|
73
|
+
MOSS-Transcribe-Diarize) are ported and numerically verified
|
|
74
|
+
(`src/funasr_mlx/models/` + `models/llm/`), with e2e validated through
|
|
75
|
+
bf16 scripts; their pipeline-factory registration is pending. The seven
|
|
76
|
+
speech families above run through the guarded CLI today.
|
|
77
|
+
|
|
78
|
+
## Local Use
|
|
79
|
+
|
|
80
|
+
Install on an Apple Silicon Mac with Python 3.11+:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
python3 -m pip install -e .
|
|
84
|
+
funasr-mlx --help
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Inspect a local checkpoint or extract bounded features:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
funasr-mlx inspect /path/to/model.safetensors
|
|
91
|
+
funasr-mlx features recording.wav --cmvn none
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Inference requires a local converted checkpoint directory and a matching
|
|
95
|
+
native JSON or YAML configuration:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
funasr-mlx infer /path/to/checkpoint-dir --model sensevoice --audio recording.wav
|
|
99
|
+
funasr-mlx infer /path/to/checkpoint-dir --model ct-punc --text "hello world" --tokens /path/to/tokens.txt
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## W4A16 quantization
|
|
103
|
+
|
|
104
|
+
4-bit weights with 16-bit activations for the LLM-ASR decoders, using MLX
|
|
105
|
+
affine group quantization (group size 64). At batch size 1 the decode step
|
|
106
|
+
streams the weights once per step, so reading 4x fewer bytes is the largest
|
|
107
|
+
remaining lever; measured on an M4 it cuts peak inference memory 41-59% and
|
|
108
|
+
decode time up to 2.7x across the five families.
|
|
109
|
+
|
|
110
|
+
Convert a native checkpoint, then load it exactly as an unquantized one:
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
python3 tools/convert_llm_checkpoints.py --family qwen3-asr-0.6b \
|
|
114
|
+
--source /path/to/qwen3-asr-0.6b --destination /path/to/qwen3-asr-0.6b-w4a16 \
|
|
115
|
+
--native-source --quantize tower
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`--quantize` picks how much is quantized, and the choice is not cosmetic:
|
|
119
|
+
|
|
120
|
+
| scope | reaches | buys |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `decoder` | decoder Linear layers | decode time |
|
|
123
|
+
| `tower` | + the audio tower | peak memory; the tower runs once per utterance, so no decode cost |
|
|
124
|
+
| `all` | + the tied token embedding | removes the per-step LM-head read, but only some families survive it |
|
|
125
|
+
|
|
126
|
+
The token embedding is the tied LM head, so quantizing it injects 4-bit error
|
|
127
|
+
directly into every step's logits. It is safe for `fun-asr-nano` (tokens fully
|
|
128
|
+
identical) and destroys the transcript for `qwen3-asr-0.6b` and `glm-asr-nano`,
|
|
129
|
+
so `all` is opt-in per family rather than a default. `lm_head`, position tables,
|
|
130
|
+
convolutions and norms are never quantized. Loading is strict: the config
|
|
131
|
+
records `quantized_tensors` and `quantization_skipped`, and a key that should
|
|
132
|
+
have been quantized but was not is an error, not a silent fallback.
|
|
133
|
+
|
|
134
|
+
Per-family scope, checkpoint sizes, measured speedups and peak-memory drops are
|
|
135
|
+
in `.tmp/t9/RESULTS.md`; the scheme and its failure modes are in
|
|
136
|
+
`.tmp/t9/LEDGER.md`.
|
|
137
|
+
|
|
138
|
+
## Downloading published checkpoints
|
|
139
|
+
|
|
140
|
+
No hub dependencies (stdlib HTTPS only; HF and MS mirrors with per-file
|
|
141
|
+
fallback; ~5-6 MB/s ceiling is the hosts' per-IP throttle, not client):
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
funasr-mlx download qwen3-asr-0.6b-mlx --variant w4a16 --dest ./models
|
|
145
|
+
# classic models omit --variant:
|
|
146
|
+
funasr-mlx download sensevoice-mlx-fp16 --dest ./models
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Then transcribe directly from the download:
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
funasr-mlx transcribe ./models/qwen3-asr-0.6b-mlx/w4a16 --audio recording.wav
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## LLM-ASR transcription
|
|
156
|
+
|
|
157
|
+
Three interfaces share one resident-worker protocol (KV cache reset-reuse
|
|
158
|
+
between requests; RTF 0.079 on the 0.6B w4, M4 Air):
|
|
159
|
+
|
|
160
|
+
CLI (single file):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
funasr-mlx transcribe /path/to/qwen3-asr-0.6b-w4a16-awq-w8tower-g32 --audio recording.wav
|
|
164
|
+
# {"text": "...", "token_ids": [...], "family": "qwen3-asr", "wall_ms": 650.8}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Python API (routes through the guarded subprocess automatically):
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from funasr_mlx.api import transcribe, download_model
|
|
171
|
+
path = download_model("qwen3-asr-0.6b-mlx", variant="w4a16", dest="./models")
|
|
172
|
+
result = transcribe("recording.wav", model=path / "w4a16")
|
|
173
|
+
print(result["text"])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
HTTP server (OpenAI-compatible endpoint; raw body or multipart form):
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
funasr-mlx serve /path/to/checkpoint --port 8000
|
|
180
|
+
curl -X POST http://127.0.0.1:8000/v1/audio/transcriptions -F "file=@recording.wav"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Supported families: `qwen3-asr` (0.6B/1.7B), `glm-asr-nano`,
|
|
184
|
+
`moss-transcribe-diarize`, `fun-asr-nano` (family inferred from the
|
|
185
|
+
checkpoint directory name when omitted).
|
|
186
|
+
|
|
187
|
+
## Pseudo-streaming (all transcription families)
|
|
188
|
+
|
|
189
|
+
`funasr_mlx.pseudo_streaming.PseudoStream` gives every transcription model
|
|
190
|
+
a streaming-style API over a growing audio buffer: each `push()` re-runs the
|
|
191
|
+
model on `buffer[0:t]`, `close()` returns output bit-identical to offline
|
|
192
|
+
(measured: 94.6% of displayed characters survive to final; revisions are
|
|
193
|
+
confined to the rolling tail, .tmp/opt/LOG.md).
|
|
194
|
+
|
|
195
|
+
- LLM families (`qwen3-asr`, `glm-asr-nano`, `moss`, `fun-asr-nano`):
|
|
196
|
+
`TranscribeWorker.pseudo_stream()`; tower+prefill recompute per push
|
|
197
|
+
(~150-800ms per 1s chunk on the 0.6B w4).
|
|
198
|
+
- Classic families (`sensevoice`, `paraformer`): single-pass per push;
|
|
199
|
+
`classic_transcriber(model, config, family=..., tokens=..., cmvn=...)`
|
|
200
|
+
(cmvn REQUIRED for sensevoice -- without `am.mvn` the encoder collapses
|
|
201
|
+
to all blanks).
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from funasr_mlx.api import TranscribeWorker
|
|
205
|
+
worker = TranscribeWorker("models/qwen3-asr-0.6b-mlx/w4a16")
|
|
206
|
+
stream = worker.pseudo_stream(chunk_seconds=1.0)
|
|
207
|
+
for chunk in mic: # numpy float32 @16k, any sizes
|
|
208
|
+
ev = stream.push(chunk)
|
|
209
|
+
if ev: render(ev.text) # stable prefix + rolling tail
|
|
210
|
+
final = stream.close() # == offline transcription
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Memory guard (optional, dev-machine tool)
|
|
214
|
+
|
|
215
|
+
`funasr_mlx.memory.run_guarded` is a host-protection wrapper developed on a
|
|
216
|
+
16GB MacBook Air: it bounds per-process RSS and wired-memory headroom while
|
|
217
|
+
big checkpoints load. It is NOT a project requirement:
|
|
218
|
+
|
|
219
|
+
* Library callers (`funasr_mlx.api`, `pipeline.load_model`) load and run
|
|
220
|
+
in-process by default, with no guard and no policy environment.
|
|
221
|
+
* `funasr-mlx` CLI still wraps execution in the guard by default
|
|
222
|
+
(`--memory-gb`), convenient for constrained machines; set
|
|
223
|
+
`FUNASR_MLX_MEMORY_POLICY=""` handling per platform if you want to bypass.
|
|
224
|
+
* Budget checks (weights-vs-limit, shard caps) apply only when a policy is
|
|
225
|
+
present in the environment.
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
Optional `test`, `reference` and `convert` dependency groups. Source
|
|
231
|
+
differential tests use a sibling FunASR checkout; unavailable references
|
|
232
|
+
skip. Guarded verification:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
PYTHONPATH=src PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m funasr_mlx.runtime \
|
|
236
|
+
--root . --report .tmp/check.json -- \
|
|
237
|
+
python3 -m pytest -q --tb=short --basetemp=.tmp/pytest-check
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
503 tests pass in the guarded suite. Real-checkpoint accuracy, quantized
|
|
241
|
+
variants, and latency/RTF figures are documented in `PROGRESS.md`; the W4A16
|
|
242
|
+
per-family numbers are in `.tmp/t9/RESULTS.md`.
|
|
243
|
+
|
|
244
|
+
## Attribution
|
|
245
|
+
|
|
246
|
+
Qwen3/Llama decoder layouts and the KV cache are vendored (trimmed) from
|
|
247
|
+
[mlx-lm](https://github.com/ml-explore/mlx-lm) (Apache 2.0, Copyright ©
|
|
248
|
+
2023-2024 Apple Inc.); source and license notes are in the module
|
|
249
|
+
docstrings. Model architectures follow their official references
|
|
250
|
+
(FunASR, OpenAI Whisper, Qwen, GLM, OpenMOSS).
|
|
251
|
+
|
|
252
|
+
## Status
|
|
253
|
+
|
|
254
|
+
Experimental: numerically verified ports, not a production service. No
|
|
255
|
+
fastest / lowest-memory / production-quality claim is made.
|