Initial release: fp16 CoreML .mlpackage MDX-Net stem separation models + model card + export tooling
Browse filesUVR MDX-Net (9482, Voc FT, Inst HQ 3) converted to CoreML mlprogram fp16 for ANE/GPU on iOS. Includes export pipeline, LICENSE, example audio.
- .gitattributes +1 -0
- LICENSE +30 -0
- README.md +114 -0
- UVR-MDX-NET-Inst_HQ_3.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- UVR-MDX-NET-Inst_HQ_3.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- UVR-MDX-NET-Inst_HQ_3.mlpackage/Manifest.json +18 -0
- UVR-MDX-NET-Voc_FT.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- UVR-MDX-NET-Voc_FT.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- UVR-MDX-NET-Voc_FT.mlpackage/Manifest.json +18 -0
- UVR_MDXNET_9482.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- UVR_MDXNET_9482.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- UVR_MDXNET_9482.mlpackage/Manifest.json +18 -0
- example.mp3 +3 -0
- export/README.md +159 -0
- export/export_mdx_coreml.py +228 -0
- export/requirements.txt +23 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
example.mp3 filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Gyoom
|
| 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 IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
The model weights are derived from the Ultimate Vocal Remover (UVR) MDX-Net
|
| 26 |
+
models by Anjok07 (UVR-MDXNET 9482) and Kimberley Jensen (UVR-MDX-NET Voc FT),
|
| 27 |
+
distributed under the MIT License via https://github.com/TRvlvr/model_repo and
|
| 28 |
+
mirrored at https://huggingface.co/Politrees/UVR_resources. This repository
|
| 29 |
+
redistributes format-converted (ONNX -> CoreML fp16) copies under the same
|
| 30 |
+
MIT terms.
|
README.md
CHANGED
|
@@ -1,3 +1,117 @@
|
|
| 1 |
---
|
| 2 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: mit
|
| 3 |
+
tags:
|
| 4 |
+
- coreml
|
| 5 |
+
- apple-silicon
|
| 6 |
+
- apple-neural-engine
|
| 7 |
+
- audio-source-separation
|
| 8 |
+
- music-source-separation
|
| 9 |
+
- mdx-net
|
| 10 |
+
- uvr
|
| 11 |
+
- vocals
|
| 12 |
+
- instrumental
|
| 13 |
+
- fp16
|
| 14 |
+
- mlprogram
|
| 15 |
+
- on-device
|
| 16 |
+
pipeline_tag: audio-to-audio
|
| 17 |
---
|
| 18 |
+
|
| 19 |
+
# UVR MDX-Net CoreML β on-device music stem separation for iOS
|
| 20 |
+
|
| 21 |
+
On-device **vocals / instrumental (music stem) separation** models, converted from the Ultimate Vocal
|
| 22 |
+
Remover (UVR) MDX-Net checkpoints to **CoreML `.mlpackage`** files (fp16, `mlprogram`) for the Apple
|
| 23 |
+
Neural Engine / GPU / CPU on iOS and macOS.
|
| 24 |
+
|
| 25 |
+
Each `.mlpackage` is the **learned core only** β the STFT/iSTFT live in your app's DSP, so the whole
|
| 26 |
+
signal pipeline stays under your control and runs entirely on-device. No cloud, no network.
|
| 27 |
+
|
| 28 |
+
## Models
|
| 29 |
+
|
| 30 |
+
| Model file | Source (UVR MDX-Net) | Stem | Size | `n_fft` | `hop` | `dim_f` |
|
| 31 |
+
| --- | --- | --- | --- | --- | --- | --- |
|
| 32 |
+
| `UVR_MDXNET_9482.mlpackage` | UVR-MDXNET 9482 | **vocals** | ~15 MB | 4096 | 1024 | 2048 |
|
| 33 |
+
| `UVR-MDX-NET-Voc_FT.mlpackage` | UVR-MDX-NET Voc FT | **vocals** | ~32 MB | 6144 | 1024 | 3072 |
|
| 34 |
+
| `UVR-MDX-NET-Inst_HQ_3.mlpackage` | UVR-MDX-NET Inst HQ 3 | **instrumental** | ~32 MB | 6144 | 1024 | 3072 |
|
| 35 |
+
|
| 36 |
+
All three share the same I/O contract: **NCHW**, static `dim_t = 256`, input `[1, 4, dim_f, 256]`
|
| 37 |
+
complex-as-channels `[L_re, L_im, R_re, R_im]` β output of the same shape = the predicted stem's
|
| 38 |
+
spectrogram. Real **fp16** I/O with fp16 compute, `mlprogram`, `minimum_deployment_target = iOS16`.
|
| 39 |
+
|
| 40 |
+
> β οΈ **Inst HQ 3 inverts the residual.** It is architecturally identical to Voc FT but predicts the
|
| 41 |
+
> **instrumental**, so the free residual is `vocals = mix β model(mix)` β the opposite polarity from
|
| 42 |
+
> 9482 / Voc FT. A caller that assumes "output = vocals" will get the stems swapped.
|
| 43 |
+
|
| 44 |
+
## Download
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
# huggingface_hub (Python)
|
| 48 |
+
pip install huggingface_hub
|
| 49 |
+
python - <<'PY'
|
| 50 |
+
from huggingface_hub import snapshot_download
|
| 51 |
+
snapshot_download("gyoom-sa/UVR-MDX-CoreML", local_dir="./UVR-MDX-CoreML")
|
| 52 |
+
PY
|
| 53 |
+
|
| 54 |
+
# or the CLI
|
| 55 |
+
hf download gyoom-sa/UVR-MDX-CoreML --local-dir ./UVR-MDX-CoreML
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
The `.mlpackage` folders keep their on-disk structure in this repo, so `git clone` / `hf download`
|
| 59 |
+
yields ready-to-compile packages. Add them to your Xcode target (or call
|
| 60 |
+
`MLModel.compileModel(at:)` at runtime) and they compile to `.mlmodelc`.
|
| 61 |
+
|
| 62 |
+
## Usage (iOS / Swift)
|
| 63 |
+
|
| 64 |
+
The model operates on a **spectrogram**, not raw audio. Build the STFT in your app (Apple's
|
| 65 |
+
Accelerate / vDSP), feed the complex planes to the model, then iSTFT the output. Per model:
|
| 66 |
+
**9482** `n_fft 4096, hop 1024, dim_f 2048`; **Voc FT / Inst HQ 3** `n_fft 6144, hop 1024, dim_f 3072`.
|
| 67 |
+
Use a periodic Hann window, `center=True` reflect padding, **unnormalized** STFT, drop the Nyquist bin
|
| 68 |
+
(`dim_f = n_fft/2`), and pack planes in order `[L_re, L_im, R_re, R_im]` with flat row-major index
|
| 69 |
+
`((plane*dim_f)+bin)*dim_t + frame`.
|
| 70 |
+
|
| 71 |
+
```swift
|
| 72 |
+
import CoreML
|
| 73 |
+
|
| 74 |
+
// Accelerator is a LOAD-TIME choice. .all == ANE β GPU β CPU (the baked default).
|
| 75 |
+
let config = MLModelConfiguration()
|
| 76 |
+
config.computeUnits = .all // .cpuAndGPU | .cpuOnly also available
|
| 77 |
+
let model = try MLModel(contentsOf: compiledURL, configuration: config)
|
| 78 |
+
|
| 79 |
+
// Per chunk. dim_f = 2048 (9482) or 3072 (Voc FT / Inst HQ 3); dim_t = 256. Real fp16 I/O.
|
| 80 |
+
let input = try MLMultiArray(shape: [1, 4, dimF as NSNumber, dimT as NSNumber],
|
| 81 |
+
dataType: .float16)
|
| 82 |
+
let p = input.dataPointer.bindMemory(to: Float16.self, capacity: input.count)
|
| 83 |
+
// pack the STFT: p[((plane*dimF)+bin)*dimT + frame] = value (plane 0..3 = L_re,L_im,R_re,R_im)
|
| 84 |
+
|
| 85 |
+
let out = try model.prediction(from: MLDictionaryFeatureProvider(dictionary: ["input": input]))
|
| 86 |
+
let stem = out.featureValue(for: "output")!.multiArrayValue! // fp16 [1,4,dim_f,256] β iSTFT
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
For the stem models, `vocals = model(mix)`; for **Inst HQ 3**, `instrumental = model(mix)` and
|
| 90 |
+
`vocals = mix β instrumental`. The equal-power "shift trick" denoise (`0.5Β·model(x) β 0.5Β·model(βx)`)
|
| 91 |
+
is optional.
|
| 92 |
+
|
| 93 |
+
## Notes & limitations
|
| 94 |
+
|
| 95 |
+
- **STFT is outside the graph.** These are the learned core only; the app must reproduce the host
|
| 96 |
+
DSP contract above (Hann window, unnormalized STFT, reflect padding, Nyquist bin dropped, plane
|
| 97 |
+
order, flat index). Getting the packing wrong silently degrades or swaps the stems.
|
| 98 |
+
- **fp16 is quality-safe for MDX** β peak activations sit far below the 65504 fp16 ceiling (measured
|
| 99 |
+
~560 for 9482 / ~1384 for Voc FT), and the pipeline normalizes the mix, so the fp16 output floor
|
| 100 |
+
(~73 dB) is well above audible noise.
|
| 101 |
+
- **Residual polarity** differs per model (see the Inst HQ 3 warning).
|
| 102 |
+
- These are **format-converted on-device artifacts**, not models loadable in `transformers` or via
|
| 103 |
+
HF Inference. `pipeline_tag: audio-to-audio` is set for task-based discoverability only.
|
| 104 |
+
|
| 105 |
+
## Reproducibility
|
| 106 |
+
|
| 107 |
+
The exact conversion pipeline (`export_mdx_coreml.py` + pinned `requirements.txt`) and the full
|
| 108 |
+
technical write-up (precision, compute units, verification SNR, iOS DSP contract) live in the
|
| 109 |
+
[`export/`](./export/README.md) folder of this repo. `example.mp3` is provided for a quick smoke test.
|
| 110 |
+
|
| 111 |
+
## License & attribution
|
| 112 |
+
|
| 113 |
+
The model weights are derived from the Ultimate Vocal Remover (UVR) MDX-Net models by **Anjok07**
|
| 114 |
+
(UVR-MDXNET 9482) and **Kimberley Jensen** (UVR-MDX-NET Voc FT / Inst HQ 3), distributed under the
|
| 115 |
+
MIT License via https://github.com/TRvlvr/model_repo and mirrored at
|
| 116 |
+
https://huggingface.co/Politrees/UVR_resources. This repository redistributes format-converted
|
| 117 |
+
(ONNX β CoreML fp16) copies under the same MIT terms. See [`LICENSE`](./LICENSE).
|
UVR-MDX-NET-Inst_HQ_3.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fe4055846ccddb4a04765feb20ec7fe6530624383323782a91d125f760f05ac4
|
| 3 |
+
size 115669
|
UVR-MDX-NET-Inst_HQ_3.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a687c627a66e8dc3dc6947e31d35526164f234757208624506ff2aaf47bd1a5
|
| 3 |
+
size 33377792
|
UVR-MDX-NET-Inst_HQ_3.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"95ff6d20-7277-4643-b3ca-76a3abbfb95a": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"c2c2bdc2-b64a-4519-94aa-8b8cd087b7c5": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "95ff6d20-7277-4643-b3ca-76a3abbfb95a"
|
| 18 |
+
}
|
UVR-MDX-NET-Voc_FT.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bdd6a5c0b2d3d9eae431f82a58e194c43f08cc3f9da664fdb4daa9d7c4b9bc43
|
| 3 |
+
size 115669
|
UVR-MDX-NET-Voc_FT.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ee8aeba2251618d8248bfc3bbad49bb5d3b3ee0b6caa47182e08f2e5e3131c22
|
| 3 |
+
size 33377792
|
UVR-MDX-NET-Voc_FT.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"690b47fa-f6b7-4811-8dc1-b7c5bcc91664": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"8d9d7018-0d95-46da-97b6-330fc9f0f65d": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "690b47fa-f6b7-4811-8dc1-b7c5bcc91664"
|
| 18 |
+
}
|
UVR_MDXNET_9482.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0512ade072df20bd137e83051cb86dd21d32361bdd90535b94c23224ba569a83
|
| 3 |
+
size 115552
|
UVR_MDXNET_9482.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c58b40c6767e15c2e31be86eca74ddb72765cc169ca698559812daed0f273a1b
|
| 3 |
+
size 14850048
|
UVR_MDXNET_9482.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"d57d5b90-d8e8-4c71-a9d2-987177ed2f60": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"fa44167b-afd1-441a-9edb-1970eee79c0e": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "fa44167b-afd1-441a-9edb-1970eee79c0e"
|
| 18 |
+
}
|
example.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1b3783ea92eafc1741e6730bc0dc22b047c56dfcb0defc8af085ec9b5e937f56
|
| 3 |
+
size 2402037
|
export/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Export pipeline for the UVR-MDX-CoreML repository
|
| 2 |
+
|
| 3 |
+
This folder is the **one-shot conversion pipeline used to produce the CoreML `.mlpackage` models
|
| 4 |
+
shipped in this repository** (`gyoom-sa/UVR-MDX-CoreML`). It is self-contained: it takes a UVR
|
| 5 |
+
MDX-Net source `.onnx` and emits a GPU/ANE-ready fp16 CoreML `.mlpackage` for iOS β nothing here
|
| 6 |
+
depends on anything outside this repo.
|
| 7 |
+
|
| 8 |
+
It reproduces the same **STFT-outside-the-graph** formula for all three models: the `.mlpackage` is
|
| 9 |
+
the learned core only; the STFT/iSTFT stay in the app's own DSP.
|
| 10 |
+
|
| 11 |
+
## Models
|
| 12 |
+
|
| 13 |
+
Source ONNX (from the [TRvlvr/model_repo](https://github.com/TRvlvr/model_repo) UVR releases):
|
| 14 |
+
|
| 15 |
+
| Source ONNX | Output `.mlpackage` | Size | `dim_f` | Predicted stem |
|
| 16 |
+
| --- | --- | --- | --- | --- |
|
| 17 |
+
| `UVR_MDXNET_9482.onnx` | `UVR_MDXNET_9482.mlpackage` | ~15 MB | 2048 | **vocals** |
|
| 18 |
+
| `UVR-MDX-NET-Voc_FT.onnx` | `UVR-MDX-NET-Voc_FT.mlpackage` | ~32 MB | 3072 | **vocals** |
|
| 19 |
+
| `UVR-MDX-NET-Inst_HQ_3.onnx` | `UVR-MDX-NET-Inst_HQ_3.mlpackage` | ~32 MB | 3072 | **instrumental** |
|
| 20 |
+
|
| 21 |
+
All keep **NCHW** I/O with a static `dim_t = 256`: input `[1, 4, dim_f, 256]`, complex-as-channels
|
| 22 |
+
`[L_re, L_im, R_re, R_im]` β output with the same shape = the predicted stem's spectrogram. **Real
|
| 23 |
+
fp16 I/O**, fp16 compute, `mlprogram`, `minimum_deployment_target = iOS16` (~half the size of an
|
| 24 |
+
fp32 `.mlpackage` because the weights are stored fp16).
|
| 25 |
+
|
| 26 |
+
> **Inst HQ 3 inverts the residual.** It is architecturally identical to Voc FT (same 178 ops, same
|
| 27 |
+
> `dim_f 3072 / n_fft 6144 / hop 1024`, 16.68 M params, no embedded metadata) but it is an *Inst*
|
| 28 |
+
> model: the graph predicts the **instrumental**, so the free residual is `vocals = mix β model(mix)`
|
| 29 |
+
> β the opposite polarity from 9482/Voc FT. A caller that assumes "output = vocals" gets the stems
|
| 30 |
+
> swapped.
|
| 31 |
+
|
| 32 |
+
## Pipeline (`export_mdx_coreml.py`)
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
onnx2torch(onnx) β nn.Module β [SNR-gate vs ONNX Runtime > 100 dB]
|
| 36 |
+
β torch.jit.trace β coremltools.convert(mlprogram, fp16, compute_units, iOS16) β .mlpackage β verify
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
1. `onnx2torch(onnx)` β `nn.Module` (a plain conv U-Net: conv / bn / relu / convtranspose / matmul).
|
| 40 |
+
2. SNR-gate the torch module vs ONNX Runtime (must clear `--min-snr`, default 100 dB).
|
| 41 |
+
3. `torch.jit.trace` β `coremltools.convert(..., convert_to="mlprogram",
|
| 42 |
+
compute_precision=FLOAT16, compute_units=ALL, minimum_deployment_target=iOS16)`, keeping NCHW I/O
|
| 43 |
+
and real fp16 I/O (`dtype=np.float16`).
|
| 44 |
+
4. Verify: on-disk spec is an `mlProgram` with input/output `[1,4,dim_f,256]` fp16; on **macOS** also
|
| 45 |
+
a CoreML-vs-ONNX `predict` SNR (see "Verification status").
|
| 46 |
+
|
| 47 |
+
**Why `onnx2torch` (not a direct ONNXβCoreML path).** coremltools dropped its ONNX front-end years
|
| 48 |
+
ago; the supported route is TorchβCoreML. `onnx2torch` bridges our ONNX to a torch `nn.Module`,
|
| 49 |
+
which traces and converts cleanly, keeping the conversion input numerically faithful to the ONNX.
|
| 50 |
+
|
| 51 |
+
## Precision β fp16, and why it's safe
|
| 52 |
+
|
| 53 |
+
`compute_precision=FLOAT16` runs the whole graph in fp16 (the ~2Γ lever, native to the ANE and GPU).
|
| 54 |
+
MDX has no whole-tensor reduction β only per-channel BatchNorm that folds into the conv β so nothing
|
| 55 |
+
overflows the 65504 fp16 ceiling (peak activation ~560 for 9482 / ~1384 for Voc FT, measured). **I/O
|
| 56 |
+
is real fp16 too**, which is quality-safe for the same reason: the app's pipeline normalizes the mix,
|
| 57 |
+
so the input spectrogram sits far below the ceiling. Real fp16 I/O is the fastest, most idiomatic
|
| 58 |
+
config for an ANE/GPU model β no fp32βfp16 boundary cast, half the I/O bandwidth on the ~2M-element
|
| 59 |
+
tensors.
|
| 60 |
+
|
| 61 |
+
## Compute units β a LOAD-TIME choice
|
| 62 |
+
|
| 63 |
+
`--compute-units all` bakes `ComputeUnit.ALL` (ANE + GPU + CPU) as the model's default, but on iOS the
|
| 64 |
+
real selector is **load-time**: `MLModelConfiguration.computeUnits`. `.all` lets CoreML place ops on
|
| 65 |
+
the **Apple Neural Engine** first (fastest + most power-efficient on iPhone/iPad), then GPU, then CPU.
|
| 66 |
+
Use `.cpuAndGPU` to force the GPU and `.cpuOnly` as the CPU floor. The `--compute-units` flag here
|
| 67 |
+
only affects the Python `predict` default; the shipping app chooses per its own config (see below).
|
| 68 |
+
|
| 69 |
+
## Pinned environment
|
| 70 |
+
|
| 71 |
+
```
|
| 72 |
+
Python 3.11
|
| 73 |
+
coremltools 9.0
|
| 74 |
+
onnx2torch 1.5.15
|
| 75 |
+
torch 2.9.1+cpu (via the pytorch cpu index)
|
| 76 |
+
onnx, onnxruntime, numpy
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Rebuild from [`requirements.txt`](requirements.txt):
|
| 80 |
+
|
| 81 |
+
```bash
|
| 82 |
+
python3.11 -m venv .venv
|
| 83 |
+
.venv/bin/pip install --extra-index-url https://download.pytorch.org/whl/cpu -r requirements.txt
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
The `--extra-index-url` is REQUIRED: torch is pinned to the `+cpu` wheel and must resolve from the
|
| 87 |
+
pytorch cpu index, otherwise pip pulls the multi-GB CUDA build (and its `nvidia-*` deps). Pinning
|
| 88 |
+
torch in the same resolve also stops pip from silently upgrading it to a CUDA torch.
|
| 89 |
+
|
| 90 |
+
> coremltools 9.0 prints "Torch 2.9.1 has not been tested (2.7.0 is newest tested)" β a generic
|
| 91 |
+
> warning; it converts the MDX conv U-Net cleanly regardless (verified, torch-vs-ONNX SNR ~108β112
|
| 92 |
+
> dB).
|
| 93 |
+
|
| 94 |
+
## Run
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
PY=.venv/bin/python
|
| 98 |
+
$PY export_mdx_coreml.py ../UVR_MDXNET_9482.onnx output/UVR_MDXNET_9482.mlpackage
|
| 99 |
+
$PY export_mdx_coreml.py ../UVR-MDX-NET-Voc_FT.onnx output/UVR-MDX-NET-Voc_FT.mlpackage
|
| 100 |
+
$PY export_mdx_coreml.py ../UVR-MDX-NET-Inst_HQ_3.onnx output/UVR-MDX-NET-Inst_HQ_3.mlpackage --min-snr 90
|
| 101 |
+
# options: --compute-units {all,cpuAndGPU,cpuAndNE,cpuOnly} --deployment-target {iOS15..iOS18} --min-snr
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
The source `.onnx` files are the TRvlvr UVR releases (see the models table above); point the script
|
| 105 |
+
at your download of each.
|
| 106 |
+
|
| 107 |
+
**Why Inst HQ 3 needs `--min-snr 90`.** Its onnx2torch fidelity is **95.4 dB**, below the 100 dB
|
| 108 |
+
default. That is not a broken conversion: the error is dense and unstructured with a
|
| 109 |
+
*scale-invariant* absolute magnitude (~1.8e-4 max, unchanged from gaussianΓ1 to gaussianΓ0.05 inputs)
|
| 110 |
+
β plain fp32 accumulation round-off. Inst HQ 3 simply carries ~4Γ larger activations than Voc FT
|
| 111 |
+
(peak ~9.8 vs ~2.4), so the same relative round-off reads as a lower RMS SNR. A real op-level defect
|
| 112 |
+
(the failure mode this gate exists to catch) lands at 10β30 dB, not 95. For context the **shipped
|
| 113 |
+
artifact is fp16**, whose output-quantization floor is ~73 dB, so a 100 dB gate on the fp32
|
| 114 |
+
intermediate is stricter than the deliverable can ever be; 90 dB still sits ~17 dB above that floor.
|
| 115 |
+
|
| 116 |
+
## Verification status
|
| 117 |
+
|
| 118 |
+
- **onnx2torch fidelity (any OS):** torch-module vs ONNX Runtime SNR β **108 dB** (9482) / **112 dB**
|
| 119 |
+
(Voc FT) β the CoreML conversion input is faithful. Gated at 100 dB.
|
| 120 |
+
- **Structural (any OS):** on-disk `.mlpackage` is an `mlProgram`, I/O `[1,4,dim_f,256]` fp16.
|
| 121 |
+
- **Numeric CoreML fidelity + on-device (macOS-pending):** `model.predict()` needs the CoreML
|
| 122 |
+
runtime, which is **macOS-only** β on Linux the script reports it as `MACOS-PENDING`. Re-run
|
| 123 |
+
`export_mdx_coreml.py` on a Mac to get the CoreML-vs-ONNX predict SNR, and measure the ANE/GPU RTF
|
| 124 |
+
on-device.
|
| 125 |
+
|
| 126 |
+
## iOS integration (the contract to port)
|
| 127 |
+
|
| 128 |
+
The `.mlpackage` is only the learned core β an iOS app must reproduce the **same** host DSP the
|
| 129 |
+
shipped Android app uses: periodic Hann window, `center=True` reflect padding, **unnormalized** STFT
|
| 130 |
+
(`torch.stft normalized=False`), **Nyquist bin dropped** (`dim_f = n_fft/2`), plane order
|
| 131 |
+
`[L_re, L_im, R_re, R_im]`, flat row-major index `((plane*dim_f)+bin)*dim_t + frame`. Per model:
|
| 132 |
+
**9482** `n_fft 4096, hop 1024, dim_f 2048`; **Voc FT / Inst HQ 3** `n_fft 6144, hop 1024,
|
| 133 |
+
dim_f 3072`. The pipeline (chunk at 10% overlap β STFT β model β iSTFT β overlap-add;
|
| 134 |
+
instrumental = mix β vocals for the stem models, the inverse for Inst HQ 3) is the standard
|
| 135 |
+
MDX separation loop. Port the STFT/iSTFT to Accelerate/vDSP; the model call is:
|
| 136 |
+
|
| 137 |
+
```swift
|
| 138 |
+
import CoreML
|
| 139 |
+
|
| 140 |
+
// Load with the accelerator policy. `.all` == the baked default (ANE β GPU β CPU). Xcode compiles the
|
| 141 |
+
// .mlpackage into a .mlmodelc when it's added to the target (or MLModel.compileModel(at:) at runtime).
|
| 142 |
+
let config = MLModelConfiguration()
|
| 143 |
+
config.computeUnits = .all // GPU-only: .cpuAndGPU Β· CPU floor: .cpuOnly
|
| 144 |
+
let model = try MLModel(contentsOf: compiledURL, configuration: config)
|
| 145 |
+
|
| 146 |
+
// Per chunk. dim_f = 2048 (9482) or 3072 (Voc FT / Inst HQ 3); dim_t = 256. Real fp16 I/O.
|
| 147 |
+
let input = try MLMultiArray(shape: [1, 4, dimF as NSNumber, dimT as NSNumber], dataType: .float16)
|
| 148 |
+
let p = input.dataPointer.bindMemory(to: Float16.self, capacity: input.count)
|
| 149 |
+
// pack the STFT: p[((plane*dimF)+bin)*dimT + frame] = value (plane 0..3 = L_re,L_im,R_re,R_im)
|
| 150 |
+
let out = try model.prediction(from: MLDictionaryFeatureProvider(dictionary: ["input": input]))
|
| 151 |
+
let stems = out.featureValue(for: "output")!.multiArrayValue! // fp16 [1,4,dim_f,256] β iSTFT
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
> `MLMultiArray` is row-major for the given shape, so the flat index above matches the packing
|
| 155 |
+
> exactly. The UVR "shift trick" denoise (`0.5Β·model(x) β 0.5Β·model(βx)`) is optional. Remember the
|
| 156 |
+
> **Inst HQ 3 residual inversion** (`vocals = mix β model(mix)`) β see the models table.
|
| 157 |
+
|
| 158 |
+
The repository root `README.md` is the user-facing entry point; this document is the conversion
|
| 159 |
+
tooling reference for re-exporting or auditing the shipped artifacts.
|
export/export_mdx_coreml.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""One-shot: a UVR MDX-Net .onnx -> a GPU/ANE-ready fp16 CoreML .mlpackage for iOS.
|
| 3 |
+
|
| 4 |
+
Independent STFT-outside-the-graph export: the STFT/iSTFT stay OUTSIDE the graph (in the app's own
|
| 5 |
+
DSP), so the .mlpackage is the learned core only, with the ONNX's
|
| 6 |
+
NCHW I/O kept intact β input [1, 4, dim_f, 256] complex-as-channels [L_re, L_im, R_re, R_im] -> output
|
| 7 |
+
same shape = the vocal spectrogram.
|
| 8 |
+
|
| 9 |
+
Pipeline (each step gated so a bad export fails loudly, not silently):
|
| 10 |
+
1. onnx2torch(onnx) -> nn.Module
|
| 11 |
+
2. SNR-gate the torch module vs ONNX Runtime (must be > --min-snr, default 100 dB)
|
| 12 |
+
3. torch.jit.trace -> coremltools.convert(mlprogram, fp16, compute_units, iOS target) -> .mlpackage
|
| 13 |
+
4. verify: input shape [1,4,dim_f,256] from the saved spec; on macOS also a CoreML-vs-ONNX predict SNR.
|
| 14 |
+
|
| 15 |
+
Why no op-level surgery. CoreML's converter and its Metal-GPU / Apple-Neural-Engine backends lower
|
| 16 |
+
`ConvTranspose + ReLU` natively, so targeting `mlprogram` is a single clean convert β no op-version
|
| 17 |
+
patching, no post-conversion fixes.
|
| 18 |
+
|
| 19 |
+
fp16 (`compute_precision=FLOAT16`) is the ~2x lever and is MDX-safe (peak activation << the 65504 fp16
|
| 20 |
+
ceiling; MDX has no whole-tensor reduction). The Apple Neural Engine is fp16-native.
|
| 21 |
+
|
| 22 |
+
Compute units are a LOAD-TIME choice on-device (Swift `MLModelConfiguration.computeUnits`). Passing
|
| 23 |
+
`--compute-units` here only sets the default the Python `predict` uses on macOS; the shipped iOS app
|
| 24 |
+
picks the accelerator itself. Default is ALL (ANE + GPU + CPU) β see README.
|
| 25 |
+
|
| 26 |
+
Env (pinned, see requirements.txt): coremltools 9.0, onnx2torch 1.5.15, torch 2.9.1+cpu, onnx,
|
| 27 |
+
onnxruntime, numpy, Python 3.11. NOTE: converting runs anywhere; `predict()` (the numeric CoreML check
|
| 28 |
+
in step 4) is macOS-only β on Linux the .mlpackage is produced and structurally checked, and the
|
| 29 |
+
CoreML-vs-ONNX SNR is reported as macOS-pending.
|
| 30 |
+
|
| 31 |
+
Usage:
|
| 32 |
+
python export_mdx_coreml.py UVR_MDXNET_9482.onnx UVR_MDXNET_9482.mlpackage
|
| 33 |
+
python export_mdx_coreml.py UVR-MDX-NET-Voc_FT.onnx UVR-MDX-NET-Voc_FT.mlpackage
|
| 34 |
+
"""
|
| 35 |
+
import argparse
|
| 36 |
+
import os
|
| 37 |
+
import platform
|
| 38 |
+
import sys
|
| 39 |
+
import types
|
| 40 |
+
import warnings
|
| 41 |
+
|
| 42 |
+
warnings.filterwarnings("ignore")
|
| 43 |
+
os.environ.setdefault("GRPC_VERBOSITY", "NONE")
|
| 44 |
+
|
| 45 |
+
# onnx2torch eagerly imports torchvision; MDX uses no vision ops, so stub it out when the real package
|
| 46 |
+
# is missing or ABI-broken in the conversion env.
|
| 47 |
+
try:
|
| 48 |
+
import torchvision # noqa: F401
|
| 49 |
+
except Exception:
|
| 50 |
+
_tv = types.ModuleType("torchvision")
|
| 51 |
+
_tv.__version__ = "0.0.0"
|
| 52 |
+
_ops = types.ModuleType("torchvision.ops")
|
| 53 |
+
|
| 54 |
+
def _stub(*a, **k):
|
| 55 |
+
raise NotImplementedError("torchvision op unavailable (stub)")
|
| 56 |
+
|
| 57 |
+
for _n in ("nms", "batched_nms", "roi_align", "RoIAlign", "DeformConv2d", "deform_conv2d"):
|
| 58 |
+
setattr(_ops, _n, _stub)
|
| 59 |
+
_tv.ops = _ops
|
| 60 |
+
sys.modules["torchvision"] = _tv
|
| 61 |
+
sys.modules["torchvision.ops"] = _ops
|
| 62 |
+
|
| 63 |
+
import numpy as np
|
| 64 |
+
import onnx
|
| 65 |
+
import onnx2torch
|
| 66 |
+
import onnxruntime as ort
|
| 67 |
+
import torch
|
| 68 |
+
import coremltools as ct
|
| 69 |
+
|
| 70 |
+
DIM_T = 256 # native trained segment (~5.92 s @ hop 1024); static in the shipped .mlpackage
|
| 71 |
+
|
| 72 |
+
# --compute-units / --deployment-target string -> coremltools enum.
|
| 73 |
+
COMPUTE_UNITS = {
|
| 74 |
+
"all": ct.ComputeUnit.ALL, # ANE + GPU + CPU (default; best on Apple silicon)
|
| 75 |
+
"cpuAndGPU": ct.ComputeUnit.CPU_AND_GPU,
|
| 76 |
+
"cpuAndNE": ct.ComputeUnit.CPU_AND_NE,
|
| 77 |
+
"cpuOnly": ct.ComputeUnit.CPU_ONLY,
|
| 78 |
+
}
|
| 79 |
+
DEPLOY_TARGETS = {
|
| 80 |
+
"iOS15": ct.target.iOS15,
|
| 81 |
+
"iOS16": ct.target.iOS16,
|
| 82 |
+
"iOS17": ct.target.iOS17,
|
| 83 |
+
"iOS18": ct.target.iOS18,
|
| 84 |
+
}
|
| 85 |
+
IN_NAME = "input" # CaC spectrogram [1, 4, dim_f, 256] (Swift MLFeatureProvider key)
|
| 86 |
+
OUT_NAME = "output" # vocal spectrogram [1, 4, dim_f, 256]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def onnx_dim_f(onnx_path):
|
| 90 |
+
"""MDX input is [batch, 4, dim_f, dim_t]; return dim_f (2048 for 9482, 3072 for Voc FT)."""
|
| 91 |
+
dims = onnx.load(onnx_path).graph.input[0].type.tensor_type.shape.dim
|
| 92 |
+
return int(dims[2].dim_value)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def sample(dim_f):
|
| 96 |
+
return np.random.randn(1, 4, dim_f, DIM_T).astype(np.float32)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def snr(ref, got):
|
| 100 |
+
den = np.sqrt((ref ** 2).mean())
|
| 101 |
+
num = np.sqrt(((ref - got) ** 2).mean())
|
| 102 |
+
return 20.0 * np.log10(den / (num + 1e-12))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def module_snr(mod, onnx_path, dim_f, trials=2):
|
| 106 |
+
"""Fidelity of the onnx2torch module vs ONNX Runtime β the CoreML conversion INPUT. This gate runs
|
| 107 |
+
fully on Linux (no CoreML runtime needed)."""
|
| 108 |
+
sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
|
| 109 |
+
iname = sess.get_inputs()[0].name
|
| 110 |
+
worst = 1e9
|
| 111 |
+
for _ in range(trials):
|
| 112 |
+
x = sample(dim_f)
|
| 113 |
+
ref = sess.run(None, {iname: x})[0]
|
| 114 |
+
with torch.no_grad():
|
| 115 |
+
got = mod(torch.from_numpy(x)).numpy()
|
| 116 |
+
worst = min(worst, snr(ref, got))
|
| 117 |
+
return float(worst)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def dir_size_mb(path):
|
| 121 |
+
total = 0
|
| 122 |
+
for root, _, files in os.walk(path):
|
| 123 |
+
for f in files:
|
| 124 |
+
total += os.path.getsize(os.path.join(root, f))
|
| 125 |
+
return total / 1e6
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def coreml_predict_snr(mlmodel, onnx_path, dim_f, trials=2):
|
| 129 |
+
"""macOS-only: run the compiled CoreML model and compare vs ONNX Runtime. Raises on non-macOS
|
| 130 |
+
(no CoreML runtime), which the caller catches and reports as macOS-pending."""
|
| 131 |
+
sess = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
|
| 132 |
+
iname = sess.get_inputs()[0].name
|
| 133 |
+
worst, oshape = 1e9, None
|
| 134 |
+
for _ in range(trials):
|
| 135 |
+
x = sample(dim_f) # fp32 reference input
|
| 136 |
+
ref = sess.run(None, {iname: x})[0] # fp32 ONNX golden
|
| 137 |
+
# Model I/O is fp16: feed the fp16 view so SNR reflects the true deployed path (fp16 I/O +
|
| 138 |
+
# fp16 compute) vs the fp32 ONNX golden.
|
| 139 |
+
got = mlmodel.predict({IN_NAME: x.astype(np.float16)})[OUT_NAME]
|
| 140 |
+
oshape = tuple(np.asarray(got).shape)
|
| 141 |
+
worst = min(worst, snr(ref, np.asarray(got, dtype=np.float32)))
|
| 142 |
+
return float(worst), oshape
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main():
|
| 146 |
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
| 147 |
+
ap.add_argument("onnx", help="source UVR MDX-Net .onnx")
|
| 148 |
+
ap.add_argument("out", help="output .mlpackage")
|
| 149 |
+
ap.add_argument("--min-snr", type=float, default=100.0)
|
| 150 |
+
ap.add_argument("--compute-units", choices=list(COMPUTE_UNITS), default="all",
|
| 151 |
+
help="baked default for Python predict; the iOS app sets this at load time (default: all)")
|
| 152 |
+
ap.add_argument("--deployment-target", choices=list(DEPLOY_TARGETS), default="iOS16")
|
| 153 |
+
args = ap.parse_args()
|
| 154 |
+
|
| 155 |
+
dim_f = onnx_dim_f(args.onnx)
|
| 156 |
+
print(f"[1/4] onnx2torch: {args.onnx} (dim_f={dim_f}, dim_t={DIM_T})", flush=True)
|
| 157 |
+
mod = onnx2torch.convert(args.onnx).eval()
|
| 158 |
+
|
| 159 |
+
msnr = module_snr(mod, args.onnx, dim_f)
|
| 160 |
+
print(f"[2/4] torch-module SNR vs ONNX Runtime = {msnr:.1f} dB", flush=True)
|
| 161 |
+
if msnr < args.min_snr:
|
| 162 |
+
raise SystemExit(f"FAIL: onnx2torch fidelity {msnr:.1f} dB < {args.min_snr} dB (do not ship)")
|
| 163 |
+
|
| 164 |
+
print(f"[3/4] trace + coremltools.convert -> {args.out} "
|
| 165 |
+
f"(mlprogram, fp16, compute_units={args.compute_units}, {args.deployment_target})",
|
| 166 |
+
flush=True)
|
| 167 |
+
example = torch.from_numpy(sample(dim_f))
|
| 168 |
+
with torch.no_grad():
|
| 169 |
+
traced = torch.jit.trace(mod, example)
|
| 170 |
+
# NCHW I/O is kept from the ONNX so the app's CaC packing feeds it directly (no transpose).
|
| 171 |
+
# I/O is real fp16 (dtype `np.float16`, matching the fp16 compute) β the fastest, most idiomatic
|
| 172 |
+
# config for an fp16 ANE/GPU model: no fp32<->fp16 boundary cast, half the I/O bandwidth on the
|
| 173 |
+
# ~2M-element tensors per chunk, and the ANE's native precision end to end. It is quality-safe
|
| 174 |
+
# because the pipeline normalizes the mix, so the input spectrogram sits far below the 65504 fp16
|
| 175 |
+
# ceiling (peak activation ~560/1384 measured β fp16-safety end to end, extended to
|
| 176 |
+
# the boundary). The Swift side feeds/reads an MLMultiArray(.float16); packing the fp32 host-DSP
|
| 177 |
+
# spectrogram into fp16 (and back) is a trivial element-wise cast. See README "iOS integration".
|
| 178 |
+
mlmodel = ct.convert(
|
| 179 |
+
traced,
|
| 180 |
+
inputs=[ct.TensorType(name=IN_NAME, shape=(1, 4, dim_f, DIM_T), dtype=np.float16)],
|
| 181 |
+
outputs=[ct.TensorType(name=OUT_NAME, dtype=np.float16)],
|
| 182 |
+
convert_to="mlprogram",
|
| 183 |
+
compute_precision=ct.precision.FLOAT16, # fp16: the ~2x lever, MDX-safe; ANE is fp16-native
|
| 184 |
+
compute_units=COMPUTE_UNITS[args.compute_units],
|
| 185 |
+
minimum_deployment_target=DEPLOY_TARGETS[args.deployment_target],
|
| 186 |
+
)
|
| 187 |
+
mlmodel.save(args.out)
|
| 188 |
+
|
| 189 |
+
# [4/4] verify. Input shape comes straight from the saved spec (works on any OS). The numeric
|
| 190 |
+
# CoreML-vs-ONNX SNR needs the CoreML runtime -> macOS only; on Linux report it as pending.
|
| 191 |
+
# Read the ON-DISK artifact's spec (the source of truth for the shippable .mlpackage; the
|
| 192 |
+
# in-memory convert result doesn't finalize the fp16 I/O boundary until serialized).
|
| 193 |
+
spec = ct.models.MLModel(args.out, skip_model_load=True).get_spec()
|
| 194 |
+
in_shape = list(spec.description.input[0].type.multiArrayType.shape)
|
| 195 |
+
out_shape = list(spec.description.output[0].type.multiArrayType.shape)
|
| 196 |
+
# CoreML ArrayFeatureType.ArrayDataType: FLOAT32=65568, FLOAT16=65552, DOUBLE=65600.
|
| 197 |
+
io_dtype = {65568: "fp32", 65552: "fp16", 65600: "fp64"}.get(
|
| 198 |
+
spec.description.input[0].type.multiArrayType.dataType, "?")
|
| 199 |
+
size_mb = dir_size_mb(args.out)
|
| 200 |
+
shape_ok = (in_shape == [1, 4, dim_f, DIM_T]
|
| 201 |
+
and spec.WhichOneof("Type") == "mlProgram"
|
| 202 |
+
and io_dtype == "fp16")
|
| 203 |
+
|
| 204 |
+
csnr, cshape, verified = None, None, False
|
| 205 |
+
try:
|
| 206 |
+
csnr, cshape = coreml_predict_snr(mlmodel, args.onnx, dim_f)
|
| 207 |
+
verified = True
|
| 208 |
+
except Exception as e: # non-macOS (no CoreML runtime) or predict failure
|
| 209 |
+
note = f"{type(e).__name__}: {str(e).splitlines()[0][:120]}" if str(e) else type(e).__name__
|
| 210 |
+
|
| 211 |
+
print(f"[4/4] verify: in={in_shape} out={out_shape or 'unspecified (shape-preserving U-Net)'} | "
|
| 212 |
+
f"io={io_dtype} compute=fp16 | mlprogram | size={size_mb:.1f} MB", flush=True)
|
| 213 |
+
if verified:
|
| 214 |
+
ok = shape_ok and csnr >= args.min_snr and cshape == (1, 4, dim_f, DIM_T)
|
| 215 |
+
print(f" CoreML-vs-ONNX predict SNR = {csnr:.1f} dB (macOS) | out={cshape}", flush=True)
|
| 216 |
+
print(" " + ("PASS β GPU/ANE-ready fp16 .mlpackage" if ok else "FAIL β do not ship"), flush=True)
|
| 217 |
+
raise SystemExit(0 if ok else 1)
|
| 218 |
+
else:
|
| 219 |
+
ok = shape_ok
|
| 220 |
+
print(f" CoreML predict SNR: MACOS-PENDING (no CoreML runtime here β {note})", flush=True)
|
| 221 |
+
print(" re-run this script on macOS to get the on-CoreML fidelity number", flush=True)
|
| 222 |
+
print(" " + ("PASS (structural) β .mlpackage produced; numeric CoreML check macOS-pending"
|
| 223 |
+
if ok else "FAIL β input shape mismatch"), flush=True)
|
| 224 |
+
raise SystemExit(0 if ok else 1)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
if __name__ == "__main__":
|
| 228 |
+
main()
|
export/requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pinned MDX-Net -> CoreML export toolchain for THIS repository (see README.md).
|
| 2 |
+
# Self-contained: rebuild from this folder, no other repo paths needed.
|
| 3 |
+
#
|
| 4 |
+
# Rebuild (Python 3.11) from the export/ folder:
|
| 5 |
+
# python3.11 -m venv .venv
|
| 6 |
+
# .venv/bin/pip install --extra-index-url https://download.pytorch.org/whl/cpu -r requirements.txt
|
| 7 |
+
#
|
| 8 |
+
# The `--extra-index-url` is REQUIRED: torch is pinned to the +cpu wheel and must resolve from the
|
| 9 |
+
# pytorch cpu index, otherwise pip pulls the multi-GB CUDA build (and its nvidia-* deps). Pinning torch
|
| 10 |
+
# in the same resolve also stops pip from silently upgrading it to a CUDA torch.
|
| 11 |
+
#
|
| 12 |
+
# coremltools 9.0 prints "Torch 2.9.1 has not been tested (2.7.0 is newest tested)" β a generic warning;
|
| 13 |
+
# it converts the MDX conv U-Net (conv/bn/relu/convtranspose/matmul) cleanly regardless (verified,
|
| 14 |
+
# torch-vs-ONNX SNR ~108-112 dB).
|
| 15 |
+
#
|
| 16 |
+
# Converting runs on Linux; `model.predict()` (the numeric CoreML-vs-ONNX check in step 4) needs the
|
| 17 |
+
# CoreML runtime and is macOS-only.
|
| 18 |
+
torch==2.9.1+cpu
|
| 19 |
+
coremltools==9.0
|
| 20 |
+
onnx2torch==1.5.15
|
| 21 |
+
onnx
|
| 22 |
+
onnxruntime
|
| 23 |
+
numpy
|