Dirdx
/

Dirdx ahmadw commited on
Commit
be2c293
·
0 Parent(s):

Duplicate from ahmadw/YuE2-3B-MLX

Browse files

Co-authored-by: Ahmad <ahmadw@users.noreply.huggingface.co>

.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz 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
4bit/config.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "bfloat16",
6
+ "tie_word_embeddings": false,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2ForCausalLM"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_yue2.YuE2Config",
20
+ "AutoModelForCausalLM": "modeling_yue2.YuE2ForCausalLM"
21
+ },
22
+ "model_type": "yue2",
23
+ "hidden_size": 2048,
24
+ "num_hidden_layers": 28,
25
+ "num_attention_heads": 16,
26
+ "num_key_value_heads": 8,
27
+ "head_dim": 128,
28
+ "intermediate_size": 6144,
29
+ "vocab_size": 184704,
30
+ "rms_norm_eps": 1e-06,
31
+ "rope_theta": 1000000,
32
+ "max_position_embeddings": 24576,
33
+ "latent_type": "vae",
34
+ "latent_dim": 64,
35
+ "max_latent_frames": 24576,
36
+ "timestep_shift": 1.0,
37
+ "output_attentions": false,
38
+ "quantization": {
39
+ "group_size": 64,
40
+ "bits": 4,
41
+ "nar_bits": 8,
42
+ "mode": "affine"
43
+ }
44
+ }
4bit/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b782c22ef2d34a233e45106793b8248b0a2ef9de7e823177b02d4bab6e6b05ac
3
+ size 3370707798
4bit/qwen.tiktoken ADDED
The diff for this file is too large to render. See raw diff
 
4bit/vae.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc7bea8e4f8e764be6ee2ce3291606a5381aaa20f5c4eb26e86ca81dd1eeac6e
3
+ size 265441814
4bit/vae_config.json ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "float32",
6
+ "tie_word_embeddings": true,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2VAE"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_vae.YuE2VAEConfig",
20
+ "AutoModel": "modeling_vae.YuE2VAE"
21
+ },
22
+ "model_type": "yue2_vae",
23
+ "encoder_config": {
24
+ "c_mults": [
25
+ 1,
26
+ 2,
27
+ 4,
28
+ 8,
29
+ 16,
30
+ 32
31
+ ],
32
+ "channels": 64,
33
+ "in_channels": 2,
34
+ "latent_dim": 128,
35
+ "strides": [
36
+ 2,
37
+ 2,
38
+ 4,
39
+ 4,
40
+ 5,
41
+ 6
42
+ ],
43
+ "use_snake": true
44
+ },
45
+ "decoder_config": {
46
+ "c_mults": [
47
+ 1,
48
+ 2,
49
+ 4,
50
+ 8,
51
+ 16,
52
+ 32
53
+ ],
54
+ "channels": 64,
55
+ "final_tanh": false,
56
+ "latent_dim": 64,
57
+ "out_channels": 2,
58
+ "snake_type": "vanilla",
59
+ "strides": [
60
+ 2,
61
+ 2,
62
+ 4,
63
+ 4,
64
+ 5,
65
+ 6
66
+ ],
67
+ "use_filter": false,
68
+ "use_snake": true
69
+ },
70
+ "sample_rate": 48000,
71
+ "latent_dim": 64,
72
+ "downsampling_ratio": 1920,
73
+ "audio_channels": 2,
74
+ "release_variant": "standard",
75
+ "decode_core_frames": 1024,
76
+ "decode_halo_frames": 16,
77
+ "output_attentions": false
78
+ }
4bit/yue2_generation_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "abc": {
3
+ "temperature": 0.7,
4
+ "top_p": 0.9,
5
+ "top_k": 30,
6
+ "repetition_penalty": 1.005,
7
+ "penalty_window": 100,
8
+ "min_tokens": 32,
9
+ "max_tokens": 4096
10
+ },
11
+ "semantic": {
12
+ "temperature": 1.0,
13
+ "top_p": 0.95,
14
+ "top_k": 100,
15
+ "repetition_penalty": 1.2,
16
+ "penalty_window": 50,
17
+ "min_tokens": 200,
18
+ "max_tokens": 9000
19
+ },
20
+ "ode_steps": 32,
21
+ "ode_method": "midpoint",
22
+ "context": 24576,
23
+ "version": "yue2-native-v1"
24
+ }
8bit/config.json ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "bfloat16",
6
+ "tie_word_embeddings": false,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2ForCausalLM"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_yue2.YuE2Config",
20
+ "AutoModelForCausalLM": "modeling_yue2.YuE2ForCausalLM"
21
+ },
22
+ "model_type": "yue2",
23
+ "hidden_size": 2048,
24
+ "num_hidden_layers": 28,
25
+ "num_attention_heads": 16,
26
+ "num_key_value_heads": 8,
27
+ "head_dim": 128,
28
+ "intermediate_size": 6144,
29
+ "vocab_size": 184704,
30
+ "rms_norm_eps": 1e-06,
31
+ "rope_theta": 1000000,
32
+ "max_position_embeddings": 24576,
33
+ "latent_type": "vae",
34
+ "latent_dim": 64,
35
+ "max_latent_frames": 24576,
36
+ "timestep_shift": 1.0,
37
+ "output_attentions": false,
38
+ "quantization": {
39
+ "group_size": 64,
40
+ "bits": 8,
41
+ "mode": "affine"
42
+ }
43
+ }
8bit/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bb6ff9895114be4aaa6e1b3a7d272eee55b54c9e619716c67355b45673fe3d7c
3
+ size 4264488042
8bit/qwen.tiktoken ADDED
The diff for this file is too large to render. See raw diff
 
