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.
Files changed (143) hide show
  1. funasr_mlx-0.1.0/.gitignore +14 -0
  2. funasr_mlx-0.1.0/AGENTS.md +13 -0
  3. funasr_mlx-0.1.0/LICENSE +26 -0
  4. funasr_mlx-0.1.0/OPTIMIZATION.md +154 -0
  5. funasr_mlx-0.1.0/PKG-INFO +255 -0
  6. funasr_mlx-0.1.0/PROGRESS.md +220 -0
  7. funasr_mlx-0.1.0/README.md +220 -0
  8. funasr_mlx-0.1.0/docs/port-notes/__init__.py.md +79 -0
  9. funasr_mlx-0.1.0/docs/port-notes/_layout_state.py.md +27 -0
  10. funasr_mlx-0.1.0/docs/port-notes/_strict_load.py.md +61 -0
  11. funasr_mlx-0.1.0/docs/port-notes/api.py.md +55 -0
  12. funasr_mlx-0.1.0/docs/port-notes/awq.py.md +40 -0
  13. funasr_mlx-0.1.0/docs/port-notes/calibrate.py.md +31 -0
  14. funasr_mlx-0.1.0/docs/port-notes/campplus.py.md +35 -0
  15. funasr_mlx-0.1.0/docs/port-notes/checkpoint_stream.py.md +19 -0
  16. funasr_mlx-0.1.0/docs/port-notes/checkpoints.py.md +19 -0
  17. funasr_mlx-0.1.0/docs/port-notes/cli.py.md +19 -0
  18. funasr_mlx-0.1.0/docs/port-notes/comments.md +2399 -0
  19. funasr_mlx-0.1.0/docs/port-notes/ct_punc.py.md +71 -0
  20. funasr_mlx-0.1.0/docs/port-notes/download.py.md +58 -0
  21. funasr_mlx-0.1.0/docs/port-notes/emotion2vec.py.md +67 -0
  22. funasr_mlx-0.1.0/docs/port-notes/frontend.py.md +19 -0
  23. funasr_mlx-0.1.0/docs/port-notes/fsmn_vad.py.md +73 -0
  24. funasr_mlx-0.1.0/docs/port-notes/fun_asr_nano.py.md +27 -0
  25. funasr_mlx-0.1.0/docs/port-notes/functions.md +8731 -0
  26. funasr_mlx-0.1.0/docs/port-notes/glm_asr.py.md +49 -0
  27. funasr_mlx-0.1.0/docs/port-notes/glm_asr_thinker.py.md +55 -0
  28. funasr_mlx-0.1.0/docs/port-notes/gptq.py.md +78 -0
  29. funasr_mlx-0.1.0/docs/port-notes/kv_cache.py.md +37 -0
  30. funasr_mlx-0.1.0/docs/port-notes/llama.py.md +46 -0
  31. funasr_mlx-0.1.0/docs/port-notes/llm_asr.py.md +52 -0
  32. funasr_mlx-0.1.0/docs/port-notes/memory.py.md +34 -0
  33. funasr_mlx-0.1.0/docs/port-notes/moss_transcribe.py.md +49 -0
  34. funasr_mlx-0.1.0/docs/port-notes/paraformer.py.md +43 -0
  35. funasr_mlx-0.1.0/docs/port-notes/paraformer_streaming.py.md +36 -0
  36. funasr_mlx-0.1.0/docs/port-notes/pipeline.py.md +19 -0
  37. funasr_mlx-0.1.0/docs/port-notes/pseudo_streaming.py.md +27 -0
  38. funasr_mlx-0.1.0/docs/port-notes/quant.py.md +35 -0
  39. funasr_mlx-0.1.0/docs/port-notes/qwen3.py.md +58 -0
  40. funasr_mlx-0.1.0/docs/port-notes/qwen3_asr.py.md +64 -0
  41. funasr_mlx-0.1.0/docs/port-notes/qwen3_asr_thinker.py.md +64 -0
  42. funasr_mlx-0.1.0/docs/port-notes/rope.py.md +52 -0
  43. funasr_mlx-0.1.0/docs/port-notes/runtime.py.md +19 -0
  44. funasr_mlx-0.1.0/docs/port-notes/sanm.py.md +83 -0
  45. funasr_mlx-0.1.0/docs/port-notes/sensevoice.py.md +43 -0
  46. funasr_mlx-0.1.0/docs/port-notes/server.py.md +43 -0
  47. funasr_mlx-0.1.0/docs/port-notes/splice.py.md +64 -0
  48. funasr_mlx-0.1.0/docs/port-notes/whisper.py.md +64 -0
  49. funasr_mlx-0.1.0/pyproject.toml +43 -0
  50. funasr_mlx-0.1.0/src/funasr_mlx/__init__.py +3 -0
  51. funasr_mlx-0.1.0/src/funasr_mlx/api.py +92 -0
  52. funasr_mlx-0.1.0/src/funasr_mlx/checkpoint_stream.py +138 -0
  53. funasr_mlx-0.1.0/src/funasr_mlx/checkpoints.py +857 -0
  54. funasr_mlx-0.1.0/src/funasr_mlx/cli.py +275 -0
  55. funasr_mlx-0.1.0/src/funasr_mlx/download.py +147 -0
  56. funasr_mlx-0.1.0/src/funasr_mlx/frontend.py +250 -0
  57. funasr_mlx-0.1.0/src/funasr_mlx/llm_asr.py +306 -0
  58. funasr_mlx-0.1.0/src/funasr_mlx/memory.py +323 -0
  59. funasr_mlx-0.1.0/src/funasr_mlx/models/__init__.py +1 -0
  60. funasr_mlx-0.1.0/src/funasr_mlx/models/_layout_state.py +45 -0
  61. funasr_mlx-0.1.0/src/funasr_mlx/models/_strict_load.py +147 -0
  62. funasr_mlx-0.1.0/src/funasr_mlx/models/campplus.py +941 -0
  63. funasr_mlx-0.1.0/src/funasr_mlx/models/ct_punc.py +796 -0
  64. funasr_mlx-0.1.0/src/funasr_mlx/models/emotion2vec.py +947 -0
  65. funasr_mlx-0.1.0/src/funasr_mlx/models/fsmn_vad.py +1177 -0
  66. funasr_mlx-0.1.0/src/funasr_mlx/models/fun_asr_nano.py +211 -0
  67. funasr_mlx-0.1.0/src/funasr_mlx/models/glm_asr.py +143 -0
  68. funasr_mlx-0.1.0/src/funasr_mlx/models/glm_asr_thinker.py +80 -0
  69. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/__init__.py +14 -0
  70. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/awq.py +377 -0
  71. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/calibrate.py +179 -0
  72. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/gptq.py +701 -0
  73. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/kv_cache.py +57 -0
  74. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/llama.py +136 -0
  75. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/quant.py +297 -0
  76. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/qwen3.py +168 -0
  77. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/rope.py +35 -0
  78. funasr_mlx-0.1.0/src/funasr_mlx/models/llm/splice.py +38 -0
  79. funasr_mlx-0.1.0/src/funasr_mlx/models/moss_transcribe.py +160 -0
  80. funasr_mlx-0.1.0/src/funasr_mlx/models/paraformer.py +898 -0
  81. funasr_mlx-0.1.0/src/funasr_mlx/models/paraformer_streaming.py +406 -0
  82. funasr_mlx-0.1.0/src/funasr_mlx/models/qwen3_asr.py +186 -0
  83. funasr_mlx-0.1.0/src/funasr_mlx/models/qwen3_asr_thinker.py +85 -0
  84. funasr_mlx-0.1.0/src/funasr_mlx/models/sanm.py +789 -0
  85. funasr_mlx-0.1.0/src/funasr_mlx/models/sensevoice.py +451 -0
  86. funasr_mlx-0.1.0/src/funasr_mlx/models/whisper.py +980 -0
  87. funasr_mlx-0.1.0/src/funasr_mlx/pipeline.py +196 -0
  88. funasr_mlx-0.1.0/src/funasr_mlx/pseudo_streaming.py +103 -0
  89. funasr_mlx-0.1.0/src/funasr_mlx/runtime.py +82 -0
  90. funasr_mlx-0.1.0/src/funasr_mlx/server.py +97 -0
  91. funasr_mlx-0.1.0/tests/test_alignment_asr_static.py +775 -0
  92. funasr_mlx-0.1.0/tests/test_alignment_assets.py +77 -0
  93. funasr_mlx-0.1.0/tests/test_alignment_comparison.py +89 -0
  94. funasr_mlx-0.1.0/tests/test_alignment_data.py +141 -0
  95. funasr_mlx-0.1.0/tests/test_alignment_inputs.py +44 -0
  96. funasr_mlx-0.1.0/tests/test_alignment_utilities_static.py +481 -0
  97. funasr_mlx-0.1.0/tests/test_alignment_whisper_static.py +84 -0
  98. funasr_mlx-0.1.0/tests/test_asr.py +1514 -0
  99. funasr_mlx-0.1.0/tests/test_awq.py +220 -0
  100. funasr_mlx-0.1.0/tests/test_calib_manifest.py +407 -0
  101. funasr_mlx-0.1.0/tests/test_calibrate.py +270 -0
  102. funasr_mlx-0.1.0/tests/test_campplus.py +350 -0
  103. funasr_mlx-0.1.0/tests/test_checkpoint_stream.py +29 -0
  104. funasr_mlx-0.1.0/tests/test_checkpoints.py +63 -0
  105. funasr_mlx-0.1.0/tests/test_cli.py +120 -0
  106. funasr_mlx-0.1.0/tests/test_ct_punc.py +666 -0
  107. funasr_mlx-0.1.0/tests/test_emotion2vec.py +741 -0
  108. funasr_mlx-0.1.0/tests/test_emotion_reference.py +109 -0
  109. funasr_mlx-0.1.0/tests/test_frontend.py +134 -0
  110. funasr_mlx-0.1.0/tests/test_fsmn_vad.py +651 -0
  111. funasr_mlx-0.1.0/tests/test_glm_asr_tower.py +111 -0
  112. funasr_mlx-0.1.0/tests/test_gptq.py +657 -0
  113. funasr_mlx-0.1.0/tests/test_gptq_acceptance.py +342 -0
  114. funasr_mlx-0.1.0/tests/test_memory.py +91 -0
  115. funasr_mlx-0.1.0/tests/test_model_reference.py +167 -0
  116. funasr_mlx-0.1.0/tests/test_moss_transcribe.py +133 -0
  117. funasr_mlx-0.1.0/tests/test_pipeline.py +302 -0
  118. funasr_mlx-0.1.0/tests/test_prepare_large_alignment.py +136 -0
  119. funasr_mlx-0.1.0/tests/test_quantization.py +945 -0
  120. funasr_mlx-0.1.0/tests/test_reference_parity.py +97 -0
  121. funasr_mlx-0.1.0/tests/test_runtime.py +179 -0
  122. funasr_mlx-0.1.0/tests/test_small_alignment_launcher.py +29 -0
  123. funasr_mlx-0.1.0/tests/test_whisper.py +581 -0
  124. funasr_mlx-0.1.0/tools/alignment_asr.py +2068 -0
  125. funasr_mlx-0.1.0/tools/alignment_utilities.py +1197 -0
  126. funasr_mlx-0.1.0/tools/alignment_whisper.py +211 -0
  127. funasr_mlx-0.1.0/tools/awq_calibrate.py +215 -0
  128. funasr_mlx-0.1.0/tools/benchmark_speed.py +236 -0
  129. funasr_mlx-0.1.0/tools/build_alignment_inputs.py +85 -0
  130. funasr_mlx-0.1.0/tools/build_calib_manifest.py +386 -0
  131. funasr_mlx-0.1.0/tools/compare_alignment_arrays.py +203 -0
  132. funasr_mlx-0.1.0/tools/convert_llm_checkpoints.py +238 -0
  133. funasr_mlx-0.1.0/tools/gptq_acceptance.py +538 -0
  134. funasr_mlx-0.1.0/tools/gptq_calibrate.py +683 -0
  135. funasr_mlx-0.1.0/tools/gptq_decode_bench.py +432 -0
  136. funasr_mlx-0.1.0/tools/inspect_alignment_weights.py +67 -0
  137. funasr_mlx-0.1.0/tools/prepare_alignment_assets.py +349 -0
  138. funasr_mlx-0.1.0/tools/prepare_alignment_data.py +371 -0
  139. funasr_mlx-0.1.0/tools/prepare_large_alignment.py +87 -0
  140. funasr_mlx-0.1.0/tools/release_pack.py +195 -0
  141. funasr_mlx-0.1.0/tools/run_small_alignment.py +119 -0
  142. funasr_mlx-0.1.0/tools/setup_small_alignment.py +55 -0
  143. funasr_mlx-0.1.0/tools/zh_extended_manifest.py +58 -0
@@ -0,0 +1,14 @@
1
+ .venv/
2
+ .tmp/
3
+ .pi/
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ *.egg-info/
9
+ build/
10
+ dist/
11
+ checkpoints/
12
+ *.safetensors
13
+ *.npz
14
+ .DS_Store
@@ -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.
@@ -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.