Skip to content

feat: support LoRA conversion for multiplicative PEFT methods (OFT, BOFT)#3360

Draft
peft-jambot wants to merge 7 commits into
huggingface:mainfrom
peft-jambot:FEAT-get-additive-delta
Draft

feat: support LoRA conversion for multiplicative PEFT methods (OFT, BOFT)#3360
peft-jambot wants to merge 7 commits into
huggingface:mainfrom
peft-jambot:FEAT-get-additive-delta

Conversation

@peft-jambot

@peft-jambot peft-jambot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Description

Add support for converting multiplicative PEFT methods (OFT, BOFT) to LoRA via convert_to_lora. Previously, only additive methods (where W' = W + ΔW) could be converted, because convert_to_lora used get_delta_weight() to obtain the delta and SVD-approximate it. Multiplicative methods like OFT (W' = W @ R) and BOFT (W' = diag(s) @ W @ R) return a transformation matrix from get_delta_weight(), not an additive delta.

This PR introduces a get_additive_delta() method on BaseTunerLayer that computes the actual additive delta W' - W for any PEFT method. For additive methods, the default implementation simply delegates to get_delta_weight(). For multiplicative methods, the method is overridden to construct the effective weight W' and subtract the base weight W.

Fork issue: peft-jambot#4

Changes

  • src/peft/tuners/tuners_utils.py: Added get_additive_delta() method to BaseTunerLayer. Default implementation returns get_delta_weight(adapter_name), which works for all existing additive methods with zero changes.
  • src/peft/tuners/oft/layer.py: Override get_additive_delta() for OFT.Linear — computes W @ R^T - W where R is the orthogonal rotation matrix. Also enables supports_lora_conversion() = True.
  • src/peft/tuners/boft/layer.py: Override get_additive_delta() for BOFT.Linear — computes (W @ R^T) * s - W where R is the rotation matrix and s is the scaling vector. Also enables supports_lora_conversion() = True.
  • src/peft/tuners/lora/conversion.py: Updated _convert_module_to_lora() and convert_to_lora() to call get_additive_delta() instead of get_delta_weight(). Updated docstrings and error messages accordingly.
  • tests/test_lora_conversion.py: Added TestMultiplicativeLoraConversion class with 7 tests covering OFT and BOFT conversion: supports_lora_conversion checks, approximation quality (MSE < 0.1), targeted module mapping, and save_as_lora round-trip.

How I tested

  • pytest tests/test_lora_conversion.py -v38 passed (31 existing + 7 new)
  • pytest tests/test_custom_models.py -k "OFT and not Quant"1788 passed, 52 skipped
  • pytest tests/test_custom_models.py -k "BOFT"557 passed, 13 skipped
  • make style → all checks passed

No GPU/quantization tests were run, as the changes only affect the LoRA conversion code path, which operates on dequantized weights.

AI assistance statement

This PR was prepared with AI assistance (Hermes Agent). The human submitter reviewed every changed line and ran the relevant tests.

…OFT)

Add get_additive_delta method to BaseTunerLayer that returns the additive
delta W' - W for any PEFT method. The default implementation delegates to
get_delta_weight (correct for all additive methods like LoRA, LoKr, LoHa).
Override get_additive_delta in OFT and BOFT Linear layers to compute the
actual additive delta by constructing the effective weight W' from the
multiplicative transformation and subtracting the base weight W.

Update conversion.py to call get_additive_delta instead of get_delta_weight,
enabling LoRA conversion for multiplicative methods. Enable
supports_lora_conversion on OFT and BOFT Linear layers.

Key math:
- OFT: W' = W @ R^T, delta = W @ R^T - W
- BOFT: W' = (W @ R^T) * s, delta = (W @ R^T) * s - W

Tests: Added TestMultiplicativeLoraConversion class in test_lora_conversion.py
with 7 tests covering OFT and BOFT conversion (supports_lora_conversion,
approximation quality, targeted modules identity, save_as_lora round-trip).
All 38 tests in test_lora_conversion.py pass. 1788 OFT/BOFT tests in
test_custom_models.py pass.

@BenjaminBossan BenjaminBossan 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 for the PR. I've added some comments, please check.

