alexwengg commited on
Commit
af99490
Β·
verified Β·
1 Parent(s): 69720d3

Upload VectorEstimatorVariants/README.md

Browse files
Files changed (1) hide show
  1. VectorEstimatorVariants/README.md +183 -0
VectorEstimatorVariants/README.md ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supertonic-3 β€” CoreML conversion
2
+
3
+ Hand-port of [Supertone Supertonic-3 v1.7.3](https://huggingface.co/Supertone/supertonic-3)
4
+ from ONNX to PyTorch to CoreML. 31 languages, 44.1 kHz, flow-matching
5
+ diffusion (8 denoising steps, classifier-free guidance baked into the
6
+ ONNX graph via batch-2 duplication).
7
+
8
+ End-to-end pipeline:
9
+
10
+ ```
11
+ text β†’ UnicodeProcessor β†’ token_ids, text_mask
12
+ β”œβ”€β”€ duration_predictor β†’ duration_sec
13
+ └── text_encoder β†’ text_emb [B, 256, T]
14
+ ↓
15
+ sample_noisy_latent(duration_sec) β†’ noisy [B, 144, L], latent_mask
16
+ ↓
17
+ for 8 steps: vector_estimator(noisy, text_emb, style, masks, step, total)
18
+ ↓
19
+ vocoder(denoised_latent) β†’ wav [B, 512*6*L]
20
+ ```
21
+
22
+ Audio chunk granularity:
23
+ - AE / vocoder frame: 512 / 44100 β‰ˆ **11.6 ms**
24
+ - TTL latent slot (model "tick"): 512 Γ— 6 / 44100 β‰ˆ **69.7 ms**
25
+
26
+ ## Layout
27
+
28
+ ```
29
+ models/tts/supertonic-3/
30
+ β”œβ”€β”€ README.md
31
+ β”œβ”€β”€ pyproject.toml # uv project (Python 3.11, torch + coremltools 8)
32
+ └── coreml/
33
+ β”œβ”€β”€ trials.md # numerical-parity bug log (4 vector_estimator gotchas)
34
+ β”œβ”€β”€ __init__.py
35
+ β”œβ”€β”€ common.py # ONNX-graph loader utilities (assign_param, etc.)
36
+ β”œβ”€β”€ text_encoder.py # PyTorch port: build_text_encoder_from_onnx
37
+ β”œβ”€β”€ duration_predictor.py
38
+ β”œβ”€β”€ vector_estimator.py
39
+ β”œβ”€β”€ vocoder.py
40
+ β”œβ”€β”€ convert_coreml.py # PyTorch trace -> .mlpackage for all 4 modules
41
+ β”œβ”€β”€ validate.py # ONNX vs PyTorch parity check
42
+ β”œβ”€β”€ verify_coreml.py # CoreML vs PyTorch parity check
43
+ β”œβ”€β”€ infer.py # end-to-end PyTorch TTS driver (text -> wav)
44
+ └── infer_coreml.py # end-to-end CoreML TTS driver (text -> wav)
45
+ ```
46
+
47
+ ## Setup
48
+
49
+ ```bash
50
+ cd models/tts/supertonic-3/
51
+ uv sync
52
+
53
+ # Fetch upstream ONNX + style + tokenizer assets
54
+ mkdir -p build/_onnx build/voice_styles
55
+ HF=https://huggingface.co/Supertone/supertonic-3/resolve/main
56
+ for f in text_encoder duration_predictor vector_estimator vocoder; do
57
+ curl -L $HF/_onnx/${f}.onnx -o build/_onnx/${f}.onnx
58
+ done
59
+ curl -L $HF/_onnx/tts.json -o build/_onnx/tts.json
60
+ curl -L $HF/_onnx/unicode_indexer.json -o build/_onnx/unicode_indexer.json
61
+ curl -L $HF/voice_styles/M1.json -o build/voice_styles/M1.json
62
+ ```
63
+
64
+ ## Convert
65
+
66
+ ```bash
67
+ # FP32 (numerical reference; ALL modules fall back to CPU on ANE)
68
+ uv run python -m coreml.convert_coreml build/_onnx --out-dir build/_mlpackage
69
+
70
+ # FP16 (required for ANE residency; 3/4 modules land on ANE β€” see Profile below)
71
+ uv run python -m coreml.convert_coreml build/_onnx --fp16 --out-dir build/_mlpackage_fp16
72
+
73
+ # Fixed-shape VectorEstimator variant for ANE profiling (RangeDim/Enum hit
74
+ # ANE shape limits β€” see trials.md "Dynamic shapes vs ANE"):
75
+ uv run python -m coreml.convert_ve_fixed \
76
+ --onnx build/_onnx/vector_estimator.onnx \
77
+ --out build/_mlpackage_fp16_fixed/VectorEstimator_L128.mlpackage \
78
+ --L 128 --T 128
79
+ ```
80
+
81
+ Produces four `.mlpackage` bundles (FP32 ~380 MB, FP16 ~190 MB; mlprogram,
82
+ iOS 18+):
83
+
84
+ | Module | FP32 | FP16 | Variable axes |
85
+ | ------------------ | ----- | ------ | ----------------------------------------- |
86
+ | vocoder | 97 MB | 48 MB | `latent.L_ttl` = RangeDim(4..512) |
87
+ | text_encoder | 35 MB | 17 MB | fixed `text.T = 128` |
88
+ | duration_predictor | 3.5 MB| 1.8 MB | fixed `text.T = 128` |
89
+ | vector_estimator | 244 MB| 122 MB | `latent.L` & `text.T` = RangeDim(17..512) |
90
+
91
+ ## Validate
92
+
93
+ ```bash
94
+ # ONNX vs PyTorch port (per module)
95
+ uv run python -m coreml.validate
96
+
97
+ # CoreML vs PyTorch port (per module)
98
+ uv run python -m coreml.verify_coreml
99
+
100
+ # End-to-end PyTorch (writes WAV)
101
+ uv run python -m coreml.infer \
102
+ --onnx-dir build/_onnx \
103
+ --voice-style build/voice_styles/M1.json \
104
+ --text "Hello world."
105
+
106
+ # End-to-end CoreML (writes WAV)
107
+ uv run python -m coreml.infer_coreml \
108
+ --mlpackage-dir build/_mlpackage \
109
+ --tts-json build/_onnx/tts.json \
110
+ --unicode-indexer build/_onnx/unicode_indexer.json \
111
+ --voice-style build/voice_styles/M1.json \
112
+ --text "Hello world."
113
+ ```
114
+
115
+ Final parity vs ONNX-Runtime CPU:
116
+
117
+ | Module | PyTorch vs ONNX max_abs | CoreML vs PyTorch max_abs |
118
+ | ------------------ | ----------------------- | ------------------------- |
119
+ | vocoder | 2.53e-4 | 1.41e-6 |
120
+ | text_encoder | 9.77e-2 (relaxed tol) | 2.33e-4 |
121
+ | duration_predictor | 3.04e-6 | 3.82e-6 |
122
+ | vector_estimator | 1.21e-3 | 2.96e-5 |
123
+
124
+ End-to-end CoreML on M-series CPU+ANE: **~0.74 s** to synthesize
125
+ 6.32 s of audio for a single English sentence (RTFx β‰ˆ 8.5x), 8
126
+ denoising steps. ASR-verified against FluidAudio Parakeet TDT.
127
+
128
+ ## Profile (FP16, Apple M2, macOS 26.5, `cpu_and_neural_engine`)
129
+
130
+ | Module | CPU% | GPU% | ANE% | Predict | Notes |
131
+ | ----------------------------------- | ---- | ---- | ---- | ------- | ----- |
132
+ | duration_predictor | 100 | 0 | 0 | 0.82 ms | tiny, CPU-bound |
133
+ | text_encoder (T=128) | 38 | 0 | 62 | 2.15 ms | partial ANE |
134
+ | vocoder (RangeDim L 4..512) | 0 | 0 | 100 | 1.17 ms | full ANE, 4Γ— vs FP32 |
135
+ | vector_estimator (RangeDim 17..512) | β€” | β€” | β€” | β€” | dynamic shapes crash on ANE β€” must bucket to fixed L |
136
+ | vector_estimator (fixed L=128 T=128)| 6 | 0 | 94 | 3.8 ms | **lands on ANE** (M5 Pro): NE 3.82 ms vs CPU-only 14.20 ms = 3.7Γ—. `ANECCompile FAILED` msg is non-fatal β€” see trials.md "M5 Pro re-evaluation" |
137
+ | vector_estimator (fixed L=256/512) | 4 | 0 | 96 | 8.4 / 16.4 ms | ANE holds across buckets; int8 halves size (64.5 MB) at same speed/parity 41.5 dB |
138
+
139
+ See `coreml/trials.md` β†’ "ANE residency profiling" for the full breakdown,
140
+ the float-mask refactor that eliminated the bool-tile blocker, the
141
+ residual opaque `ANECCompile() FAILED (11)`, and the EnumeratedShapes
142
+ runtime stride gotcha.
143
+
144
+ ## Critical gotchas
145
+
146
+ See `coreml/trials.md` for the full log. Highlights:
147
+
148
+ 1. **CFG via batch-2 duplication** β€” the ONNX vector_estimator tiles
149
+ inputs to batch=2, runs cond + uncond in parallel, then combines
150
+ with `(noisy + (1/total)*(4*cond - 3*uncond)) * mask`. The cond
151
+ style key is **not** the user `style_ttl` β€” it is a learned
152
+ initializer at `/vector_estimator/Expand_output_0`.
153
+ 2. **Rotary is length-normalized** β€” `angles = (pos / sum(mask)) * theta`,
154
+ divisor differs for Q (latent_mask) and K (text_mask).
155
+ 3. **Attention divisor is 16.0**, not `sqrt(dk)=8`. Off-by-2x in scoring.
156
+ 4. **Style attention applies `tanh(K)`** before the score matmul; text
157
+ attention does not.
158
+ 5. **Replicate-pad lower bound** β€” ConvNeXt depthwise pads scale with
159
+ dilation: `pad = (K-1)*D/2`. CoreML enforces `pad ≀ dim-1` at load
160
+ time, hence `RangeDim.lower_bound = 17` for vector_estimator and
161
+ `4` for vocoder.
162
+ 6. **int32 vs int64 tokens** β€” CoreML wants int32, PyTorch indexes int64.
163
+ Wrap modules with a tiny `_Int32Wrapper` that casts inside the
164
+ traced graph so the external input stays int32.
165
+ 7. **Python 3.14 has no BlobWriter** β€” pin `requires-python = ">=3.11,<3.13"`.
166
+ 8. **Float masking, not bool masking** β€” `masked_fill(mask==0, -inf)` and
167
+ `where(mask==0, 0, attn)` compile to `bool tile`/`select` ops that ANE
168
+ rejects. Use `scores - (1.0 - mask) * 1e4` (additive) and `attn * mask`
169
+ (multiplicative) instead. Lifts vector_estimator from 89.6% β†’ 93.0%
170
+ ANE-eligible (though the residual opaque `ANECCompile() FAILED (11)`
171
+ still blocks final ANE landing β€” see trials.md).
172
+ 9. **coremltools `_int` cast with (1,) tensor** β€” `aten::Int` on a
173
+ (1,)-shape int tensor trips `TypeError: only 0-dimensional arrays can
174
+ be converted to Python scalars` inside coremltools' `_cast` handler.
175
+ `convert_coreml.py` monkey-patches `_cast` (`_patch_int_cast`) to
176
+ squeeze (1,) β†’ scalar before forwarding.
177
+
178
+ ## Upstream + downstream
179
+
180
+ - Upstream: <https://huggingface.co/Supertone/supertonic-3>
181
+ - Reference Python driver: <https://github.com/supertone-inc/supertonic/blob/main/py/helper.py>
182
+ - Republished CoreML: `FluidInference/supertonic-3-coreml` (HuggingFace)
183
+ - FluidAudio Swift integration: `Sources/FluidAudio/TTS/Supertonic3/`