YuE 2: Inference & LoRA Training Guide
What is YuE 2?
YuE 2 is an open-weight, full-song generation model. You give it lyrics and a style, and it writes and sings a complete song with vocals and backing. It's effectively a local Suno killer. See inference results. It has two halves:
- Score (music half): plans the song: melody, structure, phrasing.
- Sound (NAR half): turns that plan into audio.
LoRAs train both halves, and you can set each half's strength separately at inference.
Inference
The most stable way to run YuE 2 is Ruach Studio. Install it by following the repo instructions. The defaults are good unless noted below.
Settings
Generation
| Setting | Default | Range | Notes |
|---|---|---|---|
| Guidance | 1.6 | 1.0–4.0 | The style's pull. 1.6 works for most LoRAs; 3.0 also gives good results |
| Lyrics guidance | 2.2 | 1.0–2.6 | The lyrics' pull. Never set it equal to Guidance (see below) |
| Sampling (score & sound) | Leave | – | The dev's tuned stable set |
| Planning mode | Full plan | – | See the planning modes table |
Planning modes
| Mode | What it does | Use for |
|---|---|---|
| Full plan | Writes melody + chords (editable score), then the song | Best overall, with or without LoRAs |
| Melody only | Writes the melody, free accompaniment | Covers, or on its own |
| Direct | No score; straight from lyrics + style | Creative results with LoRAs (Score at 0) |
My experience:
- The trainer's default makes Direct-trained LoRAs. These work best in Full plan.
- Direct with Score at 0 is worth a try for creative results. Vocals and style can be more accurate, but the composition is slightly more hectic and sometimes less faithful to the data.
- Melody only works well on its own, not just for covers. Still, Full plan is the best overall if you're not doing a cover.
Settings page
| Setting | Default | Set to | Why |
|---|---|---|---|
| Engine → Creator Heresy → vocal octave | An octave down | As the model wrote it | The default turns planned female vocals male (see below) |
"Set the vocal octave before anything else"
By default, Ruach Studio moves the vocal line of any score the model plans down an octave, which turns planned female vocals male.
Engine → Creator Heresy → vocal octave → "As the model wrote it".
This only affects Full plan when the model writes the score itself. An output info card that says "Vocal line: An octave down" means it was still active.
LoRAs
| Setting | Full plan / Melody only | Direct | Notes |
|---|---|---|---|
| Score strength | 0.4–0.6 | 0 | In Direct, Score above 0 can make output sped up or bad |
| Sound strength | 0.7–1.0 | 0.7–1.0 |
"Check Free Tempo"
The red Free Tempo setting above the caption is easy to press by accident, even when attempting to click on the caption box and well outside its bounds. When it's on, score timing is off on songs with LoRAs (untested without), creativity is massively reduced.
It's silent: the only sign is at the bottom of the output info card. Make sure it's off.
Details
Guidance
Ruach Studio uses two guidance scales, one for the style and one for the lyrics. It added a third guidance branch (the prompt without its lyrics) to yue2.cpp for this.
- The defaults (1.6 / 2.2) apply to every new song, in every mode.
- Setting the two equal is a problem: about half the lyrics drift into hallucination, and the vowels lose their stress.
- The brightest result the dev heard was guidance 4.0 with lyrics at 2.2. I've had good results with 3.0 / 2.3.
- Nothing is clamped. Each field is coloured like a road:
- Red: under 1.
- Green: inside the dev's range.
- Amber: past the range.
- Red: well past the range.
Vocal octave
By default, Ruach Studio moves the vocal line of any score the model plans down an octave, which turns planned female vocals male.
- This only affects Full plan when the model writes the score itself. Direct, Melody only and supplied scores are never moved.
- A take card that says "Vocal line: An octave down" means the setting was still active.
Loading LoRAs
- Place the LoRA in
Ruach_Studio/loras/NAME/. It appears in the Creator right away. - When comparing LoRAs or epochs:
- Use new lyrics, not training lyrics.
- Fix the music seed and sound seed.
- Change only the epoch or LoRA.
- Keep the mode the same.
Other UIs (less stable)
There may be settings on these that match Ruach Studio for both training and inference. I just haven't found them.
HOT-Step.cpp
- Load LoRAs via YuE2 Adapters → Import adapter…. It splits the file into its two halves.
- Don't copy the file directly into
adapters/. Files without the half field are skipped.
ComfyUI
- Use CUDA 13+ and the latest PyTorch. Older setups have a huge slowdown.
AI-Toolkit (training)
- Ruach Studio's trainer is built on a custom version of AI-Toolkit, so the two should be comparable.
- I've moved to Ruach Studio since it gives the best results.
Inference Results
These are all from LoRAs, using roughly the settings above.
Yousei Teikoku - Rank 64, 100 Epochs, 15 songs
https://vocaroo.com/1imfzsfC46fy
https://vocaroo.com/12UhIcUpc5Zx
https://vocaroo.com/16Rb6EvPNW0e
Japanese Folk Metal - Rank 32, 50 epochs, 10 songs
https://files.catbox.moe/cvu2tn.flac
https://files.catbox.moe/o9n5lg.flac
https://files.catbox.moe/yel0jt.flac
Fate Gear - Rank 64, 50 epochs, 15 songs
https://vocaroo.com/1gWtTrdtr7S7
https://vocaroo.com/165I77c6pfwF
Training
Hardware: see Requirements. No suitable GPU? See Training on Modal.
1. Sourcing Audio
For the best results, use high-quality audio files.
- Free Sources: us.doubledouble.top, lucida.to
- Paid/Advanced tools: qobuz-dl
Quality Guidelines:
- Aim for FLAC or 320 kbps MP3.
- If the song is unavailable elsewhere, 192 kbps may be okay.
2. Upscaling & Enhancing (Optional)
If you are forced to use lower-quality files (such as those from YouTube), you can use the following tool to enhance musicality:
- Matchering 2: An experimental tool that yields amazing results. It requires a "target" mix (a high-quality recording/FLAC from any artist in the same genre) to reference.
3. Lyrics
- Get lyrics from Genius or similar sites.
- Add section tags:
[Verse],[Chorus],[Bridge], etc.
4. Dataset Layout
Put everything in Ruach_Studio/datasets/raw/<set>/:
Lyrics file (01. First Song.txt), lyrics only:
Styles file (styles.json) is not read by Ruach Studio. The caption script in step 6 uses it. Write one style line per song, keyed by exact filename:
Style line conventions:
- Comma-separated lowercase tags, roughly in this order: language + genre, tempo word, bpm, vocal + delivery, instruments, mood, production, stereo field.
- No performer names, no musical keys.
- Tag different singers distinctly (e.g.
raspy female lead vocal). - Flag mixed-language songs (
japanese lyrics with english phrases). - BPM detectors often return half or double tempo. Check against the song: "blistering double-kick" is likely 160–200, not 80–100. If unsure, tap along to the chorus on a tap-tempo site.
5. Make the Set
- Trainer → Material → Reload → pick your set.
- Enter the trigger and set name (same as the folder name) → Make the set.
This writes datasets/prepared/<set>/ as 001.wav + 001.txt etc. Each caption looks like:
The problem: Ruach Studio writes the same style line (the "shared style line") into every caption. Your per-song styles are lost.
6. Replace the Captions with Per-Song Styles
Save as ~/apply_styles.py:
Run it after Make the set, then check one caption:
- Pressing Make the set again overwrites the captions. Rerun the script afterwards.
- To change a BPM or tag later, edit
styles.jsonand rerun the script.
7. Train
The trainer's defaults produce Direct-trained LoRAs, which work best in Full plan at inference (see Planning modes).
Rank by dataset size (the dev's recommendation):
| Dataset | Rank |
|---|---|
| One voice, one instrument or one sound (20–50 songs) | 16, at most 32 |
| A style or an artist (varied catalogue) | 32 |
| Many voices (hours of material, each voice tagged) | 64; 128 only if 64 measurably falls short |
The trainer refuses anything above 128.
My results so far:
| Dataset | Rank | Epochs |
|---|---|---|
| 10–15 songs | 32 (default) | 50 |
| Many background vocals | 64 | 100 |
Timing: on an RTX 3090, 50 epochs on 10–20 songs takes about 1 hour, using 21–24 GB VRAM. Modal's L40S is faster (below).
Saves land every 5 epochs. In Telemetry, click epochs to add them to the Creator's LoRA list, then compare them with fixed seeds.
Training on Modal
For those without a beefy local GPU, or who want a speed-up over a 3090.
- Modal gives $30 of free GPU credits every month.
- Use the L40S (48 GB):
- 100 epochs on 15 songs at rank 64 takes ~40 min, about $1.30.
- It beat H100 and H200 on this trainer, possibly thanks to its Ada Lovelace architecture (same as the 4090).
Benchmarks (1 epoch, 15 tracks, rank 64):
| GPU | s/it | 100 epochs |
|---|---|---|
| RTX 3090 (local) | ~4.4 | ~110 min |
| H100 | 3.86 | ~97 min |
| H200 | 4.17 | ~104 min |
| L40S | 1.58 | ~40 min |
How the work is split:
- The latent cache is built locally. The VAE and tokenizers stay home.
- Modal only receives the trainer code, the base checkpoint and the prepared set.
- The output is the same LoRA format Ruach Studio saves.
Setup (once)
Upload the base checkpoint:
Save as ~/modal-ruach/modal_ruach.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | """Ruach Studio's YuE2 LoRA trainer on Modal.
Latent cache built at home; Modal gets the trainer code,
the bf16 checkpoint and the prepared set.
RUACH_GPU=L40S (default) | H100 | H200 | B200 | RTX-PRO-6000
RUACH_NGPU=2 for two cards."""
import glob, json, os, shutil, subprocess, threading, time
import modal
app = modal.App("ruach-lora")
vol = modal.Volume.from_name("ruach-data", create_if_missing=True)
LOCAL_TRAINER = os.path.expanduser(
os.environ.get("RUACH_TRAINER", "~/Ruach_Studio/lab/trainer"))
KIT = "/workspace/ruach"
TRAINER = f"{KIT}/lab/trainer"
WORK = "/tmp/ruach"
CHECKPOINT = "/data/models/checkpoints/yue2_3b_bf16.safetensors"
CU128 = "https://download.pytorch.org/whl/cu128"
GPU = os.environ.get("RUACH_GPU", "L40S")
NGPU = int(os.environ.get("RUACH_NGPU", "1"))
GPU_SPEC = GPU if NGPU == 1 else f"{GPU}:{NGPU}"
image = (
modal.Image.debian_slim(python_version="3.12")
.apt_install("ffmpeg", "libsndfile1", "rsync")
.pip_install("torch==2.11.0", "torchaudio==2.11.0", index_url=CU128)
.pip_install("safetensors==0.8.0", "transformers==4.57.6",
"einops==0.8.2", "rotary-embedding-torch==0.6.5",
"numpy", "soundfile", "librosa", "sentencepiece",
"pyarrow")
.env({"PYTHONUNBUFFERED": "1",
"PYTORCH_CUDA_ALLOC_CONF": "expandable_segments:True"})
.add_local_dir(LOCAL_TRAINER, TRAINER, copy=True,
ignore=["__pycache__", "*.pyc"])
.run_commands(f"cd {TRAINER} && python -c \"import sys; "
"sys.path.insert(0, '.'); import train; "
"import pyarrow.parquet; print('trainer imports ok')\"")
)
# every key spelled out so a change in train.py's defaults can't move it
RECIPE = {
"epochs": 100, "save_epochs": 10,
"rank": 64, "rank_ar": 0, "rank_nar": 0,
"lr": 1e-4, "lr_scale": "sqrt",
"lr_tail_epochs": 0.0, "lr_tail_floor": 0.0,
"ar_loss_weight": 0.5, "ar_kl_weight": 0.2, "ar_lr_multiplier": 0.5,
"train_window_frames": 1500, "chunk": 512,
"ar_max_tokens": 0, "ar_cap_tokens": 0,
"cot": "off", "abc_dropout": 0.5, "do_separation": False,
"seed": 42, "max_grad_norm": 1.0, "accum": 1,
"checkpointing": "auto", # off while memory allows, on at an OOM
"ema": False,
"val_fraction": 0.0, "patience": 0, "nar_phase_epochs": 0,
"resume_from": 0, "lazy_cache": "auto",
"vram_meter": False, "long_off_card0": False,
}
def _sync(src, dst):
if os.path.isdir(src):
os.makedirs(dst, exist_ok=True)
subprocess.run(["rsync", "-a", src + "/", dst + "/"], check=False)
try:
vol.commit()
print(f"[volume] synced {time.strftime('%H:%M:%S')}", flush=True)
except Exception as e: # noqa: BLE001
print(f"[volume] commit failed: {e}", flush=True)
def _captions(ds):
caps = sorted(glob.glob(f"{ds}/*.txt"))
def cached(c):
stem = os.path.splitext(os.path.basename(c))[0]
return os.path.isfile(f"{ds}/_ruach_cache/{stem}_ruach.safetensors")
return caps, [c for c in caps if not cached(c)]
def _trainer_lack(ds):
import sys
if TRAINER not in sys.path:
sys.path.insert(0, TRAINER)
import cache as latent_cache
return latent_cache.missing(ds)
@app.function(image=image, gpu=GPU_SPEC, cpu=8.0, memory=65536,
volumes={"/data": vol}, timeout=86400, max_containers=1)
def train(name: str, dataset: str, checkpoint: str = CHECKPOINT,
overrides: str = "{}"):
import torch
cards = torch.cuda.device_count()
print(f"cards: {cards} x {torch.cuda.get_device_name(0)}", flush=True)
vol.reload()
ds = f"/data/datasets/{dataset}"
caps, lack = _captions(ds)
if not caps:
raise FileNotFoundError(f"no captions in {ds}")
if lack or _trainer_lack(ds):
raise RuntimeError("tracks without a latent cache: "
"build it at home and upload the set again")
if not os.path.isfile(checkpoint):
raise FileNotFoundError(checkpoint)
r = dict(RECIPE, **json.loads(overrides))
tracks = len(caps)
cfg = {k: v for k, v in r.items() if k not in ("epochs", "save_epochs")}
cfg.setdefault("steps", int(r["epochs"] * tracks))
cfg.setdefault("save_every", int(r["save_epochs"] * tracks))
cfg.update(name=name, out=WORK, dataset=ds, checkpoint=checkpoint)
local_out, vol_out = f"{WORK}/output/{name}", f"/data/runs/{name}"
if os.path.isdir(vol_out): # same --name resumes from the last save
shutil.copytree(vol_out, local_out, dirs_exist_ok=True)
print(f"resuming from {vol_out}", flush=True)
os.makedirs(local_out, exist_ok=True)
cfg_path = f"{local_out}/config.json"
with open(cfg_path, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=1)
print(json.dumps(cfg, indent=1), flush=True)
print(f"{tracks} tracks: {cfg['steps']} steps, "
f"a save every {cfg['save_every']}", flush=True)
script = f"{TRAINER}/train.py"
cmd = (["python", "-m", "torch.distributed.run", "--standalone",
f"--nproc_per_node={cards}", script, cfg_path]
if cards > 1 else ["python", script, cfg_path])
stop = threading.Event()
def loop():
while not stop.wait(600):
_sync(local_out, vol_out)
threading.Thread(target=loop, daemon=True).start()
try:
code = subprocess.run(cmd, cwd=KIT).returncode
finally:
stop.set()
_sync(local_out, vol_out)
if code != 0:
raise RuntimeError(f"train.py exited {code}: see the log above")
@app.function(image=image, volumes={"/data": vol}, timeout=1800)
def check(dataset: str = "", checkpoint: str = CHECKPOINT):
vol.reload()
print(checkpoint, "found" if os.path.isfile(checkpoint) else "MISSING")
if dataset:
ds = f"/data/datasets/{dataset}"
caps, lack = _captions(ds)
print(f"{len(caps)} captions, {len(caps) - len(lack)} cached"
+ (" (ready)" if not lack else ""))
print(f"trainer's cache check: {len(_trainer_lack(ds))} "
"tracks to cache")
|
Per Set
1. Build the cache locally. Make the set and replace the captions (steps 5–6), press Unload in the bar, then run:
2. Upload and check. Run these from ~/modal-ruach with the venv active:
You want both lines to read: N captions, N cached (ready) and 0 tracks to cache.
3. Train.
The default in the custom Modal script is rank 64, 100 epochs, a save every 10. For the RS defaults:
- The log should show
cards: 1 x NVIDIA L40S. A run that never printscards:is waiting for a GPU. - Saves reach the volume every 10 minutes and at the end.
- To stop a run:
modal app list, thenmodal app stop <id>. - Rerunning the same
--nameresumes from the last save.
4. Download. Saves are named by step, where step = epoch × tracks. Set T to your track count:
Tips:
- Paste commands one at a time. A pasted block runs to the end, including a detached run.
- Modal's dashboard keeps old logs. Check the timestamp before reading one.