← ExpSoft ACE-Step 1.5

From 17 s/step to 2.76 s/step — pre-computed encoder embeddings for ACE-Step 1.5 LoRA training

ExpSoft ACE-Step 1.5 is a portable Windows app around ACE-Step, the open music-generation model published by ACE Studio & StepFun — MIT for ACE-Step 1.5 and XL, Apache 2.0 for the older ACE-Step v1 model on which this research was done. The v1 DiT is heavy: a naive LoRA training run on a single 16 GB GPU comes out at ≈ 17 seconds per step, which for a 5 000-step run is about a full day of GPU time. By pre-computing the frozen UMT5 / MERT / m-HuBERT encoder embeddings once at dataset build time, switching to bf16-true, keeping in-training samples reachable via VRAM page-out, and saving LoRA-only checkpoints (190 MB instead of 10 GB), the same run lands at ≈ 2.76 seconds per step — about 6× faster, with peak VRAM in the 12 GB envelope. This note walks through the seven decisions that get you there, the ones that didn’t make the cut, and the one honest limitation worth knowing up-front.

· Updated · 14 min read · By Nicolas Riquier

The starting point — and the honest research note

The model trained in this research is ACE-Step v1 (ACE Studio and StepFun, Apache 2.0), still available in the app as the legacy training stack. The models ExpSoft ACE-Step 1.5 generates with in version 1.2.0.0 — ACE-Step 1.5 and XL, MIT — use a different DiT (4B for XL) and a different text encoder (Qwen3-Embedding), so the figures below do not transfer to them. The v1 architecture is, in broad strokes, a flow-matching DiT conditioned by three frozen encoders:

During training, each audio clip in your dataset gets passed through all three encoders at every step (or rather, would, if we did things the naive way). The encoders are frozen: their weights never change. The training signal only updates the DiT (or, for our purposes, the small LoRA matrices attached to the DiT’s attention/MLP projections).

Before going further, one honest research note belongs at the top: a LoRA on this architecture cannot clone the timbre of an individual singer’s voice. The reason is structural rather than a missing setting — the m-HuBERT and singer-conditioning encoders are frozen and were trained jointly with the base DiT on a vocabulary of timbres baked in at pre-training time. A LoRA touching only the DiT can re-weight the model toward styles, genres, instrumentations, languages, structures, even broad vocal qualities the base model already saw — but it can’t introduce a new singer embedding. That’s a known limitation of any LoRA fine-tune on a frozen-encoder architecture; it’s not a bug in the trainer. The product page now states plainly that LoRA training is not supported in version 1.2.0.0.

With that out of the way: the seven decisions that turn this from a 24-hour training session into a four-hour overnight session.

Decision 1 — pre-compute the encoder embeddings, once, at dataset build time

If the three encoders are frozen, their output for a given input never changes between training steps. Running them on every step is doing the same computation 8 000 times in a row. The cleaner approach is to run them once, at dataset preparation time, and cache the output tensors next to each audio clip as a sidecar file.

Concretely: a one-shot prepare step goes through each unique track once — 24 000 dataset rows built from 12 tracks need 12 passes — and saves the four encoder outputs (MERT and m-HuBERT hidden states, text hidden states and text mask) to one .pt sidecar per track under precomputed_embeddings/. The dataset is flagged as precomputed and the trainer picks the sidecars up automatically. Measured on an RTX 5080: 17 s/step with live encoders, 2.76 s/step with the sidecars.

Two implementation details that matter for repeatability:

This single change is the biggest lever. It accounts for nearly all of the ≈ 6× speed-up.

Decision 2 — bf16-true (not bf16-mixed)

The default precision recipe for training on Ampere/Ada/Blackwell GPUs is mixed precision: parameters and gradients live in fp32, the forward and backward passes happen in bf16, and a fp32 master copy of the optimiser state is kept on the side. It’s safe — it handles loss scaling, NaN avoidance, and tiny-gradient underflow gracefully — and it costs you about 2× the VRAM of pure bf16, because fp32 weights and bf16 weights both exist.

For the v1 DiT, the difference is enormous. The DiT weights themselves are:

For a 16 GB GPU, that’s the difference between “fits with a tight batch size of 1” and “fits with comfortable head-room for activations and a small batch”. The cost of going bf16-true is reduced numerical safety: training can occasionally produce NaN gradients on outlier batches, and the optimiser is less forgiving of large learning rates. The mitigation is the watchdog of decision 3 and a learning rate of 1e-4 or lower.