8bit/vae.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc7bea8e4f8e764be6ee2ce3291606a5381aaa20f5c4eb26e86ca81dd1eeac6e
3
+ size 265441814
8bit/vae_config.json ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "float32",
6
+ "tie_word_embeddings": true,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2VAE"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_vae.YuE2VAEConfig",
20
+ "AutoModel": "modeling_vae.YuE2VAE"
21
+ },
22
+ "model_type": "yue2_vae",
23
+ "encoder_config": {
24
+ "c_mults": [
25
+ 1,
26
+ 2,
27
+ 4,
28
+ 8,
29
+ 16,
30
+ 32
31
+ ],
32
+ "channels": 64,
33
+ "in_channels": 2,
34
+ "latent_dim": 128,
35
+ "strides": [
36
+ 2,
37
+ 2,
38
+ 4,
39
+ 4,
40
+ 5,
41
+ 6
42
+ ],
43
+ "use_snake": true
44
+ },
45
+ "decoder_config": {
46
+ "c_mults": [
47
+ 1,
48
+ 2,
49
+ 4,
50
+ 8,
51
+ 16,
52
+ 32
53
+ ],
54
+ "channels": 64,
55
+ "final_tanh": false,
56
+ "latent_dim": 64,
57
+ "out_channels": 2,
58
+ "snake_type": "vanilla",
59
+ "strides": [
60
+ 2,
61
+ 2,
62
+ 4,
63
+ 4,
64
+ 5,
65
+ 6
66
+ ],
67
+ "use_filter": false,
68
+ "use_snake": true
69
+ },
70
+ "sample_rate": 48000,
71
+ "latent_dim": 64,
72
+ "downsampling_ratio": 1920,
73
+ "audio_channels": 2,
74
+ "release_variant": "standard",
75
+ "decode_core_frames": 1024,
76
+ "decode_halo_frames": 16,
77
+ "output_attentions": false
78
+ }
8bit/yue2_generation_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "abc": {
3
+ "temperature": 0.7,
4
+ "top_p": 0.9,
5
+ "top_k": 30,
6
+ "repetition_penalty": 1.005,
7
+ "penalty_window": 100,
8
+ "min_tokens": 32,
9
+ "max_tokens": 4096
10
+ },
11
+ "semantic": {
12
+ "temperature": 1.0,
13
+ "top_p": 0.95,
14
+ "top_k": 100,
15
+ "repetition_penalty": 1.2,
16
+ "penalty_window": 50,
17
+ "min_tokens": 200,
18
+ "max_tokens": 9000
19
+ },
20
+ "ode_steps": 32,
21
+ "ode_method": "midpoint",
22
+ "context": 24576,
23
+ "version": "yue2-native-v1"
24
+ }
README.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: cc-by-nc-4.0
3
+ base_model: m-a-p/YuE2-3B
4
+ base_model_relation: quantized
5
+ library_name: mlx
6
+ pipeline_tag: text-to-audio
7
+ tags:
8
+ - mlx
9
+ - music-generation
10
+ - yue2
11
+ language:
12
+ - en
13
+ - zh
14
+ ---
15
+
16
+ # YuE2-3B MLX
17
+
18
+ Native [MLX](https://github.com/ml-explore/mlx) port of [m-a-p/YuE2-3B](https://huggingface.co/m-a-p/YuE2-3B) (AR/NAR
19
+ Mixture-of-Transformers music generation) with the [YuE2-Vae](https://huggingface.co/m-a-p/YuE2-Vae) decoder folded in.
20
+ Runs on Apple Silicon with no PyTorch dependency. Lyrics + style prompt in, 48 kHz stereo song out, with optional
21
+ editable ABC score planning.
22
+
23
+ ## Variants
24
+
25
+ | Folder | Backbone | Size | AR logits / NAR velocity error vs torch bf16 |
26
+ |---|---|---|---|
27
+ | `bf16/` | bf16 | 7.0 GB | 1.4% / 1.6% |
28
+ | `8bit/` | 8-bit affine, group size 64, all backbone Linear layers | 4.2 GB | 1.0% / 1.9% |
29
+ | `4bit/` | 4-bit AR path, 8-bit NAR path, group size 64 | 3.4 GB | 8.1% / 4.4% |
30
+
31
+ The NAR (flow-matching) path stays at 8 bits in the 4-bit variant because fully 4-bit flow matching drifts
32
+ (12% velocity error). VAE decode is bit-exact in all variants. `8bit/` is the recommended default: near-bf16 output
33
+ at roughly twice the decode speed.
34
+
35
+ Each folder contains `model.safetensors`, `vae.safetensors`, `config.json`, `vae_config.json`, `qwen.tiktoken`
36
+ and `yue2_generation_config.json`. The Python files at the repo root are the inference code.
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ pip install mlx tiktoken numpy huggingface_hub
42
+ hf download ahmadw/YuE2-3B-MLX --include "*.py" "8bit/*" --local-dir YuE2-3B-MLX
43
+ cd YuE2-3B-MLX
44
+ python generate.py --model 8bit \
45
+ --style "English, indie pop, bright acoustic guitar, soft drums, warm lead vocal" \
46
+ --lyrics "[Verse]
47
+ Soft morning light is touching the window.
48
+ [Chorus]
49
+ Stay with the rhythm, let it carry us home." \
50
+ --cot full --seed 831001 --out song.wav
51
+ ```
52
+
53
+ Options: `--cot off|melody|full` (symbolic ABC planning; `full` is the upstream default), `--abc-file` to supply your
54
+ own score, `--cfg-scale`, `--steps` (NAR midpoint steps, default 32), `--max-semantic-tokens` (song length cap, 25
55
+ frames per second), `--decode-latents <out>.latents.npy` to re-decode a saved run. A generated ABC score is written
56
+ next to the wav.
57
+
58
+ Measured on an M-series Mac: bf16 decodes at about 70 tokens/s (35 with CFG), 8bit at about 80 with CFG.
59
+ A three-minute song takes a few minutes end to end.
60
+
61
+ ## Converting yourself
62
+
63
+ ```bash
64
+ python convert.py --output YuE2-3B-MLX/8bit -q --bits 8 # or --bits 4 (NAR stays 8-bit), or no -q for bf16
65
+ python test_parity.py --mlx YuE2-3B-MLX/8bit # needs torch + transformers
66
+ ```
67
+
68
+ Sampling uses MLX's RNG, so seeds reproduce within MLX only.
69
+
70
+ ## License
71
+
72
+ Weights are derived from m-a-p/YuE2-3B and YuE2-Vae and inherit their **CC BY-NC 4.0** license (non-commercial).
73
+ The SnakeBeta activation and stable-audio-tools decoder code are MIT-licensed by NVIDIA and Stability AI respectively;
74
+ see the upstream `THIRD_PARTY_NOTICES.md`. Please cite the original YuE2 authors (m-a-p) when using these weights.
bf16/config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "bfloat16",
6
+ "tie_word_embeddings": false,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2ForCausalLM"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_yue2.YuE2Config",
20
+ "AutoModelForCausalLM": "modeling_yue2.YuE2ForCausalLM"
21
+ },
22
+ "model_type": "yue2",
23
+ "hidden_size": 2048,
24
+ "num_hidden_layers": 28,
25
+ "num_attention_heads": 16,
26
+ "num_key_value_heads": 8,
27
+ "head_dim": 128,
28
+ "intermediate_size": 6144,
29
+ "vocab_size": 184704,
30
+ "rms_norm_eps": 1e-06,
31
+ "rope_theta": 1000000,
32
+ "max_position_embeddings": 24576,
33
+ "latent_type": "vae",
34
+ "latent_dim": 64,
35
+ "max_latent_frames": 24576,
36
+ "timestep_shift": 1.0,
37
+ "output_attentions": false
38
+ }
bf16/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e6012e94aca961eebc2f57ae0c5417b1f40125c33c41e5f3a6ca0b28982361f7
3
+ size 7261441586
bf16/qwen.tiktoken ADDED
The diff for this file is too large to render. See raw diff
 
bf16/vae.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dc7bea8e4f8e764be6ee2ce3291606a5381aaa20f5c4eb26e86ca81dd1eeac6e
3
+ size 265441814
bf16/vae_config.json ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "return_dict": true,
3
+ "output_hidden_states": false,
4
+ "torchscript": false,
5
+ "dtype": "float32",
6
+ "tie_word_embeddings": true,
7
+ "is_encoder_decoder": false,
8
+ "is_decoder": false,
9
+ "add_cross_attention": false,
10
+ "architectures": [
11
+ "YuE2VAE"
12
+ ],
13
+ "bos_token_id": null,
14
+ "pad_token_id": null,
15
+ "eos_token_id": null,
16
+ "decoder_start_token_id": null,
17
+ "transformers_version": "4.57.6",
18
+ "auto_map": {
19
+ "AutoConfig": "modeling_vae.YuE2VAEConfig",
20
+ "AutoModel": "modeling_vae.YuE2VAE"
21
+ },
22
+ "model_type": "yue2_vae",
23
+ "encoder_config": {
24
+ "c_mults": [
25
+ 1,
26
+ 2,
27
+ 4,
28
+ 8,
29
+ 16,
30
+ 32
31
+ ],
32
+ "channels": 64,
33
+ "in_channels": 2,
34
+ "latent_dim": 128,
35
+ "strides": [
36
+ 2,
37
+ 2,
38
+ 4,
39
+ 4,
40
+ 5,
41
+ 6
42
+ ],
43
+ "use_snake": true
44
+ },
45
+ "decoder_config": {
46
+ "c_mults": [
47
+ 1,
48
+ 2,
49
+ 4,
50
+ 8,
51
+ 16,
52
+ 32
53
+ ],
54
+ "channels": 64,
55
+ "final_tanh": false,
56
+ "latent_dim": 64,
57
+ "out_channels": 2,
58
+ "snake_type": "vanilla",
59
+ "strides": [
60
+ 2,
61
+ 2,
62
+ 4,
63
+ 4,
64
+ 5,
65
+ 6
66
+ ],
67
+ "use_filter": false,
68
+ "use_snake": true
69
+ },
70
+ "sample_rate": 48000,
71
+ "latent_dim": 64,
72
+ "downsampling_ratio": 1920,
73
+ "audio_channels": 2,
74
+ "release_variant": "standard",
75
+ "decode_core_frames": 1024,
76
+ "decode_halo_frames": 16,
77
+ "output_attentions": false
78
+ }
bf16/yue2_generation_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "abc": {
3
+ "temperature": 0.7,
4
+ "top_p": 0.9,
5
+ "top_k": 30,
6
+ "repetition_penalty": 1.005,
7
+ "penalty_window": 100,
8
+ "min_tokens": 32,
9
+ "max_tokens": 4096
10
+ },
11
+ "semantic": {
12
+ "temperature": 1.0,
13
+ "top_p": 0.95,
14
+ "top_k": 100,
15
+ "repetition_penalty": 1.2,
16
+ "penalty_window": 50,
17
+ "min_tokens": 200,
18
+ "max_tokens": 9000
19
+ },
20
+ "ode_steps": 32,
21
+ "ode_method": "midpoint",
22
+ "context": 24576,
23
+ "version": "yue2-native-v1"
24
+ }
convert.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Convert m-a-p/YuE2-3B + m-a-p/YuE2-Vae into an MLX package directory.
3
+
4
+ Output layout (HF-uploadable):
5
+ model.safetensors bf16 backbone, or affine-quantized Linear layers (--quantize)
6
+ vae.safetensors f32 Oobleck decoder, weight norm folded, MLX conv layout
7
+ config.json, vae_config.json, qwen.tiktoken, yue2_generation_config.json
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import shutil
15
+ from pathlib import Path
16
+
17
+ import mlx.core as mx
18
+ import mlx.nn as nn
19
+ from mlx.utils import tree_flatten
20
+
21
+ from yue2_model import Yue2Model
22
+
23
+ TIME_EMBEDDER = {"time_embedder.mlp.0.": "time_embedder.fc1.", "time_embedder.mlp.2.": "time_embedder.fc2."}
24
+ CONV_TRANSPOSE = re.compile(r"^layers\.\d+\.layers\.1\.weight_v$")
25
+
26
+
27
+ def resolve(repo_or_dir: str, patterns: list[str]) -> Path:
28
+ if Path(repo_or_dir).is_dir():
29
+ return Path(repo_or_dir)
30
+ from huggingface_hub import snapshot_download
31
+ return Path(snapshot_download(repo_or_dir, allow_patterns=patterns))
32
+
33
+
34
+ def rename(key: str) -> str:
35
+ for old, new in TIME_EMBEDDER.items():
36
+ if key.startswith(old):
37
+ return new + key[len(old):]
38
+ return key
39
+
40
+
41
+ def fold_weight_norm(weights: dict) -> dict:
42
+ """decoder.* (g, v) pairs → single MLX-layout conv weights."""
43
+ out = {}
44
+ for key, value in weights.items():
45
+ if not key.startswith("decoder.") or key.endswith("weight_g"):
46
+ continue
47
+ key = key[len("decoder."):]
48
+ if key.endswith("weight_v"):
49
+ g = weights["decoder." + key[:-1] + "g"]
50
+ norm = mx.sqrt(mx.sum(value.astype(mx.float32) ** 2, axis=(1, 2), keepdims=True))
51
+ w = g * value / norm
52
+ # torch Conv1d [out,in,k] → mlx [out,k,in]; ConvTranspose1d [in,out,k] → mlx [out,k,in]
53
+ w = w.transpose(1, 2, 0) if CONV_TRANSPOSE.match(key) else w.transpose(0, 2, 1)
54
+ out[key[:-2]] = w
55
+ else:
56
+ out[key] = value
57
+ return out
58
+
59
+
60
+ def main() -> None:
61
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
62
+ parser.add_argument("--model", default="m-a-p/YuE2-3B", help="HF repo id or local snapshot dir")
63
+ parser.add_argument("--vae", default="m-a-p/YuE2-Vae", help="HF repo id or local snapshot dir")
64
+ parser.add_argument("--output", type=Path, default=Path.home() / "Desktop" / "Yue2-3B-MLX")
65
+ parser.add_argument("-q", "--quantize", action="store_true", help="affine-quantize backbone Linear layers")
66
+ parser.add_argument("--bits", type=int, default=8, choices=[4, 8])
67
+ parser.add_argument("--group-size", type=int, default=64)
68
+ parser.add_argument("--nar-bits", type=int, choices=[4, 8], help="bits for the NAR path (nar_self_attn/nar_mlp); default 8 (flow matching degrades at 4 bits)")
69
+ parser.add_argument("--overwrite", action="store_true")
70
+ args = parser.parse_args()
71
+
72
+ if args.output.exists():
73
+ if not args.overwrite:
74
+ raise SystemExit(f"{args.output} exists; pass --overwrite")
75
+ shutil.rmtree(args.output)
76
+ args.output.mkdir(parents=True)
77
+
78
+ model_dir = resolve(args.model, ["model.safetensors", "config.json", "qwen.tiktoken", "yue2_generation_config.json"])
79
+ vae_dir = resolve(args.vae, ["model.safetensors", "config.json"])
80
+
81
+ cfg = json.loads((model_dir / "config.json").read_text())
82
+ weights = {rename(k): v for k, v in mx.load(str(model_dir / "model.safetensors")).items()}
83
+ if args.quantize:
84
+ model = Yue2Model(cfg)
85
+ model.load_weights(list(weights.items()))
86
+ nar_bits = args.nar_bits or 8
87
+
88
+ def predicate(path, module):
89
+ if not isinstance(module, nn.Linear) or not (path.startswith("model.layers") or path == "lm_head"):
90
+ return False
91
+ return {"group_size": args.group_size, "bits": nar_bits if ".nar_" in path else args.bits}
92
+
93
+ nn.quantize(model, args.group_size, args.bits, class_predicate=predicate)
94
+ weights = dict(tree_flatten(model.parameters()))
95
+ cfg["quantization"] = {"group_size": args.group_size, "bits": args.bits, "nar_bits": nar_bits, "mode": "affine"}
96
+ print(f"[write] model.safetensors ({len(weights)} tensors)", flush=True)
97
+ mx.save_safetensors(str(args.output / "model.safetensors"), weights, metadata={"format": "mlx"})
98
+ (args.output / "config.json").write_text(json.dumps(cfg, indent=2) + "\n")
99
+
100
+ vae = fold_weight_norm(mx.load(str(vae_dir / "model.safetensors")))
101
+ print(f"[write] vae.safetensors ({len(vae)} tensors)", flush=True)
102
+ mx.save_safetensors(str(args.output / "vae.safetensors"), vae, metadata={"format": "mlx"})
103
+ shutil.copy2(vae_dir / "config.json", args.output / "vae_config.json")
104
+ for name in ("qwen.tiktoken", "yue2_generation_config.json"):
105
+ shutil.copy2(model_dir / name, args.output / name)
106
+ print(f"[done] {args.output}")
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
generate.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """YuE2 song generation on Apple Silicon with MLX.
3
+
4
+ python generate.py --model ~/Desktop/Yue2-3B-MLX --style "indie pop, warm vocal" \
5
+ --lyrics-file lyrics.txt --cot off --seed 831001 --out song.wav
6
+
7
+ Ports the yue2_infer protocol: symbolic ABC planning (cot=melody/full), semantic
8
+ codec AR decode with CFG, 32-step midpoint flow matching, tiled VAE decode.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import base64
14
+ import json
15
+ import math
16
+ import sys
17
+ import time
18
+ import unicodedata
19
+ import wave
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+
23
+ import mlx.core as mx
24
+ import numpy as np
25
+
26
+ from yue2_model import KVCache, Yue2Model, load_model
27
+ from yue2_vae import load_vae
28
+
29
+ EOD = 151643
30
+ ABC_START, ABC_END = 151847, 151848
31
+ MUSIC_START, MUSIC_END = 151851, 151852
32
+ CODEC_OFFSET, CODEC_SIZE = 151853, 32768
33
+ VOCAB, CONTEXT = 184704, 24576
34
+ SAMPLE_RATE = 48000
35
+ INSTRUCTIONS = {
36
+ "off": "Generate music with codec tokens from the given conditions.",
37
+ "melody": "Generate a melody-only ABC transcription without chord symbols, then generate music with codec tokens from the given conditions.",
38
+ "full": "Generate a chord-annotated ABC transcription, then generate music with codec tokens from the given conditions.",
39
+ }
40
+
41
+
42
+ class Tokenizer:
43
+ """The frozen Qwen text/ABC BPE (not the audio codec)."""
44
+
45
+ def __init__(self, merge_file: Path):
46
+ import tiktoken
47
+ ranks = {base64.b64decode(t): int(r) for t, r in
48
+ (line.split() for line in Path(merge_file).read_bytes().splitlines() if line)}
49
+ specials = ["<|endoftext|>", "<|im_start|>", "<|im_end|>", "<R>", "<S>", "<X>", "<mask>", "<sep>"]
50
+ specials += [f"<extra_{i}>" for i in range(200)]
51
+ specials[204:206] = ["<abc>", "</abc>"]
52
+ pattern = r"(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+"
53
+ self._enc = tiktoken.Encoding("YuE2", pat_str=pattern, mergeable_ranks=ranks,
54
+ special_tokens={s: i + len(ranks) for i, s in enumerate(specials)})
55
+
56
+ def encode(self, text):
57
+ return self._enc.encode_ordinary(unicodedata.normalize("NFC", text))
58
+
59
+ def decode(self, ids):
60
+ return self._enc.decode([int(i) for i in ids if 0 <= i < self._enc.n_vocab], errors="replace")
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class Sampling:
65
+ temperature: float = 1.0
66
+ top_p: float = 0.95
67
+ top_k: int = 100
68
+ repetition_penalty: float = 1.2
69
+ penalty_window: int = 50
70
+ min_tokens: int = 200
71
+ max_tokens: int = 9000
72
+
73
+
74
+ def request_text(style, lyrics, cot):
75
+ return f"{INSTRUCTIONS[cot]}\n[Tags]\n{style}\n[Lyrics]\n{lyrics}\n"
76
+
77
+
78
+ def token_prefix(tok, style, lyrics, cot, abc_ids=None):
79
+ base = [EOD] + tok.encode(request_text(style, lyrics, cot))
80
+ if cot == "off":
81
+ return base + [ABC_START, ABC_END, MUSIC_START]
82
+ if abc_ids is None:
83
+ return base + [ABC_START]
84
+ return base + [ABC_START] + list(abc_ids) + [ABC_END, MUSIC_START]
85
+
86
+
87
+ def negative_prefix(tok, cot, abc_ids):
88
+ base = [EOD] + tok.encode(INSTRUCTIONS[cot])
89
+ if cot == "off":
90
+ return base + [MUSIC_START]
91
+ return base + [ABC_START] + list(abc_ids) + [ABC_END, MUSIC_START]
92
+
93
+
94
+ # ── AR sampling ──────────────────────────────────────────────────────────────
95
+
96
+ def allowed_mask(phase, dtype):
97
+ idx = mx.arange(VOCAB)
98
+ end = ABC_END if phase == "abc" else MUSIC_END
99
+ ok = (idx < EOD) if phase == "abc" else ((idx >= CODEC_OFFSET) & (idx < CODEC_OFFSET + CODEC_SIZE))
100
+ return mx.where(ok | (idx == end), 0.0, -mx.inf).astype(dtype)
101
+
102
+
103
+ def distribution(logits, s: Sampling, history, step, phase, allowed, legacy_off):
104
+ # Historical cot=off arithmetic stays in BF16; symbolic/CFG paths upcast.
105
+ scores = logits if legacy_off else logits.astype(mx.float32)
106
+ end = ABC_END if phase == "abc" else MUSIC_END
107
+ scores = scores + allowed
108
+ if step < s.min_tokens:
109
+ scores[end] = -mx.inf
110
+ recent = history[-s.penalty_window:]
111
+ if s.repetition_penalty != 1.0 and recent:
112
+ freq = mx.zeros((VOCAB,), scores.dtype).at[mx.array(recent)].add(1)
113
+ alpha = mx.power(mx.array(s.repetition_penalty, scores.dtype), freq)
114
+ scores = mx.where(scores < 0, scores * alpha, scores / alpha)
115
+ if s.temperature == 0:
116
+ return scores
117
+ if s.temperature != 1:
118
+ scores = scores / s.temperature
119
+ kth = VOCAB - min(s.top_k, VOCAB)
120
+ threshold = mx.partition(scores, kth)[kth]
121
+ scores = mx.where(scores < threshold, -mx.inf, scores)
122
+ if s.top_p < 1:
123
+ order = mx.argsort(-scores)
124
+ values = scores[order]
125
+ probs = mx.softmax(values.astype(mx.float32), axis=-1)
126
+ removed = (mx.cumsum(probs) - probs) > s.top_p
127
+ removed = removed & (mx.arange(VOCAB) >= (3 if legacy_off else 1))
128
+ scores = mx.put_along_axis(scores, order, mx.where(removed, -mx.inf, values), axis=0)
129
+ return scores
130
+
131
+
132
+ def generate_tokens(model: Yue2Model, prefix, s: Sampling, seed, phase, negative=None, cfg_scale=1.0,
133
+ legacy_off=False, on_token=None):
134
+ """Return (ids, truncated). Two KV caches when CFG is active (cond / uncond)."""
135
+ if len(prefix) + s.max_tokens > CONTEXT or (negative and len(negative) + s.max_tokens > CONTEXT):
136
+ raise ValueError("Prefix + generation budget exceeds the 24576 context")
137
+ if cfg_scale != 1 and negative is None:
138
+ raise ValueError("CFG requires a negative prefix")
139
+ n_layers = len(model.model.layers)
140
+ caches = [[KVCache() for _ in range(n_layers)] for _ in range(2 if cfg_scale != 1 else 1)]
141
+ cond = model.ar_step(mx.array([prefix]), caches[0])
142
+ uncond = model.ar_step(mx.array([negative]), caches[1]) if cfg_scale != 1 else None
143
+ end = ABC_END if phase == "abc" else MUSIC_END
144
+ allowed = allowed_mask(phase, cond.dtype if legacy_off else mx.float32)
145
+ key = mx.random.key(seed)
146
+ history, eos = [], False
147
+ for step in range(s.max_tokens):
148
+ logits = cond if cfg_scale == 1 else uncond + cfg_scale * (cond - uncond)
149
+ scores = distribution(logits[0], s, history, step, phase, allowed, legacy_off)
150
+ if s.temperature == 0:
151
+ token = int(mx.argmax(scores).item())
152
+ else:
153
+ key, sub = mx.random.split(key)
154
+ token = int(mx.random.categorical(scores.astype(mx.float32), key=sub).item())
155
+ if on_token is not None:
156
+ on_token(phase, token)
157
+ if token == end:
158
+ eos = True
159
+ break
160
+ history.append(token)
161
+ if step + 1 < s.max_tokens:
162
+ nxt = mx.array([[token]])
163
+ cond = model.ar_step(nxt, caches[0])
164
+ if uncond is not None:
165
+ uncond = model.ar_step(nxt, caches[1])
166
+ return history, not eos
167
+
168
+
169
+ # ── NAR flow matching ────────────────────────────────────────────────────────
170
+
171
+ def chunk_ranges(frames, prefix_tokens, context=CONTEXT):
172
+ size = min((context - prefix_tokens - 3) // 2, CONTEXT)
173
+ if frames < 1 or size < 1:
174
+ raise ValueError("Empty codec or prefix leaves no acoustic context")
175
+ return [(a, min(a + size, frames)) for a in range(0, frames, size)]
176
+
177
+
178
+ def _logit(t):
179
+ return max(-20.0, min(20.0, math.log(t / (1 - t)))) if 0 < t < 1 else (20.0 if t >= 1 else -20.0)
180
+
181
+
182
+ def synthesize(model: Yue2Model, prefix, codec, seed, steps=32, noise=None, on_progress=None):
183
+ """Return latents [frames,64] float32 via midpoint ODE from t=1 → 0, solved per context chunk."""
184
+ if noise is None:
185
+ noise = mx.random.normal((len(codec), 64), key=mx.random.key(seed))
186
+ dt = 1.0 / steps
187
+ out = []
188
+ for a, b in chunk_ranges(len(codec), len(prefix)):
189
+ ar_tokens = prefix + [c + CODEC_OFFSET for c in codec[a:b]] + [MUSIC_END]
190
+ ar_cache = model.nar_prefill(ar_tokens)
191
+ state = noise[a:b].astype(mx.bfloat16)
192
+ for step in range(steps):
193
+ t = 1.0 - step * dt
194
+ v1 = model.nar_velocity(state, _logit(t), ar_cache, len(ar_tokens))
195
+ mid = state - v1 * (dt / 2)
196
+ state = state - model.nar_velocity(mid, _logit(t - dt / 2), ar_cache, len(ar_tokens)) * dt
197
+ mx.eval(state)
198
+ if on_progress is not None:
199
+ on_progress(step + 1, steps)
200
+ out.append(state.astype(mx.float32))
201
+ return mx.concatenate(out)
202
+
203
+
204
+ # ── Pipeline ─────────────────────────────────────────────────────────────────
205
+
206
+ class Yue2Pipeline:
207
+ def __init__(self, path, log=print):
208
+ self.path, self.log = Path(path), log
209
+ gen = json.loads((self.path / "yue2_generation_config.json").read_text())
210
+ self.abc_sampling, self.semantic_sampling = Sampling(**gen["abc"]), Sampling(**gen["semantic"])
211
+ self.ode_steps = gen.get("ode_steps", 32)
212
+ self.tokenizer = Tokenizer(self.path / "qwen.tiktoken")
213
+ self.vae = load_vae(self.path) # small; load first so a bad file fails before minutes of AR work
214
+ self.model = load_model(self.path)
215
+
216
+ def _progress(self, label):
217
+ count, start = [0], time.perf_counter()
218
+
219
+ def on_token(_phase, _token):
220
+ count[0] += 1
221
+ if count[0] % 200 == 0:
222
+ self.log(f"[{label}] {count[0]} tokens, {count[0] / (time.perf_counter() - start):.1f} tok/s")
223
+ return on_token
224
+
225
+ def __call__(self, style, lyrics, cot="full", seed=831001, abc=None, cfg_scale=None,
226
+ semantic_sampling=None, steps=None, on_latents=None):
227
+ if cot not in INSTRUCTIONS:
228
+ raise ValueError("cot must be off, melody or full")
229
+ if abc is not None and cot == "off":
230
+ raise ValueError("External ABC requires cot=melody/full")
231
+ tok = self.tokenizer
232
+ # 1. symbolic plan
233
+ abc_ids, abc_text = [], None
234
+ if cot != "off":
235
+ if abc is not None:
236
+ abc_ids, abc_text = tok.encode(abc), abc
237
+ else:
238
+ self.log("[plan] generating ABC score")
239
+ abc_ids, truncated = generate_tokens(self.model, token_prefix(tok, style, lyrics, cot),
240
+ self.abc_sampling, seed, "abc", on_token=self._progress("abc"))
241
+ abc_text = tok.decode(abc_ids)
242
+ if truncated:
243
+ self.log("[plan] ABC hit max_tokens")
244
+ prefix = token_prefix(tok, style, lyrics, cot, abc_ids)
245
+ # 2. semantic codec tokens
246
+ guidance = (1.01 if cot == "off" else 1.0) if cfg_scale is None else cfg_scale
247
+ negative = negative_prefix(tok, cot, abc_ids) if guidance != 1 else None
248
+ self.log(f"[semantic] prefix {len(prefix)} tokens, cfg {guidance}")
249
+ ids, truncated = generate_tokens(self.model, prefix, semantic_sampling or self.semantic_sampling, seed,
250
+ "semantic", negative, guidance, legacy_off=cot == "off",
251
+ on_token=self._progress("semantic"))
252
+ if truncated:
253
+ self.log("[semantic] hit max_tokens")
254
+ codec = [t - CODEC_OFFSET for t in ids]
255
+ if not codec:
256
+ raise RuntimeError("Semantic stage produced no codec tokens")
257
+ # 3. acoustic latents
258
+ self.log(f"[nar] {len(codec)} frames ({len(codec) * 1920 / SAMPLE_RATE:.1f}s), {steps or self.ode_steps} midpoint steps")
259
+ latents = synthesize(self.model, prefix, codec, seed, steps or self.ode_steps,
260
+ on_progress=lambda i, n: i % 8 == 0 and self.log(f"[nar] step {i}/{n}"))
261
+ if on_latents is not None:
262
+ on_latents(latents)
263
+ # 4. waveform
264
+ self.log("[vae] decoding")
265
+ return self.decode(latents), {"abc": abc_text, "codec": codec, "latents": latents, "prefix": prefix}
266
+
267
+ def decode(self, latents):
268
+ return mx.clip(self.vae.decode_tiled(latents), -1, 1)
269
+
270
+
271
+ def write_wav(path, audio, sample_rate=SAMPLE_RATE):
272
+ pcm = (np.clip(np.array(audio, dtype=np.float32), -1, 1) * 32767).astype("<i2")
273
+ with wave.open(str(path), "wb") as f:
274
+ f.setnchannels(pcm.shape[1])
275
+ f.setsampwidth(2)
276
+ f.setframerate(sample_rate)
277
+ f.writeframes(pcm.tobytes())
278
+
279
+
280
+ def main():
281
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
282
+ parser.add_argument("--model", type=Path, required=True, help="MLX package dir from convert.py")
283
+ parser.add_argument("--style", required=True)
284
+ group = parser.add_mutually_exclusive_group(required=True)
285
+ group.add_argument("--lyrics")
286
+ group.add_argument("--lyrics-file", type=Path)
287
+ parser.add_argument("--cot", default="full", choices=list(INSTRUCTIONS))
288
+ parser.add_argument("--abc-file", type=Path, help="external ABC score (cot=melody/full)")
289
+ parser.add_argument("--seed", type=int, default=831001)
290
+ parser.add_argument("--cfg-scale", type=float)
291
+ parser.add_argument("--steps", type=int, help="NAR midpoint steps (default from generation config)")
292
+ parser.add_argument("--max-semantic-tokens", type=int, help="cap song length in codec frames (40 ms each)")
293
+ parser.add_argument("--out", type=Path, default=Path("yue2.wav"))
294
+ parser.add_argument("--decode-latents", type=Path, help="skip generation; decode a saved <out>.latents.npy")
295
+ args = parser.parse_args()
296
+
297
+ lyrics = args.lyrics if args.lyrics is not None else args.lyrics_file.read_text()
298
+ abc = args.abc_file.read_text() if args.abc_file else None
299
+ log = lambda msg: print(msg, file=sys.stderr, flush=True)
300
+ pipe = Yue2Pipeline(args.model, log=log)
301
+ sampling = None
302
+ if args.max_semantic_tokens:
303
+ sampling = Sampling(**{**pipe.semantic_sampling.__dict__, "max_tokens": args.max_semantic_tokens,
304
+ "min_tokens": min(pipe.semantic_sampling.min_tokens, args.max_semantic_tokens)})
305
+ start = time.perf_counter()
306
+ latents_path = args.out.with_suffix(".latents.npy")
307
+ if args.decode_latents:
308
+ audio, info = pipe.decode(mx.array(np.load(args.decode_latents))), {"abc": None}
309
+ else:
310
+ audio, info = pipe(args.style, lyrics, cot=args.cot, seed=args.seed, abc=abc, cfg_scale=args.cfg_scale,
311
+ semantic_sampling=sampling, steps=args.steps,
312
+ on_latents=lambda z: np.save(latents_path, np.array(z)))
313
+ write_wav(args.out, audio)
314
+ if info["abc"] is not None:
315
+ args.out.with_suffix(".abc").write_text(info["abc"])
316
+ log(f"[done] {args.out} {audio.shape[0] / SAMPLE_RATE:.1f}s in {time.perf_counter() - start:.0f}s")
317
+
318
+
319
+ if __name__ == "__main__":
320
+ main()
test_parity.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Check the MLX port against the torch reference (needs torch, transformers, the HF snapshots).
3
+
4
+ python test_parity.py --mlx ~/Desktop/Yue2-3B-MLX
5
+
6
+ Compares AR logits, NAR velocity and VAE decode on tiny inputs. Exits non-zero on mismatch.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ from pathlib import Path
12
+
13
+ import mlx.core as mx
14
+ import numpy as np
15
+ import torch
16
+
17
+ from generate import CODEC_OFFSET, MUSIC_END, Tokenizer, token_prefix
18
+ from yue2_model import KVCache, load_model
19
+ from yue2_vae import load_vae
20
+
21
+ LATENT_START, LATENT_END, LATENT_PAD = 184621, 184622, 184623
22
+
23
+
24
+ def snapshot(repo):
25
+ from huggingface_hub import snapshot_download
26
+ return Path(snapshot_download(repo, allow_patterns=["*.json", "*.py", "model.safetensors", "qwen.tiktoken"]))
27
+
28
+
29
+ def rel_err(a, b):
30
+ a, b = np.asarray(a, np.float32), np.asarray(b, np.float32)
31
+ return float(np.abs(a - b).max() / (np.abs(b).max() + 1e-6))
32
+
33
+
34
+ def main():
35
+ parser = argparse.ArgumentParser()
36
+ parser.add_argument("--mlx", type=Path, default=Path.home() / "Desktop" / "Yue2-3B-MLX")
37
+ parser.add_argument("--ref-model", default="m-a-p/YuE2-3B")
38
+ parser.add_argument("--ref-vae", default="m-a-p/YuE2-Vae")
39
+ args = parser.parse_args()
40
+ from transformers import AutoModel, AutoModelForCausalLM
41
+
42
+ ref_dir = snapshot(args.ref_model) if not Path(args.ref_model).is_dir() else Path(args.ref_model)
43
+ vae_dir = snapshot(args.ref_vae) if not Path(args.ref_vae).is_dir() else Path(args.ref_vae)
44
+ tok = Tokenizer(args.mlx / "qwen.tiktoken")
45
+ prefix = token_prefix(tok, "pop, warm vocal", "[Verse]\nHello world\n", "off")
46
+ codec = [7, 300, 5000, 12345, 32000, 9, 9, 42]
47
+ ar_tokens = prefix + [c + CODEC_OFFSET for c in codec] + [MUSIC_END]
48
+ frames = len(codec)
49
+ rng = np.random.default_rng(0)
50
+ state = rng.standard_normal((frames, 64)).astype(np.float32)
51
+ raw_t = 0.3
52
+
53
+ # ── torch reference ──
54
+ ref = AutoModelForCausalLM.from_pretrained(ref_dir, trust_remote_code=True, torch_dtype=torch.bfloat16,
55
+ local_files_only=True).eval()
56
+ with torch.inference_mode():
57
+ ref_logits = ref(torch.tensor([ar_tokens]), use_cache=False).logits[0].float().numpy()
58
+ nar = [LATENT_START] + [LATENT_PAD] * frames + [LATENT_END]
59
+ tokens = torch.tensor([ar_tokens + nar])
60
+ ar_mask = torch.tensor([[True] * len(ar_tokens) + [False] * len(nar)])
61
+ nar_mask = ~ar_mask
62
+ content = torch.tensor([[False] * (len(ar_tokens) + 1) + [True] * frames + [False]])
63
+ ref_v = ref.nar_velocity(tokens, ar_mask, nar_mask, content, torch.tensor(state), raw_t).float().numpy()
64
+ del ref
65
+ ref_vae = AutoModel.from_pretrained(vae_dir, trust_remote_code=True, decoder_only=True, local_files_only=True).eval()
66
+ z = rng.standard_normal((1, 64, 40)).astype(np.float32)
67
+ with torch.inference_mode():
68
+ ref_audio = ref_vae.decode(torch.tensor(z))[0].T.numpy() # [L,2]
69
+ ref_tiled = ref_vae.decode_tiled(torch.tensor(z), core_frames=16, halo_frames=16)[0].T.numpy()
70
+ del ref_vae
71
+
72
+ # ── mlx ──
73
+ model = load_model(args.mlx)
74
+ caches = [KVCache() for _ in model.model.layers]
75
+ mlx_logits = np.array(model.ar_step(mx.array([ar_tokens]), caches, all_positions=True)[0].astype(mx.float32))
76
+ # incremental decode must equal the prefill logits for the same positions
77
+ caches2 = [KVCache() for _ in model.model.layers]
78
+ model.ar_step(mx.array([ar_tokens[:-3]]), caches2)
79
+ step_logits = [np.array(model.ar_step(mx.array([[t]]), caches2).astype(mx.float32))[0] for t in ar_tokens[-3:]]
80
+ ar_cache = model.nar_prefill(ar_tokens)
81
+ mlx_v = np.array(model.nar_velocity(mx.array(state).astype(mx.bfloat16), raw_t, ar_cache, len(ar_tokens)).astype(mx.float32))
82
+ vae = load_vae(args.mlx)
83
+ zc = mx.array(z[0].T) # [T,64]
84
+ mlx_audio = np.array(vae(zc[None])[0])
85
+ mlx_tiled = np.array(vae.decode_tiled(zc, core=16, halo=16))
86
+
87
+ checks = {
88
+ "ar_logits_rel_err": rel_err(mlx_logits, ref_logits),
89
+ "ar_argmax_agree": float((mlx_logits.argmax(-1) == ref_logits.argmax(-1)).mean()),
90
+ "ar_incremental_rel_err": rel_err(np.stack(step_logits), mlx_logits[-3:]),
91
+ "nar_velocity_rel_err": rel_err(mlx_v, ref_v),
92
+ "vae_rel_err": rel_err(mlx_audio, ref_audio),
93
+ "vae_tiled_rel_err": rel_err(mlx_tiled, ref_tiled),
94
+ "vae_len_ok": float(mlx_audio.shape == ref_audio.shape and mlx_tiled.shape == ref_tiled.shape),
95
+ }
96
+ for k, v in checks.items():
97
+ print(f"{k:26s} {v:.5f}")
98
+ ok = (checks["ar_logits_rel_err"] < 0.05 and checks["ar_argmax_agree"] > 0.9
99
+ and checks["ar_incremental_rel_err"] < 0.05 and checks["nar_velocity_rel_err"] < 0.05
100
+ and checks["vae_rel_err"] < 1e-3 and checks["vae_tiled_rel_err"] < 1e-3 and checks["vae_len_ok"])
101
+ print("PASS" if ok else "FAIL")
102
+ raise SystemExit(0 if ok else 1)
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
yue2_model.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """YuE2 AR/NAR Mixture-of-Transformers backbone in MLX.
2
+
3
+ Mirrors m-a-p/YuE2-3B ``modeling_yue2.py``: a Qwen3-style decoder whose every
4
+ layer carries two attention/MLP paths (AR for text+codec tokens, NAR for the
5
+ acoustic flow-matching state). Weight names follow the checkpoint except
6
+ ``time_embedder.mlp.{0,2}`` which ``convert.py`` renames to ``fc1``/``fc2``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import math
12
+ from pathlib import Path
13
+
14
+ import mlx.core as mx
15
+ import mlx.nn as nn
16
+
17
+
18
+ class KVCache:
19
+ """Append-only per-layer cache, grown in 256-slot steps (mlx_lm pattern)."""
20
+
21
+ step = 256
22
+
23
+ def __init__(self):
24
+ self.keys = self.values = None
25
+ self.offset = 0
26
+
27
+ def update(self, k, v):
28
+ prev = self.offset
29
+ B, H, T, D = k.shape
30
+ if self.keys is None or prev + T > self.keys.shape[2]:
31
+ n = ((T + self.step - 1) // self.step) * self.step
32
+ new_k, new_v = mx.zeros((B, H, n, D), k.dtype), mx.zeros((B, H, n, D), v.dtype)
33
+ if self.keys is None:
34
+ self.keys, self.values = new_k, new_v
35
+ else:
36
+ if prev % self.step:
37
+ self.keys, self.values = self.keys[..., :prev, :], self.values[..., :prev, :]
38
+ self.keys = mx.concatenate([self.keys, new_k], axis=2)
39
+ self.values = mx.concatenate([self.values, new_v], axis=2)
40
+ self.offset += T
41
+ self.keys[..., prev:self.offset, :] = k
42
+ self.values[..., prev:self.offset, :] = v
43
+ return self.keys[..., :self.offset, :], self.values[..., :self.offset, :]
44
+
45
+
46
+ class Attention(nn.Module):
47
+ def __init__(self, cfg):
48
+ super().__init__()
49
+ self.n_heads, self.n_kv, self.head_dim = cfg["num_attention_heads"], cfg["num_key_value_heads"], cfg["head_dim"]
50
+ h, d = cfg["hidden_size"], self.head_dim
51
+ self.q_proj = nn.Linear(h, self.n_heads * d, bias=False)
52
+ self.k_proj = nn.Linear(h, self.n_kv * d, bias=False)
53
+ self.v_proj = nn.Linear(h, self.n_kv * d, bias=False)
54
+ self.o_proj = nn.Linear(self.n_heads * d, h, bias=False)
55
+ self.q_norm = nn.RMSNorm(d, eps=cfg["rms_norm_eps"])
56
+ self.k_norm = nn.RMSNorm(d, eps=cfg["rms_norm_eps"])
57
+ self.rope_base = float(cfg["rope_theta"])
58
+ self.scale = d ** -0.5
59
+
60
+ def project_qkv(self, x, offset):
61
+ """Return q [B,H,T,D], k/v [B,KV,T,D] after QK-norm and RoPE at ``offset``."""
62
+ B, T, _ = x.shape
63
+ q = self.q_norm(self.q_proj(x).reshape(B, T, self.n_heads, -1)).transpose(0, 2, 1, 3)
64
+ k = self.k_norm(self.k_proj(x).reshape(B, T, self.n_kv, -1)).transpose(0, 2, 1, 3)
65
+ v = self.v_proj(x).reshape(B, T, self.n_kv, -1).transpose(0, 2, 1, 3)
66
+ rope = lambda a: mx.fast.rope(a, self.head_dim, traditional=False, base=self.rope_base, scale=1.0, offset=offset)
67
+ return rope(q), rope(k), v
68
+
69
+ def attend(self, q, k, v, mask):
70
+ B, _, T, _ = q.shape
71
+ o = mx.fast.scaled_dot_product_attention(q, k, v, scale=self.scale, mask=mask)
72
+ return self.o_proj(o.transpose(0, 2, 1, 3).reshape(B, T, -1))
73
+
74
+
75
+ class MLP(nn.Module):
76
+ def __init__(self, cfg):
77
+ super().__init__()
78
+ h, i = cfg["hidden_size"], cfg["intermediate_size"]
79
+ self.gate_proj = nn.Linear(h, i, bias=False)
80
+ self.up_proj = nn.Linear(h, i, bias=False)
81
+ self.down_proj = nn.Linear(i, h, bias=False)
82
+
83
+ def __call__(self, x):
84
+ return self.down_proj(nn.silu(self.gate_proj(x)) * self.up_proj(x))
85
+
86
+
87
+ class DecoderLayer(nn.Module):
88
+ def __init__(self, cfg):
89
+ super().__init__()
90
+ h, eps = cfg["hidden_size"], cfg["rms_norm_eps"]
91
+ self.input_layernorm = nn.RMSNorm(h, eps=eps)
92
+ self.self_attn = Attention(cfg)
93
+ self.post_attention_layernorm = nn.RMSNorm(h, eps=eps)
94
+ self.mlp = MLP(cfg)
95
+ self.nar_input_layernorm = nn.RMSNorm(h, eps=eps)
96
+ self.nar_self_attn = Attention(cfg)
97
+ self.nar_pre_mlp_layernorm = nn.RMSNorm(h, eps=eps)
98
+ self.nar_mlp = MLP(cfg)
99
+
100
+ def ar(self, x, cache: KVCache, mask):
101
+ q, k, v = self.self_attn.project_qkv(self.input_layernorm(x), cache.offset)
102
+ k, v = cache.update(k, v)
103
+ x = x + self.self_attn.attend(q, k, v, mask)
104
+ return x + self.mlp(self.post_attention_layernorm(x))
105
+
106
+ def ar_prefill_kv(self, x):
107
+ """AR pass that also returns this layer's (k, v) for the NAR to attend over."""
108
+ q, k, v = self.self_attn.project_qkv(self.input_layernorm(x), 0)
109
+ x = x + self.self_attn.attend(q, k, v, "causal")
110
+ return x + self.mlp(self.post_attention_layernorm(x)), (k, v)
111
+
112
+ def nar(self, x, ar_k, ar_v, offset):
113
+ q, k, v = self.nar_self_attn.project_qkv(self.nar_input_layernorm(x), offset)
114
+ k, v = mx.concatenate([ar_k, k], axis=2), mx.concatenate([ar_v, v], axis=2)
115
+ x = x + self.nar_self_attn.attend(q, k, v, None)
116
+ return x + self.nar_mlp(self.nar_pre_mlp_layernorm(x))
117
+
118
+
119
+ class Backbone(nn.Module):
120
+ def __init__(self, cfg):
121
+ super().__init__()
122
+ self.embed_tokens = nn.Embedding(cfg["vocab_size"], cfg["hidden_size"])
123
+ self.layers = [DecoderLayer(cfg) for _ in range(cfg["num_hidden_layers"])]
124
+ self.norm = nn.RMSNorm(cfg["hidden_size"], eps=cfg["rms_norm_eps"])
125
+
126
+
127
+ class TimestepEmbedder(nn.Module):
128
+ def __init__(self, hidden, freq_size=256):
129
+ super().__init__()
130
+ self.freq_size = freq_size
131
+ self.fc1 = nn.Linear(freq_size, hidden)
132
+ self.fc2 = nn.Linear(hidden, hidden)
133
+
134
+ def __call__(self, t, dtype):
135
+ half = self.freq_size // 2
136
+ freqs = mx.exp(-math.log(10000) * mx.arange(half, dtype=mx.float32) / half)
137
+ args = t.astype(mx.float32).reshape(-1, 1) * freqs[None]
138
+ emb = mx.concatenate([mx.cos(args), mx.sin(args)], axis=-1).astype(dtype)
139
+ return self.fc2(nn.silu(self.fc1(emb)))
140
+
141
+
142
+ class LatentPosEmbed(nn.Module):
143
+ """Holds the checkpoint's precomputed sinusoidal table ``pe`` [max_frames, hidden]."""
144
+
145
+ def __init__(self, max_frames, hidden):
146
+ super().__init__()
147
+ self.pe = mx.zeros((max_frames, hidden))
148
+
149
+
150
+ class Yue2Model(nn.Module):
151
+ def __init__(self, cfg):
152
+ super().__init__()
153
+ self.config = cfg
154
+ h, d = cfg["hidden_size"], cfg["latent_dim"]
155
+ self.model = Backbone(cfg)
156
+ self.lm_head = nn.Linear(h, cfg["vocab_size"], bias=False)
157
+ self.llm2vae = nn.Linear(h, d)
158
+ self.vae2llm = nn.Linear(d, h)
159
+ self.time_embedder = TimestepEmbedder(h)
160
+ self.latent_pos_embed = LatentPosEmbed(cfg["max_latent_frames"], h)
161
+
162
+ # AR path
163
+ def ar_step(self, tokens, caches, all_positions=False):
164
+ """tokens [B,T] → logits [B,V] of the last position (or [B,T,V])."""
165
+ x = self.model.embed_tokens(tokens)
166
+ mask = "causal" if tokens.shape[1] > 1 else None
167
+ for layer, cache in zip(self.model.layers, caches):
168
+ x = layer.ar(x, cache, mask)
169
+ x = self.model.norm(x if all_positions else x[:, -1:])
170
+ logits = self.lm_head(x)
171
+ return logits if all_positions else logits[:, -1]
172
+
173
+ # NAR path
174
+ def nar_prefill(self, ar_tokens):
175
+ """Run the AR path once over the chunk prefix; return per-layer (k, v)."""
176
+ x = self.model.embed_tokens(mx.array([ar_tokens]))
177
+ cache = []
178
+ for layer in self.model.layers:
179
+ x, kv = layer.ar_prefill_kv(x)
180
+ cache.append(kv)
181
+ mx.eval(cache)
182
+ return cache
183
+
184
+ def nar_velocity(self, state, raw_t, ar_cache, ar_length):
185
+ """Flow-matching velocity for ``state`` [T,64] at logit-time ``raw_t``."""
186
+ shift = self.config["timestep_shift"]
187
+ sig = mx.sigmoid(mx.array(raw_t, dtype=state.dtype))
188
+ t = shift * sig / (1 + (shift - 1) * sig)
189
+ n = state.shape[0] + 2 # LATENT_START + frames + LATENT_END
190
+ x = self.vae2llm(mx.pad(state, [(1, 1), (0, 0)])[None])
191
+ x = x + self.time_embedder(t, state.dtype)[None]
192
+ pos = mx.minimum(mx.arange(n), self.config["max_latent_frames"] - 1)
193
+ x = x + self.latent_pos_embed.pe[pos][None]
194
+ for layer, (k, v) in zip(self.model.layers, ar_cache):
195
+ x = layer.nar(x, k, v, ar_length)
196
+ return self.llm2vae(self.model.norm(x))[0, 1:-1]
197
+
198
+
199
+ def load_model(path: Path) -> Yue2Model:
200
+ path = Path(path)
201
+ cfg = json.loads((path / "config.json").read_text())
202
+ model = Yue2Model(cfg)
203
+ weights = mx.load(str(path / "model.safetensors"))
204
+ q = cfg.get("quantization")
205
+ if q:
206
+ nar_bits = q.get("nar_bits", q["bits"])
207
+
208
+ def predicate(path, module):
209
+ if not isinstance(module, nn.Linear) or f"{path}.scales" not in weights:
210
+ return False
211
+ return {"group_size": q["group_size"], "bits": nar_bits if ".nar_" in path else q["bits"]}
212
+
213
+ nn.quantize(model, q["group_size"], q["bits"], class_predicate=predicate)
214
+ model.load_weights(list(weights.items()))
215
+ mx.eval(model.parameters())
216
+ return model
yue2_vae.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """YuE2 Oobleck VAE decoder in MLX (channels-last; weight norm folded by convert.py)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+
8
+ import mlx.core as mx
9
+ import mlx.nn as nn
10
+
11
+ FRAME = 1920 # samples per latent frame at 48 kHz
12
+
13
+
14
+ class Snake(nn.Module):
15
+ def __init__(self, ch):
16
+ super().__init__()
17
+ self.alpha = mx.zeros((ch,))
18
+ self.beta = mx.zeros((ch,))
19
+
20
+ def __call__(self, x):
21
+ alpha, beta = mx.exp(self.alpha), mx.exp(self.beta)
22
+ return x + (1.0 / (beta + 1e-9)) * mx.sin(x * alpha) ** 2
23
+
24
+
25
+ class ResidualUnit(nn.Module):
26
+ def __init__(self, ch, dilation):
27
+ super().__init__()
28
+ self.layers = [Snake(ch), nn.Conv1d(ch, ch, 7, dilation=dilation, padding=3 * dilation),
29
+ Snake(ch), nn.Conv1d(ch, ch, 1)]
30
+
31
+ def __call__(self, x):
32
+ y = x
33
+ for layer in self.layers:
34
+ y = layer(y)
35
+ return x + y
36
+
37
+
38
+ class DecoderBlock(nn.Module):
39
+ def __init__(self, cin, cout, stride):
40
+ super().__init__()
41
+ self.layers = [Snake(cin),
42
+ nn.ConvTranspose1d(cin, cout, 2 * stride, stride=stride, padding=math.ceil(stride / 2)),
43
+ ResidualUnit(cout, 1), ResidualUnit(cout, 3), ResidualUnit(cout, 9)]
44
+
45
+ def __call__(self, x):
46
+ for layer in self.layers:
47
+ x = layer(x)
48
+ return x
49
+
50
+
51
+ class OobleckDecoder(nn.Module):
52
+ def __init__(self, cfg):
53
+ super().__init__()
54
+ c, mults, strides = cfg["channels"], [1] + list(cfg["c_mults"]), cfg["strides"]
55
+ layers = [nn.Conv1d(cfg["latent_dim"], mults[-1] * c, 7, padding=3)]
56
+ for i in range(len(mults) - 1, 0, -1):
57
+ layers.append(DecoderBlock(mults[i] * c, mults[i - 1] * c, strides[i - 1]))
58
+ layers += [Snake(mults[0] * c), nn.Conv1d(mults[0] * c, cfg["out_channels"], 7, padding=3, bias=False)]
59
+ self.layers = layers
60
+
61
+ def __call__(self, x):
62
+ for layer in self.layers:
63
+ x = layer(x)
64
+ return x
65
+
66
+ def decode_tiled(self, z, core=1024, halo=16):
67
+ """z [T,64] float32 → audio [1920*T-64, 2] float32, exact tile cores, no crossfade."""
68
+ frames, total = z.shape[0], FRAME * z.shape[0] - 64
69
+ out = []
70
+ for start in range(0, frames, core):
71
+ end = min(frames, start + core)
72
+ left, right = max(0, start - halo), min(frames, end + halo)
73
+ tile = self(z[None, left:right])[0]
74
+ crop = (start - left) * FRAME
75
+ out.append(tile[crop:crop + min(end * FRAME, total) - start * FRAME])
76
+ mx.eval(out[-1])
77
+ return mx.concatenate(out)
78
+
79
+
80
+ def load_vae(path: Path) -> OobleckDecoder:
81
+ path = Path(path)
82
+ cfg = json.loads((path / "vae_config.json").read_text())
83
+ decoder = OobleckDecoder(cfg["decoder_config"])
84
+ decoder.load_weights(str(path / "vae.safetensors"))
85
+ mx.eval(decoder.parameters())
86
+ return decoder