Add ace step lora support#14193
Conversation
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>
|
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 Environment
Generation Parametersprompt = "chinese traditional music, erhu solo, peaceful and elegant"
lyrics = "[verse]\n春风又绿江南岸\n明月何时照我还"
audio_duration = 10.0 # seconds
num_inference_steps = 8
seed = 42How to ReproduceStep 1: Install the modified diffuserscd /path/to/diffusers
git checkout add-ace-step-lora-support
pip install -e .
pip install peft soundfileStep 2: Run the generation scriptimport 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
Audio files are saved under Conclusions
|
| @unittest.skip("AceStepPipeline does not accept attention_kwargs for LoRA scale.") | ||
| def test_lora_scale_kwargs_match_fusion(self): | ||
| pass |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
We should add attention_kwargs to the pipeline call then like other LoRA-supported pipelines do.
There was a problem hiding this comment.
We should add
attention_kwargsto the pipeline call then like other LoRA-supported pipelines do.
Fixed in 2c49f32
| @unittest.skip("AceStep uses 'layers' not 'transformer_blocks'/'blocks'; base test hardcodes block names.") | ||
| def test_lora_fuse_nan(self): | ||
| pass |
There was a problem hiding this comment.
We can modify the test to include the block name of AceStep.
sayakpaul
left a comment
There was a problem hiding this comment.
Left some comments around attention_kawargs we should support it.
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>
What does this PR do?
Adds standard diffusers LoRA support to
AceStepPipeline, enabling users to load, apply, fuse, and save LoRA adapters for theAceStepTransformer1DModelusing the familiar diffusers API:Key features
load_lora_weights,set_adapters,fuse_lora,unfuse_lora,save_lora_weights,delete_adapters,enable_lora,disable_loraq_proj/k_proj/v_proj/o_projnaming, while diffusers usesto_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.set_adapters.Changes
src/diffusers/models/transformers/ace_step_transformer.pyPeftAdapterMixintoAceStepTransformer1DModelinheritancesrc/diffusers/loaders/lora_conversion_utils.py_convert_non_diffusers_ace_step_lora_to_diffusers()conversion functionsrc/diffusers/loaders/lora_pipeline.pyAceStepLoraLoaderMixin(LoraBaseMixin)classsrc/diffusers/loaders/__init__.pyAceStepLoraLoaderMixinsrc/diffusers/pipelines/ace_step/pipeline_ace_step.pyAceStepLoraLoaderMixintoAceStepPipelineinheritancetests/lora/test_lora_layers_ace_step.pyConversion details
The conversion function handles the key name mapping between the original ACE-Step-1.5 PEFT format and diffusers format:
Mapping rules:
base_model.model.prefix.q_proj.→.to_q.,.k_proj.→.to_k.,.v_proj.→.to_v.,.o_proj.→.to_out.0.transformer.prefixDesign decisions
AceStepTransformer1DModelreceives LoRA adapters, matching the original ACE-Step-1.5 training setup where only the DiT decoder is fine-tuned.Mochi1LoraLoaderMixin/CogVideoXLoraLoaderMixinpattern (simplest transformer-only LoRA mixin).AceStepPipelinedoes not exposeattention_kwargsfor per-step LoRA scale control, the transformer useslayersinstead oftransformer_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 tokenizerFIXED in commit
de90a1e7a. Changed from/nas/zcy/github_issue/models/Qwen3-Embedding-0.6B-tokenizerto"Qwen/Qwen3-Embedding-0.6B".2.make styleandmake fix-copiesnot runFIXED in commit
de90a1e7a.ruffapplied one spacing fix inlora_conversion_utils.py.make fix-copiesproduced no changes.Non-blocking Issues (for reviewer)
1.
save_lora_weightserror message mentionstext_encoder_lora_layersbut AceStep doesn't support itsrc/diffusers/loaders/lora_pipeline.py—AceStepLoraLoaderMixin.save_lora_weights:This is copied verbatim from
CogVideoXLoraLoaderMixin(annotated with# Copied from). AceStep only supports transformer LoRA, so thetext_encoder_lora_layersreference is misleading. However, modifying this line would break the# Copied fromsync mechanism. Leaving for reviewer to decide whether to break the copy link or keep as-is.2.
lora_state_dictmethod has no# Copied fromannotationThe 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_stepblock). This custom logic prevents using a standard# Copied fromannotation. The deviation is intentional and minimal.3. Documentation not updated
docs/source/en/api/pipelines/ace_step.mddoes 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
tests/lora/test_lora_layers_ace_step.py—noiseandinput_idsinget_dummy_inputs(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_simple_inference_with_text_denoiser_block_scaletest_simple_inference_with_text_denoiser_block_scale_for_all_dict_optionstest_modify_padding_modetest_simple_inference_with_text_denoiser_multi_adapter_block_loratest_correct_lora_configs_with_different_rankstest_simple_inference_with_text_denoiser_lora_and_scaleAceStepPipeline.__call__has noattention_kwargs/cross_attention_kwargs/joint_attention_kwargsparametertest_lora_scale_kwargs_match_fusiontest_set_adapters_match_attention_kwargstest_simple_inference_with_text_lora_and_scaletest_lora_B_biasbias=False;lora_biasis not applicabletest_lora_fuse_nantransformer_blocks,blocks, etc.); AceStep useslayersSummary
Verdict: READY
ACE-Step-v1.5-chinese-new-year-LoRA, r=64, alpha=128)make styleandmake fix-copiescleanFixes #14060
Before submitting
self-reviewskill on the diff?documentation guidelines, and
here are tips on formatting docstrings.
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