Skip to content

Reject direct injection for tuners with shared adapter state - #3667

Open
CoralGarden52 wants to merge 3 commits into
huggingface:mainfrom
CoralGarden52:fix/reject-direct-injected-shared-tuners
Open

Reject direct injection for tuners with shared adapter state#3667
CoralGarden52 wants to merge 3 commits into
huggingface:mainfrom
CoralGarden52:fix/reject-direct-injected-shared-tuners

Conversation

@CoralGarden52

Copy link
Copy Markdown

Related to #3631

Summary

  • Add a BaseTuner.uses_shared_state capability marker for tuners that keep adapter state shared across multiple target layers.
  • Reject direct injection for TinyLoRA, UniLoRA, VeRA, PVeRA, VBLoRA, and FRoD before the model is modified.
  • Keep get_peft_model support unchanged and document the limitation in the low-level API guide.

inject_adapter_in_model returns the mutated base model, while these tuners store shared adapter state on the tuner object. The direct API therefore cannot reliably support state-dict round-tripping. This change fails early with a clear error and points users to get_peft_model.

Tests

  • 12 passed: shared-state low-level API tests
  • 385 passed, 1 xfailed: core tuner and low-level API tests
  • 39 passed: shared-tuner low-memory loading tests
  • make quality passed
  • Full make test was started; the local run was stopped after an unrelated numerical failure in tests/test_custom_models.py::TestPeftCustomModel::test_merge_layers[Conv1d LOHA 3-Conv1dBigger-LoHaConfig-config_kwargs91] (torch.allclose during LoHa Conv1D merge).

@BenjaminBossan BenjaminBossan self-assigned this Sep 7, 2026
@BenjaminBossan

Copy link
Copy Markdown
Member

@CoralGarden52 Thanks for the PR. As it's in draft state, I assume it's not ready for review yet. If it is, mark it as ready and ping me please.

@CoralGarden52
CoralGarden52 marked this pull request as ready for review September 7, 2026 15:29
@CoralGarden52

Copy link
Copy Markdown
Author

Hi @BenjaminBossan, thanks for checking. I鈥檝e marked the PR as ready for review now.

The implementation follows your suggested class-level capability marker approach. It adds BaseTuner.uses_shared_state, rejects direct injection for the affected shared-state tuners before modifying the model, and keeps get_peft_model supported.

The targeted tests and quality checks pass. The unrelated local LoHa Conv1D numerical failure is documented in the PR description.

Could you please take another look when convenient?

@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.

@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 providing this fix and corresponding tests. I have a few comments, please check.

What's missing for me is another test though: Imagine a new PEFT method with shared state is added that forgets to set uses_shared_state = True. As is, we wouldn't notice this, same as we didn't notice for the existing methods. Therefore, we need to add one more test that checks the specific roundtrip failure you describe in #3631.

As to where to put this: Normally, test_low_level_api.py would be the best place, as this is the API we want to test. However, in that file, we don't have a full matrix of all PEFT methods. Adding it there would be too much. Thus, I think the next best place to put this test is in test_custom_models.py. Use set_init_weights_false to ensure that the PEFT adapter is not an identity transform at the start and has no effect. LMK if you have questions.

Comment thread tests/test_low_level_api.py Outdated


@pytest.mark.parametrize("config_cls, config_kwargs", SHARED_STATE_TUNER_CONFIGS)
def test_inject_adapter_in_model_rejects_shared_state_tuners(config_cls, config_kwargs):

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.

Could you please move this tests into TestLowLevelFunctional, add them after the existing tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I moved the shared-state rejection test into TestLowLevelFunctional and placed it immediately after the existing test_inject_adapter_in_model test.

Comment thread tests/test_low_level_api.py Outdated


@pytest.mark.parametrize("config_cls, config_kwargs", SHARED_STATE_TUNER_CONFIGS)
def test_get_peft_model_supports_shared_state_tuners(config_cls, config_kwargs):

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.

No need for this test, if this would fail, a bunch of existing tests would be red in the CI.

Note that if you remove this test, there is no further need for a separate SHARED_STATE_TUNER_CONFIGS constant, so you can roll the list directly into the parametrize of test_inject_adapter_in_model_rejects_shared_state_tuners

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I removed the redundant test_get_peft_model_supports_shared_state_tuners test. The shared-state configuration list is no longer defined as a separate constant and is now inlined directly into the parametrization of test_inject_adapter_in_model_rejects_shared_state_tuners.

The existing test matrix already covers the get_peft_model path.

@CoralGarden52

Copy link
Copy Markdown
Author

Hi @BenjaminBossan,

I added the requested full-matrix regression test in tests/test_custom_models.py.

The test uses TEST_CASES and set_init_weights_false, injects the adapter directly into the source model, obtains its state dict with get_peft_model_state_dict, then injects a fresh model with a different initialization seed and restores it with set_peft_model_state_dict. It checks for unexpected keys and verifies that the outputs are identical after the round-trip.

For tuners already marked with uses_shared_state = True, the expected ValueError is accepted. If a future shared-state tuner forgets to set this marker, direct injection will proceed and the round-trip assertions will catch the missing shared state described in #3631.

The full matrix passes with 331 tests passed.

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.

3 participants