-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Peft model signature #784
Peft model signature #784
Conversation
Thanks for the PR. I see where you're coming from and think there is value in your suggestion. I'm always a little bit careful when working with inspect, local functions, overwriting attributes, and descriptors, as there can be strange edge cases where they fail. When I your suggestion, I immediately thought that # inside PeftModel.__init__
update_wrapper(self.forward, self.get_base_model().forward) but that actually raises an error. I tried a few different things around that idea but nothing works. It seems to be somehow related to those methods being instance methods, but I'm really stumped. Maybe you have an idea what the issue is? I tried to isolate the issue but at the end of the day, I'm really non the wiser: # normal functions work
def spam(x, y=2) -> int:
"""spam"""
return x - y
def eggs(x, y=3) -> float:
"""eggs"""
return x + y + 1.0
update_wrapper(spam, eggs)
help(spam)
# prints
# Help on function eggs in module __main__:
# eggs(x, y=3) -> float
# eggs
# unbound methods work
class SpamAndEggs:
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
update_wrapper(SpamAndEggs.spam, SpamAndEggs.eggs)
help(SpamAndEggs().spam)
# prints
# Help on method eggs in module __main__:
# eggs(x, y=3) -> float method of __main__.SpamAndEggs instance
# eggs
# bound instance methods don't work
class SpamAndEggs:
def __init__(self):
update_wrapper(self.spam, self.eggs)
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
help(SpamAndEggs().spam)
# raises
# AttributeError: 'method' object has no attribute '__module__'
# even though the method actually *does* have an attribute '__module__' !!! So only the last case with bound instance methods fails, but that would be exactly what we need here. I really feel like there must be a simple solution and I'm just not seeing it, but I couldn't get it to work and my google-foo failed me. |
It seems the main issue with the above approach (skipping the from functools import update_wrapper
class SpamAndEggs:
def __init__(self):
update_wrapper(self.eggs, self.spam, assigned=('__doc__', '__name__', '__annotations__'))
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
help(SpamAndEggs().eggs)
# raises
# AttributeError: attribute '__doc__' of 'method' objects is not writable' which occurs again if you do class SpamAndEggs:
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
SpamAndEggs().eggs.__doc__ = 'spam' but this variant does work from functools import update_wrapper, wraps
class SpamAndEggs:
def __init__(self):
self._update_wrapper()
pass
def _update_wrapper(self):
def new_eggs(*args, **kwargs):
return self.bacon(*args, **kwargs)
update_wrapper(new_eggs, self.spam, assigned=('__doc__', '__name__', '__annotations__'))
self.eggs = new_eggs
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
def bacon(self, x, y=3) -> float:
"""bacon"""
return x + y + 1.0
help(SpamAndEggs().eggs)
print(f"6 Eggs: {SpamAndEggs().eggs(6)}")
# Output:
Help on function spam in module __main__:
spam(x, y=2) -> int
spam
6 Eggs: 10.0 It has bacon mehtod to avoid recursion limit as new_eggs would call itself when it is overwritten in `self.eggs = new_eggs This implementation seems to work on PEFT too def _update_forward_signature(self):
def new_forward(*args, **kwargs):
return self.get_base_model()(*args, **kwargs)
update_wrapper(new_forward, self.base_model.forward, assigned=('__doc__', '__name__', '__annotations__'))
self.forward = new_forward I've run the tests locally and the following fail with current implementation and proposed above changes, but they also fail in the main branch
I had to comment out |
Thanks for investigating. Yes, your proposal works, I could reproduce it, but unfortunately it also requires a locally defined function, which is one of the things I wanted to avoid. E.g. this fails now: import pickle
_ = pickle.dumps(SpamAndEggs()) So the Even though your suggestion is a nice quality of life improvement, I wouldn't want to sacrifice this functionality for it. If we find some way to make it work without that compromise, I would be in favor. |
Not sure what the issue is here, but CI is green, so maybe it's something about your local environment. |
I see that is an issue for sure, it can be made pickable by changing the local function to a normal function, but it creates its own set of issues, which can be one of the following depending if you include def eggs(self, *args, **kwargs):
return self.bacon(*args, **kwargs)
class SpamAndEggs:
def __init__(self):
self._update_wrapper()
pass
def _update_wrapper(self):
update_wrapper(eggs, SpamAndEggs.spam, assigned=('__doc__', '__annotations__'))
self.eggs = MethodType(eggs, self)
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
def bacon(self, x, y=3) -> float:
"""bacon"""
return x + y + 1.0
spam_and_eggs = SpamAndEggs()
help(spam_and_eggs.eggs)
help(spam_and_eggs.spam)
print(f"6 Eggs: {spam_and_eggs.eggs(6)}")
print(f"6 spam: {spam_and_eggs.spam(6)}")
with open("test.pkl", "wb") as f:
pickle.dump(spam_and_eggs, f)
with open("test.pkl", "rb") as f:
spam_and_eggs = pickle.load(f)
help(spam_and_eggs.eggs)
help(spam_and_eggs.spam)
print(f"6 Eggs: {spam_and_eggs.eggs(6)}")
print(f"6 spam: {spam_and_eggs.spam(6)}") Output:
While if you include name it uploads the wrong functionality from functools import update_wrapper, wraps
from types import MethodType
import pickle
def eggs(self, *args, **kwargs):
return self.bacon(*args, **kwargs)
class SpamAndEggs:
def __init__(self):
self._update_wrapper()
pass
def _update_wrapper(self):
update_wrapper(eggs, SpamAndEggs.spam, assigned=('__doc__', '__annotations__', '__name__'))
self.eggs = MethodType(eggs, self)
def spam(self, x, y=2) -> int:
"""spam"""
return x - y
def eggs(self, x, y=3) -> float:
"""eggs"""
return x + y + 1.0
def bacon(self, x, y=3) -> float:
"""bacon"""
return x + y + 1.0
spam_and_eggs = SpamAndEggs()
help(spam_and_eggs.eggs)
help(spam_and_eggs.spam)
print(f"6 Eggs: {spam_and_eggs.eggs(6)}")
print(f"6 spam: {spam_and_eggs.spam(6)}")
with open("test.pkl", "wb") as f:
pickle.dump(spam_and_eggs, f)
with open("test.pkl", "rb") as f:
spam_and_eggs = pickle.load(f)
help(spam_and_eggs.eggs)
help(spam_and_eggs.spam)
print(f"6 Eggs: {spam_and_eggs.eggs(6)}")
print(f"6 spam: {spam_and_eggs.spam(6)}")
I think maybe the simplest solution might be to implement a method in the |
Very nice investigation, good idea to look at the functionality after a pickle roundtrip. I'm still astonished that from functools import update_wrapper
class Mock:
def foo(self, x, y=3) -> int:
"""Some docs here"""
return x - y
class Wrapper:
def __init__(self, mock):
self.foo = update_wrapper(self.foo, mock.foo)
def foo(self, x, y=10) -> float:
"""Other docs"""
return x + y
wrapper = Wrapper(Mock())
# raises:
# ---> 56 setattr(wrapper, attr, value)
# AttributeError: 'method' object has no attribute '__module__'
# entering debugger
ipdb> hasattr(wrapper, '__module__')
True
ipdb> setattr(wrapper, '__module__', value)
*** AttributeError: 'method' object has no attribute '__module__' I tried a few variations of this, nothing worked. Coming back to the issue at hand, I think at this point, the solution is too complicated for what is, at least in theory, a very simple thing. I wonder if it wouldn't be easier to just update the
As for your PR, I think the code could probably be factored out as a stand-alone function which could be put somewhere (a gist or maybe even PEFT docs) and then we can reference it somewhere for people who really wish to update their docs/signatures etc. This way, people can opt in but the PEFT code itself does not have this somewhat complicated piece of code, which increases maintenance burden. |
Yes, I had suspicions it would not load correctly even if it saved succesfully. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making the updates. Could you please undo the unrelated changes concerning the import order?
Regarding this new function, I'm not sure if it's really a utility function. I think utility functions are more like functions used by other PEFT classes / functions under the hood, i.e. deep down in the call stack. For me, update_forward_signature
is more of a helper function, which sits at the top of the call stack. So I would rather introduce a new Python module for helper functions and put it there.
Before putting any more effort into this PR though, I would first like to get the opinion of @pacman100 whether we want to have this functionality in PEFT or not.
I tried something else which looked like it could work: class BoundMethod:
def __init__(self, f):
self.f = f
update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
def __call__(self, *args, **kwds):
return self.f(*args, **kwds)
# in PeftModel.__init__
self.forward = BoundMethod(model.forward) Unfortunately, this manages to somehow detach the forward call, resulting in gradients not being computed :-/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this puzzled me a lot too. Earlier doing the following in the init self.forward=self.get_base_model.foward
seemed to preserve the signature but failed with torch.nn.DataParallel
. I like the current solution, please address the comments made by Benjamin and then we should be good to go.
I've addressed the above comments, let me know if you would like the function to be in a different file so we can add typing |
Also for some reason I currently don't understand, the below code seems to provide the correct signature from transformers import AutoModelForSeq2SeqLM, WhisperForConditionalGeneration
from peft import get_peft_model, LoraConfig
model = WhisperForConditionalGeneration.from_pretrained("openai/whisper-small")
peft_config = LoraConfig(r=8, lora_alpha=32, lora_dropout=0.1, target_modules=["q_proj", "v_proj"])
peft_model = get_peft_model(model, peft_config)
print(type(peft_model))
help(peft_model.generate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to make the update_forward_signature
agnostic to the method itself? I.e. update_signature(peft_model, method=forward)
? That might require some fiddling with default_forward
(maybe partial
ing it). Or just have two options for forward
and generate
.
Also, what about my suggestion from earlier:
I would rather introduce a new Python module for helper functions and put it there.
Do you think it makes sense?
Also for some reason I currently don't understand, the below code seems to provide the correct signature
Some methods are just forwarding to the base model's method through __getattr__
. In your example, the get_peft_model
returns a PeftModel
, which doesn't have it's own generate
method (and neither has LoraModel
), hence the help you see is the help WhisperForConditionalGeneration
.
Lines 427 to 432 in 7d44026
def __getattr__(self, name: str): | |
"""Forward missing attributes to the wrapped module.""" | |
try: | |
return super().__getattr__(name) # defer to nn.Module's logic | |
except AttributeError: | |
return getattr(self.base_model, name) |
I think the best approach would be to create a function for each different functionality and then update the signature correspondingly
Yes I think it does make a lot of sense, allowing type hints and separting internal utils and external utils. I just wasn't sure if it was a change requirement or something to consider for the future The current implementation that modifies the generate method will only change the signature in those that have been overriden by a PeftModel, since those whose generate method is pulled forward with Thanks for the clarification with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I think it does make a lot of sense, allowing type hints and separting internal utils and external utils. I just wasn't sure if it was a change requirement or something to consider for the future
Thanks for making that change. IMO, we don't need to import it into __init__.py
, I prefer if users need to explicitly import it as from peft.helpers
, this makes it clear that it's a helper function.
The current implementation that modifies the generate method will only change the signature in those that have been overriden by a PeftModel, since those whose generate method is pulled forward with
__getattr__
already have the correct signature
Nice.
Something that has me concerned a bit is that we now have significant code duplication for the generate
methods. I guess we cannot just use the method from the class because of the issues we discussed earlier, thus requiring the use of functions instead of methods? If so, I wonder if it is not better to drop generate
, as the code duplication is a liability (easy to miss to update the functions here).
I think with this change it might be even possible to implement it in the PeftModel |
Oh interesting. If we want to apply that by default, it would require some rigorous testing (i.e. added unit tests) to ensure that we don't break anything. If you're willing to do that and other maintainers agree, we can make the change. Otherwise, I'm also happy with the separate helper functions. (What I still don't understand is what is the issue with methods in general, but that's for another day.) |
I think this works fine for now, if you decide this wants to be implemented by default feel free to tag me, and I'll try to help out |
The documentation is not available anymore as the PR was closed or merged. |
@kiansierra could you please run a |
done, forgot to install the doc-builder |
Oh, sorry I missed it, but could you please remove the imports from
|
removed, sorry I didn't pick that up I was focused on the code duplication at that time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic, great work and nice investigation of the issue. Thanks a lot.
Thanks a lot for your feedback and guidance on this topic |
#783
This PR modifies the signature of the default
PeftModel.forward
to be identical to the one in thebase_model
The output of the above cell is the below which would be the same as for the original model