Skip to content
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

LoftQ: Add LoftQ method integrated into LoRA. Add example code for LoftQ usage. #1150

Merged
merged 12 commits into from
Nov 29, 2023

Conversation

yxli2123
Copy link
Contributor

I have added the LoftQ method to LoRA. I have run the make style command.

@HuggingFaceDocBuilderDev

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint.


def extract_answer_number(sentence: str) -> float:
sentence = sentence.replace(",", "")
pred = [s for s in re.findall(r"-?\d+\.?\d*", sentence)]
Copy link
Member

Choose a reason for hiding this comment

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

Apparently, ruff complains that [s for s in re.findall(r"-?\d+\.?\d*", sentence)] should be list(re.findall(r"-?\d+\.?\d*", sentence)). Fine I guess, but when I tried, findall already returns a list, so neither should be necessary.

What you could do on top is pre-compile the regex. So something like

PATTERN_NUMBER = re.compile(r"-?\d+\.?\d*")

def extract_answer_number(sentence: str) -> float:
    sentence = sentence.replace(",", "")
    pred = PATTERN_NUMBER.findall(sentence)
    ...
        pred_answer = PATTERN_NUMBER.findall(segment[1])
        ...

This should be a bit faster and avoid repeating the same regex.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for your suggestion. I have edited it. Let me know if there is still something I need to do.

@yxli2123
Copy link
Contributor Author

yxli2123 commented Nov 21, 2023

I failed the test because scipy is not installed. Is it possible to add scipy package to the dependency? I may need this package to implement norm.ppf function.

@BenjaminBossan
Copy link
Member

I failed the test because scipy is not installed. Is it possible to add scipy package to the dependency? I may need this package to implement norm.ppf function.

Ah, too bad that this isn't implemented in PyTorch. I would suggest not to add scipy as a standard dependency, as we want to keep those lean. But we can add it as a dev dependency and use a local import of scipy.stats.norm inside of create_normal_map.
On top of that, let's alert users who want to use LoftQ as early as possible if they don't have scipy installed. For this, we can check in the __post_init__ of LoraConfig that scipy is installed if self.init_lora_weights == "loftq".

Alternatively, we could add our own norm.ppf function to PEFT. I checked the scipy code and it's unfortunately not straightforward to just copy it. It seems to be based on special.ndtri, which PyTorch also implements, but I'd have to brush up my math to check how to do this correctly. At least on surface, it seems possible:

image

(ndtri leads to many NaNs though)

WDYT?

Copy link
Contributor

@pacman100 pacman100 left a comment

Choose a reason for hiding this comment

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

Thank you @yxli2123 for super cool work wrt adding LoftQ initialization method for LoRA adapters! 🤩

wrt scipy error, we can overcome by importing from peft.utils.loftq_utils import loftq_init in the def loftq_init(self, adapter_name) method wherein we first raise and error if scipy is not installed.

Apart from that, left a nit suggestion and a question regarding loftq_fake.

