Skip to content

Add ace step lora support#14193

Open
chenyangzhu1 wants to merge 6 commits into
huggingface:mainfrom
chenyangzhu1:add-ace-step-lora-support
Open

Add ace step lora support#14193
chenyangzhu1 wants to merge 6 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 3 commits July 16, 2026 16:17
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for LoRA / adapters in AceStepPipeline

2 participants