The One Boolean That Ended a Week of Guessing
A week of local AI music generation, a number that would not make sense, and what changed when the pipeline finally started handing over its receipts.
TL;DR — Moving YuE2 off ComfyUI and onto the project's own
yue2-inferruntime surfaced a truncation flag, a CFG knob and a per-stage receipt that the ComfyUI nodes never expose — and the first thing they proved was that a conclusion I had written down three days earlier was wrong.
For a week the number had been taunting me. Ask for a three-minute song, get ninety-eight seconds. Ask again, get a hundred and thirty. Somewhere between the request and the audio file, music was going missing — or it wasn't, and the model was simply finishing early, and I had no way on earth to tell which.
That is the ordinary condition of running a model through a node graph. You get the audio. You do not get the receipt.
This is what happened when I stopped using one. The model is YuE2, the open lyrics-to-song system from the Multimodal Art Projection team; the hardware is a single RTX 5080 with 16 GB of VRAM; the date is 15 September 2026. I had spent a week driving it through ComfyUI's built-in nodes, then rebuilt the same work on the project's own Python runtime — and found that almost nothing I gained was about sound quality. It was about being able to see.
The receipt
Start with the boolean that ended the week. The official runtime writes two flags into result.json after every generation: truncated.abc and truncated.semantic. Across 29 runs, both were false every single time.
The songs were never being cut off. They were ending. Length is dictated by how many words there are to sing, and the project's own 213-second showcase example simply carries far more lyric than mine did. One boolean retired a week of speculation — and a script I had written to solve a problem that did not exist.
What a node graph doesn't tell you
ComfyUI's two YuE2 nodes expose eleven sampling inputs between them. The official request schema has seven fields: id, style, lyrics, cot, seed, abc, cfg_scale. Line them up and the gaps all run one way.
| Capability | ComfyUI YuE2 nodes | Official yue2-infer 0.1.6 |
|---|---|---|
| Truncation reporting | none | truncated.abc and truncated.semantic in result.json |
Semantic CFG (cfg_scale) |
not exposed at all | request field; defaults 1.0 for full/melody, 1.01 for off |
| Conditioning-mode contract | a mode string, no rules |
cot = full / melody / off, each with a documented ABC-input contract |
| Synthesis steps | whatever you set on KSampler (the template ships 20) | the release preset is fixed: 32 midpoint steps, context 24576 |
| Per-stage timing | none | seconds and tokens/s for ABC, semantic, NAR and VAE separately |
| Reproducibility receipt | none | weight SHA-256, config hash, request, seed, decoder identity, artifact hashes |
| Re-decode a finished song | not offered | latent.npy is saved; swap the decoder without regenerating the music |
| Memory levers | none | offload_ar, memory_budget_gib, vae_core_frames, query_chunk_size |
| Plan round-trip | manual string wiring between nodes | plan.save() / SymbolicPlan.load() with integrity validation |
Read that list again and notice what it is not. There is no secret quality setting on it. Every row is instrumentation — the difference between a result you can interrogate and a result you can only re-roll. Without them, a bad generation and a misread generation look exactly alike.
The confession
Three days before this, I had written down a finding: YuE2 ignores tempo instructions. It was wrong, and the new receipts made that embarrassingly easy to establish. Here is every run, requested tempo against the tempo the model actually wrote into its score:
| Requested in style | Written into the score |
|---|---|
| 112 BPM | 118 |
| 118 BPM | 122 |
| 124 BPM | 128 |
| 126 BPM | 128–132 |
| 132 BPM | 132 |
| 136 BPM | 134 |
| 140 BPM | 145 |
| 144 BPM | 140 |
Mean absolute error: 3.5 BPM, monotonic across the whole range. The model follows tempo perfectly well. What had misled me was a single prompt containing a range — "128–145" — which produced a score marked 177 BPM. I had taken one malformed input and generalised it into a law. Give the model one number and it does exactly what you asked.
Two patches, 24 gigabytes into sixteen
The documentation does not pretend otherwise: the supported baseline is "a BF16-capable NVIDIA GPU with 24GB memory and one active request per pipeline." On a 16 GB card the stock install fell over twice, and both failures were interesting.
The first was RuntimeError: USE_FLASH_ATTENTION was not enabled for build. Windows PyTorch wheels ship without the FlashAttention kernel — but YuE2's backend auto-detection only checks that the aten._flash_attention_forward operator exists and that its schema mentions seqused_k. On Windows the operator is registered. Detection passes, execution dies. The attention backend is a keyword-only argument the pipeline never forwards, so the repair is to default it to cuDNN; CUDA graphs survive intact.
The second was an out-of-memory error on a single 3.77 GiB allocation inside the non-autoregressive synthesis stage, which on CUDA sets its attention block size to the entire query length and materialises the whole sequence at once. Two documented levers close it: offload_ar, which parks the roughly 6 GB autoregressive model on the CPU while synthesis runs, and query_chunk_size, which splits the attention into 2048-token blocks for a mathematically identical result.
Then it ran: 213.8 seconds of audio in 238.7 seconds. The breakdown is worth keeping — ABC score 20.5 s, semantic tokens 48.7 s, NAR synthesis 161.4 s, VAE 4.0 s. Synthesis is 68% of the clock, which explains why ComfyUI had felt four times quicker all week. Its template runs 20 sampler steps. The release preset runs 32.
What the lyrics decide
Every guide will tell you to match the language named in your style prompt to the language of your lyrics. I tested it the only way worth testing it: hold the lyrics absolutely fixed, change one word in the style prompt, and measure both what language came out and how much of the actual lyric got sung.
| Lyrics | Language token in style | Language detected | Lyric fidelity |
|---|---|---|---|
| Korean | Korean |
Korean, 97% | 74% |
| Korean | English |
Korean, 97% | 57% |
| Korean | none | Korean, 97% | 71% |
| English | English |
English, 69% | 28% |
| English | Korean |
English, 71% | 3% |
| Korean + English mixed | both | Korean, 89% | 19% |
The language token does not choose the language. The lyrics do — Korean lyrics tagged English still came back Korean at 97% confidence. What the mismatch destroys is fidelity. The English track collapsed from 28% to 3% and dissolved into "Ah, ah, ah." Leaving the token out entirely costs almost nothing, which is consistent with the project's own showcase example: its style prompt names no language at all, and it sings in Chinese throughout.
The worst condition of all was the one I had been using for a week — Korean and English mixed inside the same lines, at 19%. The transcript falls apart precisely where the languages change.
Room for the words
I already knew that giving the model too few notes per syllable makes it rap rather than sing. It turns out there is a wall on the other side too.
- Korean: 185 syllables across 208 notes — 1.12 — sung faithfully, 74%
- English: 119 syllables across 240 notes — 2.02 — 28%, with half the notes filled by "oh oh oh"
Hand the model a plan with twice as many notes as your words can occupy and it will invent something to put in them. The usable band is roughly 1.0 to 1.3.
The lottery nobody mentions
The documentation is unusually candid here. From the generation page: "One pipeline call produces one candidate. The benchmark's candidate selection is a separate evaluation step." From the benchmark page: "Standard YuE2 selects the lower-PER candidate from two generations."
That reframes the headline numbers. YuE2 at best-of-8 posts a SongBench average of 6.9632, ahead of Suno v5's 6.8721 and Suno v6's 6.5562. Unselected, it scores 6.7316. So I ran eight generations that differed by nothing but the seed, and scored how much of the lyric each one actually sang: 0, 31, 44, 45, 50, 57, 66, 70%.
The worst of them is the one I keep going back to. Its audio is completely normal — 136 seconds, healthy dynamics, an ordinary spectrum. Play it and you hear a voice. Transcribe it and you get nothing at all. Audible, and unintelligible.
Generate once and that is the distribution you are drawing from, with your eyes closed.
All four clips were generated locally by YuE2 for this write-up, at 128 kbps. Nothing was edited, mixed or re-taken.
FAQ
Can YuE2 run on a 16 GB GPU?
Yes, with two changes. The documented baseline is a 24 GB NVIDIA card, and a stock install fails twice on 16 GB: once because the Windows PyTorch wheel has no FlashAttention kernel, and once because the non-autoregressive synthesis stage feeds its whole query sequence into a single attention call. Forcing the cuDNN attention backend, enabling offload_ar, and chunking the query fixed both without changing the output. A 213.8-second song then took 238.7 seconds end to end.
Does the style prompt control BPM in YuE2?
Yes, if you write a single number. Across 28 runs the tempo written into the generated score tracked the requested BPM with a mean absolute error of 3.5 BPM, monotonically from 112 to 144. A tempo range in the prompt ("128–145") is what breaks it — that produced a score marked 177 BPM.
Why does YuE2 sing "oh oh oh" instead of my lyrics?
Usually because there are more notes in the plan than syllables in the lyrics. In a matched pair, Korean lyrics at 185 syllables over 208 notes (1.12 notes per syllable) were sung faithfully, while English lyrics at 119 syllables over 240 notes (2.02) were half-filled with vocalisations. Aim for roughly 1.0–1.3 notes per syllable.
Should I mix languages in one set of lyrics?
No. In this test the bilingual lyric set scored the lowest fidelity of any condition, 19%, and the transcript degraded at each point where the language switched mid-line.
Is one generation enough?
No. Eight runs that differed only by seed scored 0, 31, 44, 45, 50, 57, 66 and 70% on lyric fidelity. The published benchmark numbers use candidate selection, so a single unselected call is not the same setting.
Sources: YuE2 repository (multimodal-art-projection/YuE), YuE2 generation docs, YuE2 benchmark results, generation and covers reference, m-a-p/YuE2-3B on Hugging Face, WildSongBench dataset.
Image: Johann Ernst Bach, Magnificat autograph manuscript, Public domain, via Wikimedia Commons.
← Back to all posts