Comment thread src/peft/tuners/tuners_utils.py Outdated
"""
Return the additive delta weight W' - W for the given adapter.

For additive PEFT methods (e.g. LoRA, LoKr, LoHa), this is equivalent to ``get_delta_weight``. For

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.

Please always use single backticks in all 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.

Addressed in commit 176f598.

Comment thread src/peft/tuners/tuners_utils.py
Comment thread tests/test_lora_conversion.py Outdated
def test_boft_supports_lora_conversion(self, boft_model):
assert boft_model.supports_lora_conversion()

def test_oft_converted_lora_approximates_original(self, oft_model):

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.

Isn't this already covered by _test_lora_conversion in testing_common.py?

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.

Addressed in commit 176f598.

Comment thread tests/test_lora_conversion.py Outdated
mse_converted = self.get_mse(output_converted, output_oft)
assert 0.0 < mse_converted < 0.1, f"OFT conversion MSE too high: {mse_converted}"

def test_boft_converted_lora_approximates_original(self, boft_model):

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.

Isn't this already covered by _test_lora_conversion in testing_common.py?

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.

Addressed in commit 176f598.

Comment thread tests/test_lora_conversion.py Outdated
mse_converted = self.get_mse(output_converted, output_boft)
assert 0.0 < mse_converted < 0.1, f"BOFT conversion MSE too high: {mse_converted}"

def test_oft_targeted_modules_identical(self, oft_model):

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.

This test is unnecessary.

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.

Addressed in commit 176f598.

Comment thread tests/test_lora_conversion.py Outdated
modules_lora = {k.rsplit(".", 2)[0] for k in lora_state_dict.keys() if ".lora" in k}
assert modules_oft == modules_lora

def test_boft_targeted_modules_identical(self, boft_model):

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.

This test is unnecessary.

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.

Addressed in commit 176f598.

Comment thread tests/test_lora_conversion.py Outdated
modules_lora = {k.rsplit(".", 2)[0] for k in lora_state_dict.keys() if ".lora" in k}
assert modules_boft == modules_lora

def test_oft_save_as_lora(self, oft_model, tmp_path):

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.

This test is unnecessary.

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.

Addressed in commit 176f598.

… override in additive methods

Address review feedback on PR huggingface#3360:
- Change BaseTunerLayer.get_additive_delta to raise NotImplementedError by
  default instead of calling get_delta_weight, since not all PEFT methods
  are additive
- Add get_additive_delta override to each additive PEFT method that
  supports LoRA conversion (LoRA, LoKr, LoHa, Vera, DeLoRA, RandLora,
  VBLoRA, C3A, Psoft, GraLoRA, FourierFT, WaveFT, TinyLora)
- Fix double backticks to single backticks in docstrings
- Remove 5 unnecessary tests already covered by _test_lora_conversion
  in testing_common.py
@peft-jambot

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Thanks for the review! I've addressed all comments in commit 176f598:

  1. Single backticks in docstrings — Fixed in tuners_utils.py. The OFT and BOFT docstrings already used single backticks.

  2. get_additive_delta raises NotImplementedError by defaultBaseTunerLayer.get_additive_delta now raises NotImplementedError instead of calling get_delta_weight. Each additive PEFT method that supports LoRA conversion now overrides get_additive_delta to return self.get_delta_weight(adapter_name):

    • LoRA, LoKr, LoHa, Vera, DeLoRA, RandLora, VBLoRA, C3A, Psoft, GraLoRA, FourierFT, WaveFT, TinyLora
  3. Removed 5 unnecessary tests — The OFT/BOFT conversion approximation tests were already covered by _test_lora_conversion in testing_common.py (run via test_decoder_models.py::test_lora_conversion with the OFT/BOFT configs). The targeted modules and save_as_lora tests were also removed. Kept the supports_lora_conversion tests since those verify the method is wired up correctly.

All tests pass:

  • tests/test_lora_conversion.py: 33 passed
  • tests/test_decoder_models.py -k 'test_lora_conversion and (OFT or BOFT)': 15 passed, 3 skipped (GPT2)

@BenjaminBossan BenjaminBossan 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 for the updates. There is still a bit of work to do, please check my inline comments and the discussion below.

As a bit of a clean up, could you please remove the supports_lora_conversion methods from HiRA and SHiRA, as they just return False and are thus unnecessary.

Technically speaking, MiSS doesn't require get_additive_delta because its conversion is special cased. But I think it would still be good to implement it. IIUC, when initializing MiSS with "bat", the delta_weight is already the additive delta_weight, check the code in conversion.py::_convert_miss_module_to_lora. So this case can be moved to get_additive_delta and then _convert_miss_module_to_lora only needs to be called when not using "bat", otherwise we just use the normal conversion path.

For non-bat, we still need the special code from _convert_miss_module_to_lora, but it can be simplified once the "bat" case has been factored out.

Comment thread tests/test_lora_conversion.py Outdated
Comment on lines +736 to +740
def test_oft_supports_lora_conversion(self, oft_model):
assert oft_model.supports_lora_conversion()

def test_boft_supports_lora_conversion(self, boft_model):
assert boft_model.supports_lora_conversion()

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.

These tests aren't really helpful. Let's delete them.

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.

Deleted these tests in commit 6a4247a. The TestMultiplicativeLoraConversion class is now empty and has been removed as well.

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.

In testing_common.py::_test_lora_conversion, we currently early return for PEFT methods that don't support LoRA conversion. That makes it hard to see which methods were actually tested and which ones just early returned. Please change this to use pytest.skip with an appropriate message instead.

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.

Changed to use pytest.skip with a message like 'XxxConfig does not support LoRA conversion' in commit 6a4247a. Unsupported methods now show up as SKIPPED instead of silently passing.

- Remove unnecessary supports_lora_conversion overrides from HiRA and
  SHiRA that just return False (base class default)
- Implement get_additive_delta for MiSS bat mode, factoring the bat case
  out of _convert_miss_module_to_lora into the generic get_additive_delta
  + SVD path; _convert_miss_module_to_lora now only handles standard/mini
- Replace early return with pytest.skip in _test_lora_conversion so
  unsupported PEFT methods show as skipped with a clear message
- Delete unhelpful supports_lora_conversion assertion tests for OFT/BOFT
  and the now-empty TestMultiplicativeLoraConversion class
- Add get_additive_delta to UniLora (new upstream tuner that declares
  supports_lora_conversion=True but was missing the method)
- Remove unnecessary supports_lora_conversion overrides from HiRA and
  SHiRA that just return False (base class default)
- Implement get_additive_delta for MiSS bat mode, factoring the bat case
  out of _convert_miss_module_to_lora into the generic get_additive_delta
  + SVD path; _convert_miss_module_to_lora now only handles standard/mini
- Replace early return with pytest.skip in _test_lora_conversion so
  unsupported PEFT methods show as skipped with a clear message
- Delete unhelpful supports_lora_conversion assertion tests for OFT/BOFT
  and the now-empty TestMultiplicativeLoraConversion class
- Add get_additive_delta to UniLora (new upstream tuner that declares
  supports_lora_conversion=True but was missing the method)
@peft-jambot

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Thanks for the review! I've addressed all comments in commit 6a4247a:

  1. Removed supports_lora_conversion from HiRA and SHiRA — Both methods just returned False, which is the BaseTunerLayer default, so the overrides were unnecessary.

  2. Implemented get_additive_delta for MiSS and factored out the bat case — MiSS bat mode now implements get_additive_delta (delegating to get_delta_weight), and the bat-specific SVD code has been removed from _convert_miss_module_to_lora. The _convert_module_to_lora routing now checks module.miss_fn != "bat" so that bat goes through the generic get_additive_delta + SVD path, while standard/mini still uses the exact rank-r factorization.

  3. Replaced early return with pytest.skip in _test_lora_conversion — Unsupported PEFT methods now show as SKIPPED with a clear message (e.g. HiraConfig does not support LoRA conversion) instead of silently passing.

  4. Deleted the unhelpful supports_lora_conversion assertion tests for OFT/BOFT and the now-empty TestMultiplicativeLoraConversion class.

  5. Added get_additive_delta to UniLora — This is a new additive tuner from upstream (PR Add UniLora tuner to PEFT #3257) that declares supports_lora_conversion = True but was missing get_additive_delta, causing the test_lora_conversion tests to fail with NotImplementedError.

All tests pass:

  • tests/test_lora_conversion.py: 31 passed
  • tests/test_decoder_models.py -k "test_lora_conversion": 82 passed, 134 skipped (the skips are the PEFT methods that don't support LoRA conversion, now visible with clear messages)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants