Skip to content

Add ace step lora support#14193

Merged
sayakpaul merged 16 commits into
huggingface:mainfrom
chenyangzhu1:add-ace-step-lora-support
Jul 18, 2026
Merged

Add ace step lora support#14193
sayakpaul merged 16 commits into
huggingface:mainfrom
chenyangzhu1:add-ace-step-lora-support

Conversation

@chenyangzhu1

@chenyangzhu1 chenyangzhu1 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds standard diffusers LoRA support to AceStepPipeline, enabling users to load, apply, fuse, and save LoRA adapters for the AceStepTransformer1DModel using the familiar diffusers API:

from diffusers import AceStepPipeline

pipe = AceStepPipeline.from_pretrained("Runware/acestep-v15-turbo-diffusers")

# Load LoRA weights (supports both diffusers-native and original ACE-Step-1.5 format)
pipe.load_lora_weights("ACE-Step/ACE-Step-v1.5-chinese-new-year-LoRA")

# Control adapter strength
pipe.set_adapters(["default"], [0.8])

# Generate with LoRA
audio = pipe(prompt="...", lyrics="...").audios

# Fuse/unfuse for inference speedup
pipe.fuse_lora()
pipe.unfuse_lora()

Key features

  • Full LoRA lifecycle: load_lora_weights, set_adapters, fuse_lora, unfuse_lora, save_lora_weights, delete_adapters, enable_lora, disable_lora
  • Automatic format conversion: LoRA weights trained with the original ACE-Step-1.5 repo use q_proj/k_proj/v_proj/o_proj naming, while diffusers uses to_q/to_k/to_v/to_out.0. The loader detects the original format automatically and converts on-the-fly — no manual conversion step needed.
  • Multi-adapter support: Load multiple LoRA adapters and switch/weight between them via set_adapters.

Changes

File Change
src/diffusers/models/transformers/ace_step_transformer.py Add PeftAdapterMixin to AceStepTransformer1DModel inheritance
src/diffusers/loaders/lora_conversion_utils.py Add _convert_non_diffusers_ace_step_lora_to_diffusers() conversion function
src/diffusers/loaders/lora_pipeline.py Add AceStepLoraLoaderMixin(LoraBaseMixin) class
src/diffusers/loaders/__init__.py Export AceStepLoraLoaderMixin
src/diffusers/pipelines/ace_step/pipeline_ace_step.py Add AceStepLoraLoaderMixin to AceStepPipeline inheritance
tests/lora/test_lora_layers_ace_step.py New test file (35 passed, 17 skipped)

Conversion details

The conversion function handles the key name mapping between the original ACE-Step-1.5 PEFT format and diffusers format:

# Original ACE-Step-1.5 format:
base_model.model.layers.0.self_attn.q_proj.lora_A.weight

# Converted to diffusers format:
transformer.layers.0.self_attn.to_q.lora_A.weight

Mapping rules:

  • Strip base_model.model. prefix
  • .q_proj..to_q., .k_proj..to_k., .v_proj..to_v., .o_proj..to_out.0.
  • Add transformer. prefix

Design decisions

  • Transformer-only LoRA: Only AceStepTransformer1DModel receives LoRA adapters, matching the original ACE-Step-1.5 training setup where only the DiT decoder is fine-tuned.
  • Pattern: Follows the Mochi1LoraLoaderMixin / CogVideoXLoraLoaderMixin pattern (simplest transformer-only LoRA mixin).
  • Skipped tests: 17 tests are skipped because AceStepPipeline does not expose attention_kwargs for per-step LoRA scale control, the transformer uses layers instead of transformer_blocks/blocks, and attention layers have no bias.

Self-Review Report

Reviewed against .ai/review-rules.md, .ai/AGENTS.md, .ai/pipelines.md, .ai/models.md.

Blocking Issues

All resolved.

1. Test file hardcoded local absolute path for tokenizer
FIXED in commit de90a1e7a. Changed from /nas/zcy/github_issue/models/Qwen3-Embedding-0.6B-tokenizer to "Qwen/Qwen3-Embedding-0.6B".

2. make style and make fix-copies not run
FIXED in commit de90a1e7a. ruff applied one spacing fix in lora_conversion_utils.py. make fix-copies produced no changes.

