FIX Multiple issues with target_parameters#2710
Conversation
There are a few issues with target_parameters that are fixed in this PR. Existing parametrizations When using target_parameters with LoRA, after the forward call finishes, the LoRA parametrization is removed. However, this also used to remove all other parametrizations on the same parameter, which is bad. With this PR, only the LoRA parametrization is removed. Module repr This PR also extends the __repr__ of lora.ParamWrapper to contain the parameter name, which makes it more useful. Multiple LoRA adapters with target_parameters There is an issue when adding a second LoRA adapter with target_paramters, where this second adapter would not actually be applied correctly. The corresponding unit test was too lax to notice the bug. This is not easy to fix, so for now we forbid adding a second adapter with target_parameters. This is very strict but it's better than having silent errors. Although it was possible to fix that specific issue, the solution resulted in ever deeply nested adapters (i.e. with multiple .base_layer). This in turn results in those infixes to be part of the state_dict. But then we cannot load the individual adapters correctly, except if the model is restored in the exact same order as it was previously created. This is not normally a requirement in PEFT (e.g. I can create a model with two adapters and later decide to load only one of them). In the long run, we need to think about solutions that would allow this. It may require some form of normalization of the layers to prevent ever deeper nesting. Also, what is ugly right now is that, given that the LoRA lives on a module but actually targets one of possibly multiple parameter, the LoRA weights don't actually reference said parameter in any name. That means, purely from the state_dict, it is unclear which parameter a LoRA weight belongs to. Ideally, this should be encoded in the LoRA weight key.
|
@matthewdouglas The first part of the fixes in this PR should hopefully address the issue we discussed internally with bnb. Check out the tests that show this: https://github.com/huggingface/peft/pull/2710/files#diff-c8dcf5ce96401fd88057132c84ee4a25da3550574250c9a1857b8aa1672ea540R406 |
|
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. |
|
To better illustrate the existing issues with multiple adapters, check out this script: from transformers import AutoModelForCausalLM
from peft import LoraConfig, PeftModel, get_peft_model
from safetensors.torch import load_file
model_id = "trl-internal-testing/tiny-Llama4ForCausalLM"
path = "/tmp/peft/target-params"
model = AutoModelForCausalLM.from_pretrained(model_id)
config = LoraConfig(
target_modules=[],
target_parameters=[
"feed_forward.experts.down_proj",
"feed_forward.experts.gate_up_proj",
],
)
model = get_peft_model(model, config)
model.add_adapter("other", config)
print(model)
model.save_pretrained(path)
sd = load_file(path + "/adapter_model.safetensors")
print("default sd")
for k in sd:
print(k)
print("other sd")
sd_other = load_file(path + "/other/adapter_model.safetensors")
for k in sd:
print(k)
del model
# this works
print("loading in same order")
model = AutoModelForCausalLM.from_pretrained(model_id)
model = PeftModel.from_pretrained(model, path)
out = model.load_adapter(path + "/other", adapter_name="other")
print(out)
del model
print("loading in reverse order, should theoretically not matter!")
model = AutoModelForCausalLM.from_pretrained(model_id)
model = PeftModel.from_pretrained(model, path + "/other", adapter_name="other")
out = model.load_adapter(path, adapter_name="default")
print(out)Running this gives us: We create a LoRA model with two adapters that use
|
|
I'm investigating the failing CI. Update: Done, a new test was not implemented correctly. |
githubnemo
left a comment
There was a problem hiding this comment.
Two small comments, otherwise LGTM. Thanks for handling the weirdness target params entail.
| rep = super().__repr__() | ||
| idx = rep.find("(") + 1 | ||
| # insert the name of the parameter | ||
| # insert the name of the parameter to allow the repr to be disambiguous when multiple parameters on the same mdule |
There was a problem hiding this comment.
| # insert the name of the parameter to allow the repr to be disambiguous when multiple parameters on the same mdule | |
| # insert the name of the parameter to allow the repr to be disambiguous when multiple parameters on the same module |
| self.targeted_parameter_names.append(key) | ||
| else: | ||
| # Standard case: the parameter is not already parametrized. Note, however, that the model could already | ||
| # be nested with lora.ParamWrapper. |
There was a problem hiding this comment.
Worthwhile adding how this comes to be (being already nested with ParamWrapper). I don't think that's obvious.
There are a few issues with target_parameters that are fixed in this PR. Existing parametrizations When using target_parameters with LoRA, after the forward call finishes, the LoRA parametrization is removed. However, this also used to remove all other parametrizations on the same parameter, which is bad. With this PR, only the LoRA parametrization is removed. Module repr This PR also extends the __repr__ of lora.ParamWrapper to contain the parameter name, which makes it more useful. Extend testing Added a tiny gpt-oss model to the target_parameters test suite. Multiple LoRA adapters with target_parameters There is an issue when adding a second LoRA adapter with target_paramters, where this second adapter would not actually be applied correctly. The corresponding unit test was too lax to notice the bug. This is not easy to fix, so for now we forbid adding a second adapter with target_parameters. This is very strict but it's better than having silent errors. Although it was possible to fix that specific issue, the solution resulted in ever deeply nested adapters (i.e. with multiple .base_layer). This in turn results in those infixes to be part of the state_dict. But then we cannot load the individual adapters correctly, except if the model is restored in the exact same order as it was previously created. This is not normally a requirement in PEFT (e.g. I can create a model with two adapters and later decide to load only one of them). In the long run, we need to think about solutions that would allow this. It may require some form of normalization of the layers to prevent ever deeper nesting. Also, what is ugly right now is that, given that the LoRA lives on a module but actually targets one of possibly multiple parameter, the LoRA weights don't actually reference said parameter in any name. That means, purely from the state_dict, it is unclear which parameter a LoRA weight belongs to. Ideally, this should be encoded in the LoRA weight key.
* FIX Multiple issues with target_parameters (#2710) * Bump version to 0.17.1
There are a few issues with target_parameters that are fixed in this PR. Existing parametrizations When using target_parameters with LoRA, after the forward call finishes, the LoRA parametrization is removed. However, this also used to remove all other parametrizations on the same parameter, which is bad. With this PR, only the LoRA parametrization is removed. Module repr This PR also extends the __repr__ of lora.ParamWrapper to contain the parameter name, which makes it more useful. Extend testing Added a tiny gpt-oss model to the target_parameters test suite. Multiple LoRA adapters with target_parameters There is an issue when adding a second LoRA adapter with target_paramters, where this second adapter would not actually be applied correctly. The corresponding unit test was too lax to notice the bug. This is not easy to fix, so for now we forbid adding a second adapter with target_parameters. This is very strict but it's better than having silent errors. Although it was possible to fix that specific issue, the solution resulted in ever deeply nested adapters (i.e. with multiple .base_layer). This in turn results in those infixes to be part of the state_dict. But then we cannot load the individual adapters correctly, except if the model is restored in the exact same order as it was previously created. This is not normally a requirement in PEFT (e.g. I can create a model with two adapters and later decide to load only one of them). In the long run, we need to think about solutions that would allow this. It may require some form of normalization of the layers to prevent ever deeper nesting. Also, what is ugly right now is that, given that the LoRA lives on a module but actually targets one of possibly multiple parameter, the LoRA weights don't actually reference said parameter in any name. That means, purely from the state_dict, it is unclear which parameter a LoRA weight belongs to. Ideally, this should be encoded in the LoRA weight key.
Resolves #3340 Context So far, we did not allow adding multiple LoRA adapters with target_parameters on the same layer. This was a known limitation. I have already attempted to solve this once (see #2710) but didn't have time to come up with a nice solution. As it was unclear if there was any real world need to support this, there was no further work on it since then. Now we know that there are practical application that may need it, so I resumed the work. What doesn't work The previous solution attempted to solve the issue by nesting the lora.ParamWrappers. So for adapters 'default' and 'other', we would end up having something like: param_wrapper_default(param_wrapper_other(base_layer)) This was problematic. Not only could this result in very deep nesting, which is inefficient. What's worse is that state_dict key for 'other' would contain 'base_layer.' as an infix. Therefore, if we wanted to load the 'other' adapter _without_ first loading the 'default' adapter, we would get a key mismatch. We could also not simply strip out 'base_layer.' infix because we use nesting to deal with multiple nn.Parameters on the same module, so to account for that, we need to keep the infix. Solution The solution is pretty straightfoward: We use the existing mechanism to store the parameters for the other adapter in the nn.ModuleDict. For this, we detect if the layer is already a ParamWrapper when adding the second adapter and update that layer instead of nesting it. Caveat This simple approach can, however, not work with multiple adapters that target a different set of parameters. This is because the information which parameter is targeted is not stored in the state_dict itself. Therefore, if we had different adapters targeting different parameters, we would not be able to tell which parameter is meant to be targeted.
There are a few issues with target_parameters that are fixed in this PR.
Existing parametrizations
When using
target_parameterswith LoRA, after the forward call finishes, the LoRA parametrization is removed. However, this also used to remove all other parametrizations on the same parameter, which is bad. With this PR, only the LoRA parametrization is removed.Module
reprThis PR also extends the
__repr__oflora.ParamWrapperto contain the parameter name, which makes it more useful.Extend testing
Added a tiny gpt-oss model to the
target_parameterstest suite.Multiple LoRA adapters with
target_parametersThere is an issue when adding a second LoRA adapter with
target_paramters, where this second adapter would not actually be applied correctly. The corresponding unit test was too lax to notice the bug. This is not easy to fix, so for now we forbid adding a second adapter withtarget_parameters. This is very strict but it's better than having silent errors.Although it was possible to fix that specific issue, the solution resulted in ever deeply nested adapters (i.e. with multiple
.base_layer). This in turn results in those infixes to be part of thestate_dict. But then we cannot load the individual adapters correctly, except if the model is restored in the exact same order as it was previously created. This is not normally a requirement in PEFT (e.g. I can create a model with two adapters and later decide to load only one of them).In the long run, we need to think about solutions that would allow this. It may require some form of normalization of the layers to prevent ever deeper nesting. Also, what is ugly right now is that, given that the LoRA lives on a module but actually targets one of possibly multiple parameter, the LoRA weights don't actually reference said parameter in any name. That means, purely from the
state_dict, it is unclear which parameter a LoRA weight belongs to. Ideally, this should be encoded in the LoRA weight key.