Instructions to use Dirdx/YuE2-3B-MLX-with-Hum-encoder with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use Dirdx/YuE2-3B-MLX-with-Hum-encoder with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download Dirdx/YuE2-3B-MLX-with-Hum-encoder --local-dir YuE2-3B-MLX-with-Hum-encoder
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Duplicate from ahmadw/YuE2-3B-MLX
Browse filesCo-authored-by: Ahmad <ahmadw@users.noreply.huggingface.co>
- .gitattributes +35 -0
- 4bit/config.json +44 -0
- 4bit/model.safetensors +3 -0
- 4bit/qwen.tiktoken +0 -0
- 4bit/vae.safetensors +3 -0
- 4bit/vae_config.json +78 -0
- 4bit/yue2_generation_config.json +24 -0
- 8bit/config.json +43 -0
- 8bit/model.safetensors +3 -0
- 8bit/qwen.tiktoken +0 -0
- 8bit/vae.safetensors +3 -0
- 8bit/vae_config.json +78 -0
- 8bit/yue2_generation_config.json +24 -0
- README.md +74 -0
- bf16/config.json +38 -0
- bf16/model.safetensors +3 -0
- bf16/qwen.tiktoken +0 -0
- bf16/vae.safetensors +3 -0
- bf16/vae_config.json +78 -0
- bf16/yue2_generation_config.json +24 -0
- convert.py +110 -0
- generate.py +320 -0
- test_parity.py +106 -0
- yue2_model.py +216 -0
- yue2_vae.py +86 -0
.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
|