Decision 3 — a NaN watchdog around the optimiser step

With bf16-true, divergence is a real risk. The training loop is watched by a small callback:

  1. Every 50 steps it checks the LoRA parameters with torch.isnan().any() — cheap, about 98 M parameters next to a multi-billion-parameter base model.
  2. At the first NaN or Inf it stops the run and prints a clear message naming the faulty parameter and the remedies, first of all a lower learning rate.
  3. It is always on; there is no switch to disable it.

The failure it catches is real: with bf16-true, a learning rate of 5e-4 and the upstream Adam settings, the first tests diverged to NaN around step 700–1 000, and without the watchdog the run kept going for hours, rendering silent samples. With it, a diverged run ends within 50 steps.

Decision 4 — LoRA-only checkpoints (190 MB instead of 10 GB)

A natural inclination is to save the whole training state at each checkpoint: base DiT weights + LoRA adapters + optimiser state. For the v1 model, that’s about 10 GB per checkpoint. Saving every 500 steps over a 10 000-step run is 200 GB on disk for one run. That’s untenable on a personal machine.

The good news is that the base DiT weights never change during LoRA training. They’re frozen — the same as the encoders. The only state worth saving is the LoRA adapters (about 98 million parameters in the research runs, ~190 MB in bf16) and, optionally, the optimiser state restricted to those LoRA parameters.

ACE-Step’s checkpoint format makes this clean: we save only the LoRA state-dict. On disk this is about 190 MB per checkpoint. Twenty checkpoints over a run = 3.8 GB. That fits. The base DiT weights are loaded fresh from the cached HuggingFace download at load time, and the LoRA adapters are merged on top.

This decision is what makes the “keep many intermediate checkpoints” workflow practical. In the research runs that meant saving often, rendering one sample per checkpoint and discarding the bad ones. The LORA tab still offers Full / LoRA / Both checkpoints, but LoRA training is not supported in version 1.2.0.0.

Decision 5 — VRAM page-out for in-training sampling

One of the friction points of any LoRA training workflow is being able to see what your model is learning, mid-training. The natural test is to generate a few samples every N steps with the current LoRA state and listen to them. The problem is that this requires having two things in VRAM at the same time:

12 GB + 8 GB = 20 GB. Doesn’t fit. The naive workaround is to drop the training state to disk, swap the inference state in, run the sample, swap back. That’s correct but slow (tens of seconds per sample).

The approach used here is page-out to system RAM rather than disk: at a sample step, a callback saves a temporary LoRA folder, moves the transformer, the audio autoencoder and the Adam state to the CPU and empties the CUDA cache (about 7 GB freed), starts a separate render process that loads its own inference pipeline (about 8 GB), then moves everything back and resumes training. Measured cost: about 3 to 5 minutes per sample — 23 s of diffusion and about 2 s of decoding, the rest being page-out / page-in and the start of the render pipeline.

Samples can be rendered every N steps without ending the run. On a 5 000-step run, rendering every 500 steps (10 samples) adds about 50 minutes to a run of about 4 hours.

Decision 6 — saved training profiles instead of one-size defaults

Rather than a configuration copied from a tutorial written for another GPU, the LORA tab ships saved training profiles that set every knob at once: rank, target modules, optimiser, learning rate, precision, audio cap, batch size and accumulation. The two reference recipes use batch 1, accumulation 1, bf16-true, gradient checkpointing and an 80-second audio cap, and say what to lower first on an out-of-memory error.

Decision 7 — keep the frozen encoders off the GPU from the very first step

The upstream trainer still loads the three encoders, and on a 16 GB card that alone filled the GPU (15 of 16 GB) before step 1. Offloading them in Lightning’s on_fit_start hook was too late: Lightning moves the whole module to the GPU before that hook runs.

The fix has three parts, each closing a distinct crash: offload the encoders in the earlier setup hook and detach them from the module tree so the move to the GPU skips them; once re-attached, turn their .to() / .cuda() calls into no-ops, because upstream moves the text encoder back to the GPU at every step; and cast their inputs to fp32, because the encoders stay in fp32 on the CPU while training runs in bf16-true. Without these three fixes, training this model on 16 GB was not possible.

What the research left out

The twelve tabs of version 1.2.0.0