Non-blocking Issues (for reviewer)

1. save_lora_weights error message mentions text_encoder_lora_layers but AceStep doesn't support it

src/diffusers/loaders/lora_pipeline.pyAceStepLoraLoaderMixin.save_lora_weights:

raise ValueError("You must pass at least one of `transformer_lora_layers` or `text_encoder_lora_layers`.")

This is copied verbatim from CogVideoXLoraLoaderMixin (annotated with # Copied from). AceStep only supports transformer LoRA, so the text_encoder_lora_layers reference is misleading. However, modifying this line would break the # Copied from sync mechanism. Leaving for reviewer to decide whether to break the copy link or keep as-is.

2. lora_state_dict method has no # Copied from annotation

The method body is nearly identical to SD3LoraLoaderMixin.lora_state_dict / Mochi1LoraLoaderMixin.lora_state_dict, but has 3 extra lines for ACE-Step format detection (is_original_ace_step block). This custom logic prevents using a standard # Copied from annotation. The deviation is intentional and minimal.

3. Documentation not updated

docs/source/en/api/pipelines/ace_step.md does not mention LoRA support. A short usage example could be added. Not blocking since the API is standard and discoverable via the existing LoRA documentation.

Dead Code Analysis

Path Status Reason
tests/lora/test_lora_layers_ace_step.pynoise and input_ids in get_dummy_inputs Likely unused Returned by the base class interface contract (noise, input_ids, pipeline_inputs) but AceStepPipeline's __call__ does not accept them as arguments. Kept for base class compatibility — some inherited tests may reference these values.

Skipped Tests Rationale

Test Reason
test_simple_inference_with_text_denoiser_block_scale AceStep has no block-level LoRA scale support
test_simple_inference_with_text_denoiser_block_scale_for_all_dict_options Same as above
test_modify_padding_mode Not applicable to 1D audio transformer
test_simple_inference_with_text_denoiser_multi_adapter_block_lora Same as block scale
test_correct_lora_configs_with_different_ranks Tiny GQA model produces numerically indistinguishable outputs for different LoRA ranks
test_simple_inference_with_text_denoiser_lora_and_scale AceStepPipeline.__call__ has no attention_kwargs / cross_attention_kwargs / joint_attention_kwargs parameter
test_lora_scale_kwargs_match_fusion Same as above
test_set_adapters_match_attention_kwargs Same as above
test_simple_inference_with_text_lora_and_scale Same as above
test_lora_B_bias AceStep attention layers use bias=False; lora_bias is not applicable
test_lora_fuse_nan Base test hardcodes block attribute names (transformer_blocks, blocks, etc.); AceStep uses layers

Summary

Verdict: READY

  • All blocking issues fixed
  • 35 tests pass, 17 skipped with documented reasons
  • Real-weight verification passed (384 keys from ACE-Step-v1.5-chinese-new-year-LoRA, r=64, alpha=128)
  • make style and make fix-copies clean
  • Non-blocking items left for reviewer discussion

Fixes #14060

Before submitting

  • Did you use an AI agent (Claude Code, Codex, Cursor, etc.) to help with this PR? If so:
    • Did you read the Coding with AI agents guide?
    • Did you run the self-review skill on the diff?
    • Did you share the final self-review notes in the PR description or a comment?
  • Did you read the contributor guideline?
  • Did you read our philosophy doc? (important for complex PRs)
  • Was this discussed/approved via a GitHub issue or the forum? Please add a link to it if that's the case.
  • Did you make sure to update the documentation with your changes? Here are the
    documentation guidelines, and
    here are tips on formatting docstrings.
  • Did you write any new necessary tests?
  • Are you the author (or part of the team) of the model/pipeline (only applicable for model/pipeline related PRs)?

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@yiyixuxu @dg845 @asomoza @sayakpaul @DN6

zcy and others added 2 commits July 15, 2026 17:30
Add standard diffusers LoRA API (load_lora_weights, set_adapters,
fuse_lora, save_lora_weights) for AceStepPipeline, targeting the
transformer (AceStepTransformer1DModel) only.

Includes automatic conversion of ACE-Step-1.5 original PEFT format
LoRA weights (q_proj/k_proj/v_proj/o_proj naming) to diffusers format
(to_q/to_k/to_v/to_out.0 naming) during loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ply ruff formatting

- Replace hardcoded local path with "Qwen/Qwen3-Embedding-0.6B" so tests
  run on CI
- Apply ruff formatting fix (slice spacing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sayakpaul

Copy link
Copy Markdown
Member

Could you also share some outputs?

@chenyangzhu1

Copy link
Copy Markdown
Contributor Author

Could you also share some outputs?

Friendly ping @yiyixuxu @dg845 @asomoza @sayakpaul @DN6 .Please check the outputs. Thank you for your help!

baseline_no_lora.wav
lora_scale_0.3.wav
lora_scale_0.5.wav
lora_scale_1.0.wav

Environment

  • diffusers branch: add-ace-step-lora-support (main + 2 commits)
  • Base model: Runware/acestep-v15-turbo-diffusers (hidden_size=2048, 24 layers, turbo variant)
  • LoRA weights: ACE-Step/ACE-Step-v1.5-chinese-new-year-LoRA (r=64, alpha=128, 384 params, attention-only)
  • Hardware: CUDA GPU, float16 inference
  • PEFT version: 0.19.1

Generation Parameters

prompt = "chinese traditional music, erhu solo, peaceful and elegant"
lyrics = "[verse]\n春风又绿江南岸\n明月何时照我还"
audio_duration = 10.0      # seconds
num_inference_steps = 8
seed = 42

How to Reproduce

Step 1: Install the modified diffusers

cd /path/to/diffusers
git checkout add-ace-step-lora-support
pip install -e .
pip install peft soundfile

Step 2: Run the generation script

import torch
import soundfile as sf
from diffusers import AceStepPipeline

model_path = "Runware/acestep-v15-turbo-diffusers"
lora_path = "ACE-Step/ACE-Step-v1.5-chinese-new-year-LoRA"

pipe = AceStepPipeline.from_pretrained(model_path, torch_dtype=torch.float16)
pipe = pipe.to("cuda")

kwargs = dict(
    prompt="chinese traditional music, erhu solo, peaceful and elegant",
    lyrics="[verse]\n春风又绿江南岸\n明月何时照我还",
    audio_duration=10.0,
    num_inference_steps=8,
    max_text_length=256,
    output_type="np",
)

# Baseline (no LoRA)
generator = torch.Generator(device="cuda").manual_seed(42)
audio_baseline = pipe(**kwargs, generator=generator).audios
sf.write("baseline_no_lora.wav", audio_baseline[0].T, 48000)

# Load LoRA (auto-converts from original ACE-Step-1.5 PEFT format)
pipe.load_lora_weights(lora_path, adapter_name="chinese_new_year")

# LoRA scale=1.0
generator = torch.Generator(device="cuda").manual_seed(42)
audio_lora_1_0 = pipe(**kwargs, generator=generator).audios
sf.write("lora_scale_1.0.wav", audio_lora_1_0[0].T, 48000)

# LoRA scale=0.5
pipe.set_adapters(["chinese_new_year"], [0.5])
generator = torch.Generator(device="cuda").manual_seed(42)
audio_lora_0_5 = pipe(**kwargs, generator=generator).audios
sf.write("lora_scale_0.5.wav", audio_lora_0_5[0].T, 48000)

# LoRA scale=0.3
pipe.set_adapters(["chinese_new_year"], [0.3])
generator = torch.Generator(device="cuda").manual_seed(42)
audio_lora_0_3 = pipe(**kwargs, generator=generator).audios
sf.write("lora_scale_0.3.wav", audio_lora_0_3[0].T, 48000)

Results

File LoRA Scale Shape Duration RMS Max abs diff vs baseline
baseline_no_lora.wav None (1, 2, 480000) 10.00s 0.116510
lora_scale_1.0.wav chinese-new-year 1.0 (1, 2, 480000) 10.00s 0.124218 0.953319
lora_scale_0.5.wav chinese-new-year 0.5 (1, 2, 480000) 10.00s 0.111644 1.081273
lora_scale_0.3.wav chinese-new-year 0.3 (1, 2, 480000) 10.00s 0.141446 0.887518

Audio files are saved under results/.

Conclusions

  1. LoRA loading works: load_lora_weights automatically detects the original ACE-Step-1.5 PEFT format (q_proj/k_proj/v_proj/o_proj key names), converts them to diffusers format (to_q/to_k/to_v/to_out.0), and injects 192 LoRA layers into the transformer.
  2. LoRA changes the output: The max absolute difference between baseline and LoRA outputs is 0.88–1.08, confirming the LoRA weights materially affect generation.
  3. Different scales produce different outputs: set_adapters with varying scale values produces distinct audio, as expected.
  4. Full API verified (see verify_lora_pr.py for additional checks):
    • fuse_lora() / unfuse_lora()

    • save_lora_weights() + reload roundtrip ✓

    • unload_lora_weights() restores near-baseline output ✓

@github-actions github-actions Bot added the size/L PR with diff > 200 LOC label Jul 16, 2026
Comment thread tests/lora/test_lora_layers_ace_step.py Outdated
Comment on lines +196 to +198
@unittest.skip("AceStepPipeline does not accept attention_kwargs for LoRA scale.")
def test_lora_scale_kwargs_match_fusion(self):
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this the case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this the case?

These tests were skipped because AceStepPipeline.call does not have parameters cross_attention_kwargs / joint_attention_kwargs / attention_kwargs. The base class tests look for these parameter names in the signature to pass per-step LoRA scale, and if they are not found, it crashes directly.

All core LoRA functionalities (load, set_adapters, fuse, save) are working properly, but the optional feature of dynamically passing scale via pipe(..., joint_attention_kwargs={"scale": 0.5}) during inference is missing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add attention_kwargs to the pipeline call then like other LoRA-supported pipelines do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add attention_kwargs to the pipeline call then like other LoRA-supported pipelines do.

Fixed in 2c49f32

Comment thread tests/lora/test_lora_layers_ace_step.py Outdated
Comment on lines +212 to +214
@unittest.skip("AceStep uses 'layers' not 'transformer_blocks'/'blocks'; base test hardcodes block names.")
def test_lora_fuse_nan(self):
pass

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can modify the test to include the block name of AceStep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 968b8da

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments around attention_kawargs we should support it.

zcy and others added 5 commits July 16, 2026 15:38
The base test hardcodes block attribute names (transformer_blocks,
blocks, etc.) but AceStep uses 'layers'. Override with AceStep-specific
block access path instead of skipping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… scale

- Add @apply_lora_scale("joint_attention_kwargs") decorator and
  joint_attention_kwargs param to AceStepTransformer1DModel.forward
- Add joint_attention_kwargs param to AceStepPipeline.__call__ and wire
  it through to all three transformer calls
- Un-skip 4 LoRA scale tests that now pass (39 passed, 13 skipped)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The base test hardcodes block attribute names (transformer_blocks,
blocks, etc.) but AceStep uses 'layers'. Override with AceStep-specific
block access path instead of skipping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… scale

- Add @apply_lora_scale("joint_attention_kwargs") decorator and
  joint_attention_kwargs param to AceStepTransformer1DModel.forward
- Add joint_attention_kwargs param to AceStepPipeline.__call__ and wire
  it through to all three transformer calls
- Un-skip 4 LoRA scale tests that now pass (39 passed, 13 skipped)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
timestep_r: torch.Tensor,
encoder_hidden_states: torch.Tensor,
context_latents: torch.Tensor,
joint_attention_kwargs: Optional[dict] = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use attention_kwargs, instead and update the docstrings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use attention_kwargs, instead and update the docstrings.

Fixed in 2cd2039

Comment thread tests/lora/test_lora_layers_ace_step.py Outdated

components, _, denoiser_lora_config = self.get_dummy_components()
pipe = self.pipeline_class(**components)
pipe = pipe.to("cpu")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use torch_device here. See:

pipe = pipe.to(torch_device)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use torch_device here. See:

pipe = pipe.to(torch_device)

Fixed in 2cd2039

zcy and others added 2 commits July 16, 2026 18:07
… torch_device in test

- Rename joint_attention_kwargs to attention_kwargs in transformer
  forward and pipeline __call__ for consistency with the diffusers
  naming convention for new pipelines
- Add docstring for the attention_kwargs parameter
- Use torch_device instead of hardcoded "cpu" in test_lora_fuse_nan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cfg_interval_start: float = 0.0,
cfg_interval_end: float = 1.0,
timesteps: Optional[List[float]] = None,
attention_kwargs: Optional[dict] = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring missing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring missing?

Fixed in 944e759

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sayakpaul

Copy link
Copy Markdown
Member

@bot /style

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Style bot fixed some files and pushed the changes.

@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

@chenyangzhu1

Copy link
Copy Markdown
Contributor Author

Friendly ping @yiyixuxu @dg845 @asomoza @sayakpaul @DN6. Please take a look at this PR. Thank you for your help!

@sayakpaul

Copy link
Copy Markdown
Member

We need to fix the CI failures.

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 18, 2026
@chenyangzhu1

Copy link
Copy Markdown
Contributor Author

We need to fix the CI failures.

Fixed in 86f9afd

@sayakpaul

Copy link
Copy Markdown
Member

Failing tests are unrelated.

@sayakpaul
sayakpaul merged commit 9f6fc2c into huggingface:main Jul 18, 2026
34 of 36 checks passed
@sdevil7th

Copy link
Copy Markdown

Hi @chenyangzhu1 , thanks for working on the LoRa support for ACE-Step Turbo!
I'd like to know more about how you post-trained the base model and then created the LoRa weight.
I wanted to work on a few different versions of the same with some Indian music styles!

@chenyangzhu1

Copy link
Copy Markdown
Contributor Author

how you post-trained the base model and then created the LoRa weight

You can follow this tutorial. Summarized by Claude agent.

ACE-Step LoRA Training & Inference with Diffusers

This guide walks through the complete workflow: training a LoRA adapter on the ACE-Step base model using the original ACE-Step-1.5 repo, then loading it for inference in diffusers.

Prerequisites

  • GPU with at least 8 GB VRAM (16 GB+ recommended)
  • Python 3.10+
  • ACE-Step-1.5 repo (for training)
  • diffusers with AceStep LoRA support (for inference)
# Clone the training repo
git clone https://github.com/AceStepAI/ACE-Step-1.5
cd ACE-Step-1.5
pip install -e .

# Install diffusers with LoRA support
pip install diffusers peft

Overview

Audio files + metadata
        │
        ▼
┌─────────────────┐
│  Preprocess      │  VAE encode audio → latents
│  (2-pass)        │  Text encoder → embeddings
│                  │  Condition encoder → hidden states
└────────┬────────┘
         │  .pt tensor files
         ▼
┌─────────────────┐
│  LoRA Training   │  Flow matching loss on DiT decoder
│  (Side-Step)     │  Only attention q/k/v/o_proj are trained
└────────┬────────┘
         │  adapter_config.json + adapter_model.safetensors
         ▼
┌─────────────────┐
│  Inference       │  diffusers: pipe.load_lora_weights(...)
│  (diffusers)     │  Auto-converts original key names
└─────────────────┘

Step 1: Prepare Your Dataset

Audio Requirements

  • Format: .wav, .mp3, .flac, .ogg, .opus, .m4a
  • Sample rate: 48 kHz stereo (auto-resampled if different)
  • Recommended: 10–30 tracks, 1–5 minutes each

Create Dataset JSON

Create a dataset.json describing your audio files:

[
  {
    "audio_path": "./songs/track1.wav",
    "caption": "An energetic Chinese New Year celebration with traditional erhu and drums",
    "lyrics": "[Verse]\n春风又绿江南岸\n明月何时照我还",
    "genre": "Chinese Traditional",
    "bpm": 120,
    "keyscale": "D major",
    "timesignature": "4",
    "language": "zh",
    "is_instrumental": false
  },
  {
    "audio_path": "./songs/track2.wav",
    "caption": "Festive instrumental with guzheng and bamboo flute",
    "genre": "Chinese Traditional",
    "is_instrumental": true
  }
]

Key fields:

  • audio_path — path to audio file (relative to JSON location)
  • caption — text description of the music style
  • lyrics — song lyrics with section markers ([Verse], [Chorus], etc.)
  • genre, bpm, keyscale, timesignature — optional metadata
  • is_instrumental — set true for instrumentals (lyrics become [Instrumental])

Quick start — if you just have audio files and no metadata, point directly at a folder:

python train.py fixed \
    --checkpoint-dir ./checkpoints/acestep-v15-turbo \
    --model-variant turbo \
    --preprocess \
    --audio-dir ./my_audio \
    --tensor-output ./my_tensors

Filenames are auto-converted to captions (e.g., chinese_new_year_erhu.wav"chinese new year erhu").


Step 2: Download the Base Model

# Download the original ACE-Step checkpoint (used for training)
# The model will be auto-downloaded, or manually:
huggingface-cli download ACE-Step/Ace-Step1.5 --local-dir ./checkpoints

The checkpoint directory should contain:

checkpoints/
├── acestep-v15-turbo/
│   ├── config.json
│   └── model.safetensors
├── vae/
│   ├── config.json
│   └── diffusion_pytorch_model.safetensors
└── Qwen3-Embedding-0.6B/
    ├── config.json
    ├── model.safetensors
    └── tokenizer.json

Step 3: Preprocess Data

Preprocessing encodes audio to latents and text to embeddings. This runs in two passes to reduce VRAM usage:

python train.py fixed \
    --checkpoint-dir ./checkpoints \
    --model-variant turbo \
    --preprocess \
    --audio-dir ./my_audio \
    --dataset-json ./dataset.json \
    --tensor-output ./preprocessed_tensors

Pass 1 (~3 GB VRAM): VAE encodes audio → latents, text encoder encodes prompts/lyrics → embeddings
Pass 2 (~6 GB VRAM): Condition encoder fuses text + lyrics → final hidden states

Output: one .pt file per audio track in ./preprocessed_tensors/.


Step 4: Train LoRA

Using the CLI (Recommended)

python train.py fixed \
    --checkpoint-dir ./checkpoints \
    --model-variant turbo \
    --dataset-dir ./preprocessed_tensors \
    --output-dir ./lora_output \
    --rank 64 \
    --alpha 128 \
    --learning-rate 1e-4 \
    --epochs 100 \
    --batch-size 1 \
    --gradient-accumulation 4 \
    --warmup-steps 100 \
    --save-every 10 \
    --cfg-ratio 0.15 \
    --gradient-checkpointing

Using the Interactive Wizard

python train.py
# Follow the prompts to select model, dataset, and hyperparameters

Key Hyperparameters

Parameter Default Description
--rank 64 LoRA rank (higher = more capacity, more VRAM)
--alpha 128 LoRA alpha (scaling factor, typically 2× rank)
--learning-rate 1e-4 Learning rate
--epochs 100 Number of training epochs
--batch-size 1 Batch size per device
--gradient-accumulation 4 Gradient accumulation steps (effective batch = 4)
--cfg-ratio 0.15 CFG dropout ratio (15% of steps use null condition)
--optimizer adamw Optimizer: adamw, adamw8bit, adafactor, prodigy
--scheduler cosine LR scheduler: cosine, linear, constant
--gradient-checkpointing Enable to reduce VRAM (~40% less at 2× compute)

VRAM Presets

For low-VRAM GPUs, use built-in presets:

# 8 GB VRAM
python train.py fixed --preset vram_8gb ...

# 12 GB VRAM
python train.py fixed --preset vram_12gb ...

Training Details

  • What gets trained: Only the attention projections (q_proj, k_proj, v_proj, o_proj) in the DiT decoder's 24 transformer layers — 192 LoRA modules total
  • Loss function: MSE flow matching velocity loss: MSE(model_output, noise - data)
  • Timestep sampling: Continuous logit-normal distribution (mu=-0.4, sigma=1.0)
  • CFG dropout: 15% of steps replace conditioning with the learned null embedding
  • Mixed precision: bfloat16 on CUDA, float16 on MPS

Monitor Training

tensorboard --logdir ./lora_output/runs

Step 5: Verify the Output

After training completes, the output directory contains:

lora_output/
├── checkpoints/
│   └── epoch_10_loss_0.1234/
│       ├── adapter_config.json
│       ├── adapter_model.safetensors
│       └── training_state.pt
├── final/
│   ├── adapter_config.json          ← the LoRA weights
│   └── adapter_model.safetensors    ← the LoRA weights
└── runs/                            ← TensorBoard logs

The key files are adapter_config.json and adapter_model.safetensors in the final/ directory. Example adapter_config.json:

{
  "peft_type": "LORA",
  "r": 64,
  "lora_alpha": 128,
  "lora_dropout": 0.1,
  "target_modules": ["k_proj", "v_proj", "o_proj", "q_proj"],
  "bias": "none",
  "task_type": "FEATURE_EXTRACTION"
}

Step 6: Inference with Diffusers

Load and Generate

import torch
import soundfile as sf
from diffusers import AceStepPipeline

# Load the base model (diffusers format)
pipe = AceStepPipeline.from_pretrained(
    "Runware/acestep-v15-turbo-diffusers",
    torch_dtype=torch.float16,
)
pipe = pipe.to("cuda")

# Load your trained LoRA weights
# Diffusers auto-converts the original ACE-Step key names
# (q_proj → to_q, k_proj → to_k, etc.)
pipe.load_lora_weights("./lora_output/final", adapter_name="my_style")

# Generate music
audio = pipe(
    prompt="Chinese traditional music, erhu solo, peaceful and elegant",
    lyrics="[verse]\n春风又绿江南岸\n明月何时照我还",
    audio_duration=30.0,
    num_inference_steps=8,
).audios

# Save as WAV
sf.write("output.wav", audio[0].T.cpu().numpy(), 48000)

Control LoRA Strength

# Scale down LoRA effect (0.0 = no LoRA, 1.0 = full LoRA)
pipe.set_adapters(["my_style"], [0.5])
audio = pipe(prompt="...", lyrics="...", audio_duration=30.0).audios

# Or pass scale per inference call via attention_kwargs
audio = pipe(
    prompt="...",
    lyrics="...",
    audio_duration=30.0,
    attention_kwargs={"scale": 0.7},
).audios

Fuse LoRA for Faster Inference

# Merge LoRA weights into base model (no per-step overhead)
pipe.fuse_lora()
audio = pipe(prompt="...", lyrics="...", audio_duration=30.0).audios

# Undo fusion
pipe.unfuse_lora()

Multiple LoRA Adapters

# Load multiple adapters
pipe.load_lora_weights("./lora_chinese_new_year/", adapter_name="chinese_new_year")
pipe.load_lora_weights("./lora_jazz/", adapter_name="jazz")

# Use one at a time
pipe.set_adapters(["chinese_new_year"], [1.0])
audio_cny = pipe(prompt="festive music", lyrics="...", audio_duration=30.0).audios

# Switch to another
pipe.set_adapters(["jazz"], [1.0])
audio_jazz = pipe(prompt="jazz ballad", lyrics="...", audio_duration=30.0).audios

# Blend two adapters
pipe.set_adapters(["chinese_new_year", "jazz"], [0.6, 0.4])
audio_blend = pipe(prompt="jazz with chinese elements", lyrics="...", audio_duration=30.0).audios

Save in Diffusers Format

from peft.utils import get_peft_model_state_dict

lora_state_dict = get_peft_model_state_dict(pipe.transformer, adapter_name="my_style")
pipe.save_lora_weights("./my_style_diffusers/", transformer_lora_layers=lora_state_dict)

Unload LoRA

pipe.unload_lora_weights()
# Pipeline is back to the base model

Key Name Mapping

When loading LoRA weights trained with the original ACE-Step-1.5 repo into diffusers, the key names are automatically converted:

Original ACE-Step-1.5 Diffusers
base_model.model.layers.0.self_attn.q_proj.lora_A.weight transformer.layers.0.self_attn.to_q.lora_A.weight
base_model.model.layers.0.self_attn.k_proj.lora_B.weight transformer.layers.0.self_attn.to_k.lora_B.weight
base_model.model.layers.0.cross_attn.v_proj.lora_A.weight transformer.layers.0.cross_attn.to_v.lora_A.weight
base_model.model.layers.0.cross_attn.o_proj.lora_B.weight transformer.layers.0.cross_attn.to_out.0.lora_B.weight

This conversion happens transparently inside load_lora_weights() — no manual steps needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation fixes-issue loaders lora models pipelines size/L PR with diff > 200 LOC tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for LoRA / adapters in AceStepPipeline

4 participants