examples/loftq_finetuning/README.md Outdated Show resolved Hide resolved
)
loftq_bits: int = field(default=4, metadata={"help": "Quantization bits for LoftQ"})
loftq_iter: int = field(default=1, metadata={"help": "Alternating iterations for LoftQ"})
loftq_fake: bool = field(
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't used anywhere in loftq_utils.py, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The loftq_bits and loftq_iter are used. loftq_fake and bits_pattern are not used, but could be the future feature. Anyway, I have deleted the loftq_fake and bits_pattern.

yxli2123 and others added 2 commits November 26, 2023 11:37
Co-authored-by: Sourab Mangrulkar <13534540+pacman100@users.noreply.github.com>
@yxli2123
Copy link
Contributor Author

Hi, thanks for providing constructive suggestions. I have edited the code. Could you review the code and run test?

Copy link
Member

@BenjaminBossan BenjaminBossan left a comment

Choose a reason for hiding this comment

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

Thanks a lot, I think we're almost finished with the PR.

Could you please run make style so that CI passes? Also, I found a few minor issues, please check my comments. Finally, the license snippet is missing on some files, would you please be so kind to add it? Thanks!

try:
from scipy.stats import norm
except ImportError:
raise ImportError("The required package 'scipy' is not installed. Please install it to continue.")
Copy link
Member

Choose a reason for hiding this comment

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

Could you please import norm inside of create_normal_map, just so that we can be super safe that we don't accidentally introduce a dependency on it? Also, inside of __post_init__ of LoraConfig, let's check that we can successfully import scipy if self.init_lora_weights == "loftq". That way, we can fail as early as possible.

src/peft/utils/loftq_utils.py Show resolved Hide resolved
values /= values.max()
# print(values)
return values
# assert values.
Copy link
Member

Choose a reason for hiding this comment

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

Remove

return weight

def quantize_block(self, weight):
assert len(weight.shape) == 2 and weight.shape[0] * weight.shape[1] % self.block_size == 0
Copy link
Member

Choose a reason for hiding this comment

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

Ideally, we could replace this assert and the ones below by a proper ValueError, including a helpful error message for users.

examples/loftq_finetuning/README.md Outdated Show resolved Hide resolved
yxli2123 and others added 2 commits November 28, 2023 10:21
Co-authored-by: Benjamin Bossan <BenjaminBossan@users.noreply.github.com>
@yxli2123
Copy link
Contributor Author

Thanks again. I have added license to loftq_utils.py, changed assert style to if ... raise ValueError, removed unnecessary print, moved the scipy check to __post_init__, and run make style.

Copy link
Member

@BenjaminBossan BenjaminBossan left a comment

Choose a reason for hiding this comment

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

Thanks a lot for addressing the remaining comments. This is a great PR and a very useful feature to have in PEFT.

For me, the PR is in a state that it can be merged. I'll check if we want to have another final review by a colleague. For the future, we also should add documentation and unit tests. If no one else wants to work on those, I'll try to find some time next week to tackle that.

Copy link
Contributor

@pacman100 pacman100 left a comment

Choose a reason for hiding this comment

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

Great job @yxli2123 on adding LoftQ and addressing all the comments! 🔥🚀✨

@BenjaminBossan
Copy link
Member

Ah, sorry @yxli2123 I merged #1189, which created some merge conflicts, I should have merged this PR first. Would you be so kind to fix them? Otherwise, LMK if I should push the fixes on top of your PR.

@yxli2123
Copy link
Contributor Author

I have edited my branch with the conflicts. It should be fine to just accept my branch when conflicts happen.

@BenjaminBossan BenjaminBossan merged commit 2b901ee into huggingface:main Nov 29, 2023
14 checks passed
@BenjaminBossan
Copy link
Member

Thanks a lot @yxli2123, great PR and super cool feature, I hope it will find a lot of adoption.

@yxli2123
Copy link
Contributor Author

Thank you! We are glad to contribute to this wonderful open-source project.

BenjaminBossan added a commit to BenjaminBossan/peft that referenced this pull request Nov 30, 2023
---------

Co-authored-by: Sourab Mangrulkar <13534540+pacman100@users.noreply.github.com>
Co-authored-by: Benjamin Bossan <BenjaminBossan@users.noreply.github.com>
@BenjaminBossan
Copy link
Member

BenjaminBossan commented Dec 4, 2023

Hi @yxli2123 I think I discovered a small bug. When I try to use LoftQConfig(loftq_bits=8), I get an issue in the following lines:

if not is_bnb_4bit_available():
quantizer = NFQuantizer(num_bits=num_bits, device=device, method="normal", block_size=64)
weight = weight.to(torch.float32)
res = weight.clone()
for i in range(num_iter):
torch.cuda.empty_cache()
# Quantization
if num_bits == 4 and is_bnb_4bit_available():
qweight = bnb.nn.Params4bit(
res.to("cpu"), requires_grad=False, compress_statistics=False, quant_type="nf4"
).to(device)
dequantized_weight = bnb.functional.dequantize_4bit(qweight.data, qweight.quant_state)
else:
quantized_weight, max_abs, shape = quantizer.quantize_block(res)
dequantized_weight = quantizer.dequantize_block(quantized_weight, max_abs, shape)

Since is_bnb_4bit_available() is True, quantizer is undefined. However, the first check for num_bits == 4 is False, so we go to the else condition, which tries to use quantizer, resulting in a NameError. Since there is no bnb.functional.dequantize_8bit, we could proceed similar to how we do when merging LoRA bnb 8bit. Or could we use the NFQuantizer for 8 bit?

The same error would occur for 2 bit, but I don't know how relevant that is right now.

Another error (lower priority) that I encountered was when trying to pass a model that is already quantized. Of course, this is incorrect usage and we should document that. But maybe we can raise a nice error message when we encountered that, as right now, at first it appears that everything works and we only get an error during the forward pass.

Edit: Ran one test and NFQuantizer seems to work with 8bit.

BenjaminBossan added a commit to BenjaminBossan/peft that referenced this pull request Dec 4, 2023
Add GPU tests for LoftQ with 4bit quantization.

Notes

Tests for 8bit quantization are already there but not run at the moment,
see this comment:

huggingface#1150 (comment)

In my testing, 8bit passes when using NFQuantizer, so if the original
author is fine with using that, I can make the adjustment.
@yxli2123
Copy link
Contributor Author

yxli2123 commented Dec 8, 2023

Hi @BenjaminBossan, thanks for pointing it out. As for 8bit, LoftQ doesn't have advantages over QLoRA, so we leave 8bit for experimental programs. However, if it leads to unwanted errors, we could remove 8bit. As for 2bit, since bitsandbytes hasn't supported 2bit yet, we will use Linear4bit as fake replacement. We are working on real 2bit Linear class.

@BenjaminBossan
Copy link
Member

BenjaminBossan commented Dec 8, 2023

Thanks for commenting on this. What would you think about enabling 8bit with the NFQuantizer, even if you didn't find it to work so well empirically? Maybe some users find more success. And it's better than raising an error :) We could add a note on the docs that 4bit is recommended. Regarding 2bit, we could raise an error for the time being with the message that 2bit is not yet supported but might be in the future.

Edit: Solves in #1276.

BenjaminBossan added a commit that referenced this pull request Dec 11, 2023
Add GPU tests for LoftQ with 4bit quantization.

Notes

Tests for 8bit quantization are already there but not run at the moment,
see this comment:

#1150 (comment)

In my testing, 8bit passes when using NFQuantizer, so if the original
author is fine with using that, I can make the adjustment.

---------

Co-authored-by: Younes Belkada <49240599+younesbelkada@users.noreply.github.com>
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.

4 participants