In version 1.2.0.0 the app is a song generator first; the training tabs stay for experimenters and are not supported.

  1. WELCOME — what the app does, Quick Start, licences, requirements.
  2. SETUP — one-click install of a portable Python 3.11, uv, Git, ACE-Step, PyTorch cu128 and the local lyrics engine, all inside the app folder.
  3. GENERATE — a style description plus lyrics gives a song; XL (SFT), XL (Base), XL (Turbo), 2B (Turbo) and the older 2B.
  4. REMIX — Cover, Inspire or Extend an existing track.
  5. GALLERY — songs, LoRAs, datasets and lyrics, with right-click actions and one-click MP3.
  6. LYRICS — a local model on the processor writes a first draft.
  7. HELP — ready-to-paste lyrics templates.
  8. DATASET — builds a training dataset (for experimenters, not supported).
  9. LORA — the trainer this note is about (for experimenters, not supported).
  10. DISK — what each folder takes and which ones are safe to clear.
  11. SETTINGS — optional keys such as the Hugging Face token.
  12. MORE TOOLS ? — version check and the ExpSoft products page.

Each tab has a single responsibility; there’s no global state machine and no “Run” button that does seven different things depending on which radio button was last selected.

Credits and license

Everything that makes this work belongs to the open-source communities behind it. ExpSoft’s contribution is the Windows-portable EasyInstaller around them and the engineering decisions described above. The pieces:

The C# host is .NET 8 WPF (MIT, Microsoft). The cross-app branding, the Welcome content and the engineering work described above are original ExpSoft contributions. None of this would exist without the ACE-Step team’s decision to release the model and the training scripts under an OSI-approved licence.

Take-aways

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

Why exactly 17 s/step before, 2.76 s/step after — what do those numbers mean?

Measurements taken on a single RTX 5080 (16 GB) in bf16-true, on the ACE-Step v1 model: a 12-track dataset repeated to fill the epoch, audio capped at 60 seconds, rank 96 with the MLP layers, a 5 000-step run. The 17 s/step baseline runs the three frozen encoders inside the training loop at every step; the 2.76 s/step uses the pre-computed embeddings of Decision 1, which account for nearly all of the speed-up — about 24 hours versus about 4 hours on 5 000 steps. These figures belong to the v1 stack and do not transfer to the XL models.

Why can’t a LoRA on ACE-Step 1.5 clone a specific singer’s voice?

The architecture uses frozen encoders (UMT5 for text, MERT for music content, m-HuBERT for vocal/timbre). Those encoders were trained jointly with the base DiT during pre-training, on a fixed vocabulary of timbres. A LoRA touches the DiT only — it can re-weight how the model interprets and combines those frozen embeddings, but it can’t introduce a new singer embedding. At best, a LoRA can steer the model toward styles, genres, instrumentation, language, structures and broad vocal qualities the base model already saw — never toward a specific voice the encoders weren’t trained on. Note that LoRA training is not supported in version 1.2.0.0: in the author’s tests it does not give usable results yet. This is structural, not a tuning problem.

What happens when the NaN watchdog triggers?

The watchdog checks the LoRA parameters every 50 steps. At the first NaN or Inf it stops the run and prints a message naming the faulty parameter and the remedies — most often a learning rate that is too high (1e-4 or lower is recommended with bf16-true), sometimes a corrupted or extreme clip in the dataset. It does not skip steps or pause: the run ends.

Why save LoRA-only checkpoints instead of the full model state?

The base DiT weights never change during LoRA training — they’re frozen. The only state worth saving is the LoRA adapters, which came to about 98 million parameters in the research runs (~190 MB per checkpoint) versus about 10 GB for the full training state. Saving every 500 steps over a 10 000-step run is 3.8 GB on disk for the LoRA-only version, vs 200 GB for the full version. The LoRA-only format is what makes “save often, browse later, prune the bad ones” a practical workflow on a personal SSD. The LoRA folder is what the sample renderer loads on top of the base model; resuming a run needs the full checkpoint, which is why the LORA tab offers Full / LoRA / Both (default Both).

Can in-training sampling be disabled for the fastest possible training?

Yes — in the LORA tab, “Render every N steps” set to 0 turns periodic sampling off; the run then never pages the training state out and never starts the render pipeline. Each sample costs about 3 to 5 minutes, so rendering every 500 steps on a 5 000-step run adds about 50 minutes to a run of about 4 hours; in the research runs, hearing the adapter evolve was worth it.

Want to use ExpSoft ACE-Step 1.5?

Get it on Patreon →